This commit is contained in:
2026-07-28 09:58:33 +02:00
commit 22cbf10cc8
11 changed files with 639 additions and 0 deletions
+209
View File
@@ -0,0 +1,209 @@
package routes
import (
"crypto/sha256"
"encoding/hex"
"io"
"kooltab-updateserver/internal/apk_util"
"kooltab-updateserver/internal/auth"
"kooltab-updateserver/internal/db"
"kooltab-updateserver/internal/models"
"net/http"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/gin-gonic/gin"
)
func Register(r *gin.Engine) {
api := r.Group("/api")
{
api.GET("/update", CheckUpdate)
api.GET("/releases", ListReleases)
}
admin := r.Group("/admin")
admin.Use(auth.AdminAuth())
{
admin.POST("/releases", UploadRelease)
admin.DELETE("/releases/:id", DeleteRelease)
}
}
func CheckUpdate(c *gin.Context) {
current := c.Query("version")
var latest models.Release
if err := db.DB.
Order("created_at desc").
First(&latest).Error; err != nil {
c.JSON(http.StatusOK, gin.H{
"update": false,
})
return
}
cv, err1 := semver.NewVersion(current)
lv, err2 := semver.NewVersion(latest.Version)
if err1 != nil || err2 != nil {
c.JSON(http.StatusOK, gin.H{
"update": latest.Version != current,
})
return
}
if !lv.GreaterThan(cv) {
c.JSON(http.StatusOK, gin.H{
"update": false,
})
return
}
c.JSON(http.StatusOK, gin.H{
"update": true,
"version": latest.Version,
"notes": latest.Notes,
"mandatory": latest.Mandatory,
"sha256": latest.SHA256,
"download": "/downloads/" + latest.APKFile,
})
}
func ListReleases(c *gin.Context) {
var releases []models.Release
db.DB.
Order("created_at desc").
Find(&releases)
c.JSON(http.StatusOK, releases)
}
func UploadRelease(c *gin.Context) {
notes := c.PostForm("notes")
mandatory := c.PostForm("mandatory") == "true"
file, err := c.FormFile("apk")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "APK required",
})
return
}
// Save temporarily first because we need to inspect the APK
tempPath := filepath.Join("uploads", "temp-"+file.Filename)
if err := c.SaveUploadedFile(file, tempPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
defer os.Remove(tempPath)
// Extract version information from APK
info, err := apk_util.Read(tempPath)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid APK: " + err.Error(),
})
return
}
filename := "app-" + info.Version + ".apk"
path := filepath.Join("uploads", filename)
// Prevent overwriting an existing release
if _, err := os.Stat(path); err == nil {
c.JSON(http.StatusConflict, gin.H{
"error": "version already exists",
})
return
}
// Move temp APK to final filename
if err := os.Rename(tempPath, path); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
// Calculate SHA256
hash := sha256.New()
f, err := os.Open(path)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
defer f.Close()
if _, err := io.Copy(hash, f); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
release := models.Release{
Version: info.Version,
VersionCode: int64(info.VersionCode),
APKFile: filename,
Notes: notes,
Mandatory: mandatory,
SHA256: hex.EncodeToString(hash.Sum(nil)),
}
if err := db.DB.Create(&release).Error; err != nil {
// cleanup uploaded file if DB fails
os.Remove(path)
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, release)
}
func DeleteRelease(c *gin.Context) {
var release models.Release
if err := db.DB.First(&release, c.Param("id")).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Not found",
})
return
}
os.Remove(filepath.Join("uploads", release.APKFile))
db.DB.Delete(&release)
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}