init
This commit is contained in:
commit
c1f9cb3dcc
16
LICENSE
Normal file
16
LICENSE
Normal file
|
@ -0,0 +1,16 @@
|
|||
Copyright 2024 matu6968
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the “Software”), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
92
README.md
Normal file
92
README.md
Normal file
|
@ -0,0 +1,92 @@
|
|||
# WebDesk 3rd party App Market server
|
||||
|
||||
This allows you to host custom (known as 3rd party) App Market repositories of WebDesk applications.
|
||||
|
||||
## Features
|
||||
- Listing currently uploaded apps (/apps endpoint)
|
||||
- Showing app source code (under /uploads/app_name/index.js)
|
||||
- Uploading new apps (/apps endpoint with Bearer token)
|
||||
- Editing app info (/editapp endpoint with Bearer token)
|
||||
- Delete apps (/delete endpoint with Bearer token)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go (1.23.1 or later, older will work with go.mod changes to the version)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone https://git.fluffy.pw/matu6968/webdesk-app-market-server
|
||||
```
|
||||
|
||||
2. Go to the project directory:
|
||||
```
|
||||
cd webdesk-app-market-server
|
||||
```
|
||||
|
||||
3. Build the binary and install web renderer dependencies:
|
||||
```
|
||||
go build -o app-market-server
|
||||
```
|
||||
|
||||
4. Execture the binary:
|
||||
```
|
||||
./app-market-server
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
||||
In the .env file this is the only thing you can set
|
||||
|
||||
```
|
||||
PORT=8080
|
||||
TOKEN=bearer-token-here # The program will automatically make a new token if not found so do not bother putting your own one
|
||||
```
|
||||
## Autostart with systemd or OpenRC
|
||||
|
||||
You can autostart the web server using systemd or OpenRC (init scripts are in the init-scripts folder)
|
||||
To use it, edit the script accordingly (edit username on what user it is going to run and the path to the binary on where it will run from)
|
||||
|
||||
## for systemd edit the following lines:
|
||||
|
||||
```
|
||||
; Don't forget to change the value here!
|
||||
; There is no reason to run this program as root, just use your username
|
||||
User=examplename # change this to your username
|
||||
|
||||
WorkingDirectory=/path/to/binary # change this to the path where the binary resides
|
||||
```
|
||||
### and to add it as a service:
|
||||
|
||||
```
|
||||
sudo cp /path/to/cloned/repo/init-scripts/app-market-server.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable app-market-server.service
|
||||
sudo systemctl start app-market-server.service
|
||||
```
|
||||
|
||||
## for OpenRC edit the following lines:
|
||||
|
||||
```
|
||||
command="bash -c cd ~/webdesk-app-market-server && ./app-market-server" # if you have put the eprintclone binary somewhere else change this line
|
||||
# Don't forget to change the value here!
|
||||
# There is no reason to run this program as root, just use your username
|
||||
command_user="userexample" # change this to your usernames
|
||||
```
|
||||
|
||||
### and to add it as a service:
|
||||
|
||||
```
|
||||
sudo cp /path/to/cloned/repo/init-scripts/app-market-server /etc/init.d/
|
||||
sudo rc-update add app-market-server
|
||||
sudo rc-service app-market-server start
|
||||
```
|
||||
|
||||
### How does this work
|
||||
To use the API look at the [wiki](https://git.fluffy.pw/matu6968/webdesk-app-market-server/wiki)
|
||||
|
||||
# !IMPORTANT!
|
||||
|
||||
You will need a directory where your program is with read/write rights otherwaise actions like deleting apps will fail.
|
||||
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module main.go
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
|||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
15
init-scripts/app-market-server
Normal file
15
init-scripts/app-market-server
Normal file
|
@ -0,0 +1,15 @@
|
|||
#!/sbin/openrc-run
|
||||
|
||||
name=app-market-server
|
||||
description="3rd party WebDesk App Market server"
|
||||
|
||||
command="bash -c cd ~/webdesk-app-market-server && ./app-market-server" # if you have put the eprintclone binary somewhere else change this line
|
||||
# Don't forget to change the value here!
|
||||
# There is no reason to run this program as root, just use your username
|
||||
command_user="userexample" # change this to your username
|
||||
|
||||
pidfile="/run/${RC_SVCNAME}.pid"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
}
|
29
init-scripts/app-market-server.service
Executable file
29
init-scripts/app-market-server.service
Executable file
|
@ -0,0 +1,29 @@
|
|||
[Unit]
|
||||
Description=3rd party WebDesk App Market server
|
||||
Requires=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
; Don't forget to change the value here!
|
||||
; There is no reason to run this program as root, just use your username
|
||||
User=examplename # change this to your username
|
||||
|
||||
WorkingDirectory=/path/to/binary # change this to the path where the binary resides
|
||||
|
||||
ExecStart=./app-market-server
|
||||
|
||||
; Always restart the script
|
||||
Restart=always
|
||||
|
||||
; cf. https://www.darkcoding.net/software/the-joy-of-systemd/
|
||||
; /usr, /boot and /etc are read-only
|
||||
ProtectSystem=full
|
||||
; /tmp is isolated from all other processes
|
||||
PrivateTmp=false
|
||||
; Don't allow process to raise privileges (e.g. disable suid)
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
313
main.go
Normal file
313
main.go
Normal file
|
@ -0,0 +1,313 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"ver"`
|
||||
ID string `json:"appid"`
|
||||
Info string `json:"info"`
|
||||
Developer string `json:"pub"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
var (
|
||||
apps []App
|
||||
appsFilePath = "apps.json"
|
||||
uploadDir = "uploads"
|
||||
bearerToken string
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
func loadApps() error {
|
||||
data, err := ioutil.ReadFile(appsFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, &apps)
|
||||
}
|
||||
|
||||
func saveApps() error {
|
||||
data, err := json.MarshalIndent(apps, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(appsFilePath, data, 0644)
|
||||
}
|
||||
|
||||
func generateToken() (string, error) {
|
||||
tokenBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(tokenBytes), nil
|
||||
}
|
||||
|
||||
func loadOrGenerateToken() error {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("No .env file found. Generating a new token.")
|
||||
}
|
||||
bearerToken = os.Getenv("TOKEN")
|
||||
if bearerToken == "" {
|
||||
var err error
|
||||
bearerToken, err = generateToken()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate token: %v", err)
|
||||
}
|
||||
err = saveTokenToEnv(bearerToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save token to .env: %v", err)
|
||||
}
|
||||
log.Printf("Generated new token: %s\n", bearerToken)
|
||||
}
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveTokenToEnv(token string) error {
|
||||
return ioutil.WriteFile(".env", []byte("TOKEN="+token), 0644)
|
||||
}
|
||||
|
||||
func listAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
json.NewEncoder(w).Encode(apps)
|
||||
}
|
||||
|
||||
func saveUploadedFile(customPath, fileName string, file multipart.File) (string, error) {
|
||||
appDir := filepath.Join(uploadDir, customPath)
|
||||
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath := filepath.Join(appDir, fileName)
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/%s/%s/%s", uploadDir, customPath, fileName), nil
|
||||
}
|
||||
|
||||
func uploadAppHandler(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token != "Bearer "+bearerToken {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||
http.Error(w, "Invalid form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
metadata := r.FormValue("metadata")
|
||||
var newApp App
|
||||
if err := json.Unmarshal([]byte(metadata), &newApp); err != nil {
|
||||
http.Error(w, "Invalid metadata JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "File upload error", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
customPath := r.FormValue("customPath")
|
||||
if customPath == "" {
|
||||
customPath = strings.ReplaceAll(newApp.Name, " ", "_")
|
||||
}
|
||||
|
||||
filePath, err := saveUploadedFile(customPath, handler.Filename, file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
newApp.Path = filePath
|
||||
|
||||
mutex.Lock()
|
||||
apps = append(apps, newApp)
|
||||
if err := saveApps(); err != nil {
|
||||
mutex.Unlock()
|
||||
http.Error(w, "Failed to save app data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
mutex.Unlock()
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(newApp)
|
||||
}
|
||||
|
||||
func deleteAppHandler(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token != "Bearer "+bearerToken {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
appID := r.URL.Query().Get("id")
|
||||
if appID == "" {
|
||||
http.Error(w, "App ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
for i, app := range apps {
|
||||
if app.ID == appID {
|
||||
// Remove the app's file if it exists
|
||||
if err := os.Remove(filepath.Join(".", app.Path)); err != nil {
|
||||
log.Printf("Failed to delete file: %v\n", err)
|
||||
}
|
||||
|
||||
// Remove the app from the list
|
||||
apps = append(apps[:i], apps[i+1:]...)
|
||||
if err := saveApps(); err != nil {
|
||||
http.Error(w, "Failed to save apps", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("App deleted successfully"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
func editAppHandler(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token != "Bearer "+bearerToken {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||
http.Error(w, "Invalid form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
appID := r.FormValue("id")
|
||||
if appID == "" {
|
||||
http.Error(w, "App ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var updatedApp *App
|
||||
mutex.Lock()
|
||||
for i := range apps {
|
||||
if apps[i].ID == appID {
|
||||
updatedApp = &apps[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
mutex.Unlock()
|
||||
|
||||
if updatedApp == nil {
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if name := r.FormValue("name"); name != "" {
|
||||
updatedApp.Name = name
|
||||
}
|
||||
|
||||
if info := r.FormValue("info"); info != "" {
|
||||
updatedApp.Info = info
|
||||
}
|
||||
|
||||
// Check if a new file is uploaded
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
customPath := r.FormValue("customPath")
|
||||
if customPath == "" {
|
||||
customPath = strings.ReplaceAll(updatedApp.Name, " ", "_")
|
||||
}
|
||||
|
||||
filePath, err := saveUploadedFile(customPath, handler.Filename, file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the old file
|
||||
os.Remove(filepath.Join(".", updatedApp.Path))
|
||||
updatedApp.Path = filePath
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if err := saveApps(); err != nil {
|
||||
http.Error(w, "Failed to save app data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(updatedApp)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := loadOrGenerateToken(); err != nil {
|
||||
log.Fatalf("Error loading or generating token: %v", err)
|
||||
}
|
||||
|
||||
if err := loadApps(); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Println("apps.json not found. Starting with an empty app list.")
|
||||
} else {
|
||||
log.Fatalf("Failed to load apps.json: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
http.Handle("/uploads/", http.StripPrefix("/uploads", http.FileServer(http.Dir(uploadDir))))
|
||||
|
||||
http.HandleFunc("/apps", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
listAppsHandler(w, r)
|
||||
case http.MethodPost:
|
||||
uploadAppHandler(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/delete", deleteAppHandler)
|
||||
http.HandleFunc("/editapp", editAppHandler)
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
fmt.Printf("Server starting on port %s\n", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user