make massive changes and improve stability

This commit is contained in:
matu6968 2024-11-21 01:24:55 +01:00
parent cad79181c1
commit 5f6091e60c
2 changed files with 262 additions and 291 deletions

View File

@ -41,11 +41,11 @@ In the .env file this is the only thing you can set
``` ```
PORT=8080 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 AUTH_TOKEN=bearer-token-here # The program will automatically make a new token if not found so do not bother putting your own one
``` ```
# Client demonstrations # Client demonstrations
[JavaScript](https://git.fluffy.pw/matu6968/webdesk-app-market-server/src/branch/main/client-demo/demo.js) [Go](https://git.fluffy.pw/matu6968/webdesk-app-market-client)
## Autostart with systemd or OpenRC ## Autostart with systemd or OpenRC

547
main.go
View File

@ -1,313 +1,284 @@
package main package main
import ( import (
"crypto/rand" "encoding/json"
"encoding/hex" "fmt"
"encoding/json" "io/ioutil"
"fmt" "math/rand"
"io" "net/http"
"io/ioutil" "os"
"log" "path/filepath"
"mime/multipart" "strings"
"net/http" "time"
"os"
"path/filepath"
"sync"
"strings"
"github.com/joho/godotenv" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/joho/godotenv"
) )
type App struct { type App struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"ver"` Ver string `json:"ver"`
ID string `json:"appid"` AppID string `json:"appid"`
Info string `json:"info"` Info string `json:"info"`
Developer string `json:"pub"` Pub string `json:"pub"`
Path string `json:"path"` Path string `json:"path"`
} }
var ( type AppMetadata struct {
apps []App Name string `json:"name"`
appsFilePath = "apps.json" Ver string `json:"ver"`
uploadDir = "uploads" Info string `json:"info"`
bearerToken string Pub string `json:"pub"`
mutex sync.Mutex
)
func loadApps() error {
data, err := ioutil.ReadFile(appsFilePath)
if err != nil {
return err
}
return json.Unmarshal(data, &apps)
} }
func saveApps() error { func init() {
data, err := json.MarshalIndent(apps, "", " ") // Load .env file
if err != nil { if err := godotenv.Load(); err != nil {
return err // Create .env if it doesn't exist
} authToken := uuid.New().String()
return ioutil.WriteFile(appsFilePath, data, 0644) defaultPort := "8080"
envContent := fmt.Sprintf("AUTH_TOKEN=%s\nPORT=%s", authToken, defaultPort)
ioutil.WriteFile(".env", []byte(envContent), 0644)
godotenv.Load()
}
} }
func generateToken() (string, error) { func authMiddleware() gin.HandlerFunc {
tokenBytes := make([]byte, 16) return func(c *gin.Context) {
if _, err := rand.Read(tokenBytes); err != nil { authHeader := c.GetHeader("Authorization")
return "", err if authHeader == "" {
} c.JSON(http.StatusUnauthorized, gin.H{"error": "No authorization header"})
return hex.EncodeToString(tokenBytes), nil c.Abort()
} return
}
func loadOrGenerateToken() error { token := strings.Replace(authHeader, "Bearer ", "", 1)
if err := godotenv.Load(); err != nil { if token != os.Getenv("AUTH_TOKEN") {
log.Println("No .env file found. Generating a new token.") c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
} c.Abort()
bearerToken = os.Getenv("TOKEN") return
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 { c.Next()
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() { func main() {
if err := loadOrGenerateToken(); err != nil { r := gin.Default()
log.Fatalf("Error loading or generating token: %v", err)
}
if err := loadApps(); err != nil { // Serve static files
if os.IsNotExist(err) { r.Static("/apps/files", "./apps/files")
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)))) // Group routes for /apps
apps := r.Group("/apps")
{
apps.GET("", func(c *gin.Context) {
data, err := ioutil.ReadFile("apps.json")
if err != nil {
c.JSON(http.StatusOK, []App{})
return
}
http.HandleFunc("/apps", func(w http.ResponseWriter, r *http.Request) { var apps []App
switch r.Method { json.Unmarshal(data, &apps)
case http.MethodGet: c.JSON(http.StatusOK, apps)
listAppsHandler(w, r) })
case http.MethodPost: // Handle other methods for /apps
uploadAppHandler(w, r) apps.Handle("POST", "", methodNotAllowedHandler("GET"))
default: apps.Handle("PUT", "", methodNotAllowedHandler("GET"))
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) apps.Handle("DELETE", "", methodNotAllowedHandler("GET"))
} apps.Handle("PATCH", "", methodNotAllowedHandler("GET"))
}) }
http.HandleFunc("/delete", deleteAppHandler) // Group routes for /uploadapp
http.HandleFunc("/editapp", editAppHandler) uploadapp := r.Group("/uploadapp")
{
uploadapp.POST("", authMiddleware(), func(c *gin.Context) {
metadataStr := c.PostForm("metadata")
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
return
}
port := os.Getenv("PORT") var metadata AppMetadata
fmt.Printf("Server starting on port %s\n", port) if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {
log.Fatal(http.ListenAndServe(":"+port, nil)) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid metadata"})
return
}
// Generate 12-digit app ID
rand.Seed(time.Now().UnixNano())
appID := fmt.Sprintf("%012d", rand.Intn(1000000000000))
// Create new app entry
newApp := App{
Name: metadata.Name,
Ver: metadata.Ver,
AppID: appID,
Info: metadata.Info,
Pub: metadata.Pub,
Path: fmt.Sprintf("/apps/files/%s_%s%s", metadata.Name, metadata.Ver, filepath.Ext(file.Filename)),
}
// Save file
if err := os.MkdirAll("apps/files", 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create directory"})
return
}
if err := c.SaveUploadedFile(file, "."+newApp.Path); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
return
}
// Update apps.json
var apps []App
data, _ := ioutil.ReadFile("apps.json")
json.Unmarshal(data, &apps)
apps = append(apps, newApp)
appsJSON, _ := json.Marshal(apps)
ioutil.WriteFile("apps.json", appsJSON, 0644)
c.JSON(http.StatusOK, newApp)
})
uploadapp.Handle("GET", "", methodNotAllowedHandler("POST"))
uploadapp.Handle("PUT", "", methodNotAllowedHandler("POST"))
uploadapp.Handle("DELETE", "", methodNotAllowedHandler("POST"))
uploadapp.Handle("PATCH", "", methodNotAllowedHandler("POST"))
}
// Group routes for /editapp
editapp := r.Group("/editapp")
{
editapp.PUT("", authMiddleware(), func(c *gin.Context) {
// Get metadata from form
metadataStr := c.PostForm("metadata")
if metadataStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Metadata is required"})
return
}
var updateData struct {
AppID string `json:"appid"`
App AppMetadata `json:"app"`
}
if err := json.Unmarshal([]byte(metadataStr), &updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid metadata format"})
return
}
var apps []App
data, _ := ioutil.ReadFile("apps.json")
json.Unmarshal(data, &apps)
found := false
var oldFilePath string
for i := range apps {
if apps[i].AppID == updateData.AppID {
oldFilePath = "." + apps[i].Path
apps[i].Name = updateData.App.Name
apps[i].Ver = updateData.App.Ver
apps[i].Info = updateData.App.Info
// Handle file update if provided
if file, err := c.FormFile("file"); err == nil {
// Delete old file
os.Remove(oldFilePath)
// Generate new path
newPath := fmt.Sprintf("/apps/files/%s_%s%s",
apps[i].Name, apps[i].Ver, filepath.Ext(file.Filename))
apps[i].Path = newPath
// Save new file
if err := c.SaveUploadedFile(file, "."+newPath); err != nil {
c.JSON(http.StatusInternalServerError,
gin.H{"error": "Failed to save new file"})
return
}
}
apps[i].Pub = updateData.App.Pub
found = true
break
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "App not found"})
return
}
appsJSON, _ := json.Marshal(apps)
ioutil.WriteFile("apps.json", appsJSON, 0644)
c.JSON(http.StatusOK, gin.H{"message": "App updated successfully"})
})
editapp.Handle("GET", "", methodNotAllowedHandler("PUT"))
editapp.Handle("POST", "", methodNotAllowedHandler("PUT"))
editapp.Handle("DELETE", "", methodNotAllowedHandler("PUT"))
editapp.Handle("PATCH", "", methodNotAllowedHandler("PUT"))
}
// Group routes for /deleteapp
deleteapp := r.Group("/deleteapp")
{
deleteapp.DELETE("", authMiddleware(), func(c *gin.Context) {
var request struct {
AppID string `json:"appid"`
}
if err := c.BindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
var apps []App
data, _ := ioutil.ReadFile("apps.json")
json.Unmarshal(data, &apps)
found := false
var filePath string
for i := range apps {
if apps[i].AppID == request.AppID {
filePath = "." + apps[i].Path
apps = append(apps[:i], apps[i+1:]...)
found = true
break
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "App not found"})
return
}
// Delete the file
os.Remove(filePath)
// Update apps.json
appsJSON, _ := json.Marshal(apps)
ioutil.WriteFile("apps.json", appsJSON, 0644)
c.JSON(http.StatusOK, gin.H{"message": "App deleted successfully"})
})
deleteapp.Handle("GET", "", methodNotAllowedHandler("DELETE"))
deleteapp.Handle("POST", "", methodNotAllowedHandler("DELETE"))
deleteapp.Handle("PUT", "", methodNotAllowedHandler("DELETE"))
deleteapp.Handle("PATCH", "", methodNotAllowedHandler("DELETE"))
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r.Run(":" + port)
} }
// Helper function to create method not allowed handlers
func methodNotAllowedHandler(allowedMethod string) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{
"error": fmt.Sprintf("Method not allowed. Only %s is supported for this endpoint.", allowedMethod),
})
}
}