init
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package apk_util
|
||||
|
||||
import (
|
||||
"github.com/shogo82148/androidbinary/apk"
|
||||
)
|
||||
|
||||
// Info holds basic APK version information.
|
||||
type Info struct {
|
||||
Version string
|
||||
VersionCode int32
|
||||
}
|
||||
|
||||
// Version returns the versionName from an APK's manifest.
|
||||
func Version(path string) (string, error) {
|
||||
a, err := apk.OpenFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer a.Close()
|
||||
|
||||
v, err := a.Manifest().VersionName.String()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Read returns version name and version code from an APK's manifest.
|
||||
func Read(path string) (*Info, error) {
|
||||
a, err := apk.OpenFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer a.Close()
|
||||
|
||||
manifest := a.Manifest()
|
||||
|
||||
version, err := manifest.VersionName.String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code, err := manifest.VersionCode.Int32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Info{
|
||||
Version: version,
|
||||
VersionCode: code,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AdminAuth() gin.HandlerFunc {
|
||||
|
||||
token := os.Getenv("ADMIN_TOKEN")
|
||||
|
||||
return func(c *gin.Context) {
|
||||
|
||||
if c.GetHeader("X-Admin-Token") != token {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"kooltab-updateserver/internal/models"
|
||||
"log"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func Connect() {
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("updater.db"), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
DB = db
|
||||
|
||||
if err := DB.AutoMigrate(&models.Release{}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Release struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Version string `gorm:"unique;not null"` // versionName
|
||||
VersionCode int64 `gorm:"not null"` // Android versionCode
|
||||
|
||||
APKFile string `gorm:"not null"`
|
||||
Notes string
|
||||
Mandatory bool
|
||||
SHA256 string
|
||||
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package internal
|
||||
Reference in New Issue
Block a user