Compare commits

...

4 Commits

4 changed files with 374 additions and 293 deletions

View File

@ -1,7 +1,5 @@
# WebDesk 3rd party App Market server # WebDesk 3rd party App Market server
# THIS APP MARKET SERVER IS IN BETA AND MISSING FEATURES SO DON'T USE IT YET FOR PROD
This allows you to host custom (known as 3rd party) App Market repositories of WebDesk applications. This allows you to host custom (known as 3rd party) App Market repositories of WebDesk applications.
## Features ## Features
@ -43,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

31
go.mod
View File

@ -3,3 +3,34 @@ module main.go
go 1.23.1 go 1.23.1
require github.com/joho/godotenv v1.5.1 require github.com/joho/godotenv v1.5.1
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

81
go.sum
View File

@ -1,2 +1,83 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

549
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")
{
port := os.Getenv("PORT") uploadapp.POST("", authMiddleware(), func(c *gin.Context) {
fmt.Printf("Server starting on port %s\n", port) metadataStr := c.PostForm("metadata")
log.Fatal(http.ListenAndServe(":"+port, nil)) file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
return
}
var metadata AppMetadata
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != 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),
})
}
}