admin_backend/controller/apk.go
2026-05-13 14:09:48 +08:00

341 lines
8.7 KiB
Go

package controller
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"backend/util"
"github.com/gin-gonic/gin"
)
const (
apkStorageRoot = "./runtime/apk"
apkManifestName = "manifest.json"
apkStaticURLPrefix = "/apk-static"
defaultApkUploadToken = "apk-upload-token"
apkUploadTokenHeader = "X-Apk-Upload-Token"
apkUploadTokenEnvName = "APK_UPLOAD_TOKEN"
uploadedAtTimeFormat = time.RFC3339
defaultApkStorageName = "current.apk"
defaultApkPackageType = "without_sdk"
)
var apkEnvironments = []string{"dev", "stable", "prod"}
var apkPackageTypes = []string{"with_sdk", "without_sdk"}
type apkPackageMeta struct {
DownloadPath string `json:"downloadPath"`
Env string `json:"env"`
PackageType string `json:"packageType"`
Exists bool `json:"exists"`
FileName string `json:"fileName"`
Size int64 `json:"size"`
UploadedAt string `json:"uploadedAt"`
Version string `json:"version"`
}
type apkEnvPackages struct {
Env string `json:"env"`
Variants map[string]apkPackageMeta `json:"variants"`
}
type apkManifest struct {
Packages map[string]apkEnvPackages `json:"packages"`
}
func UploadApkPackage(c *gin.Context) {
if !validateApkUploadToken(c.GetHeader(apkUploadTokenHeader)) {
failed(c, "上传令牌无效")
return
}
env := strings.TrimSpace(c.PostForm("env"))
if !isValidApkEnv(env) {
failed(c, "无效的 apk 环境,必须是 dev、stable 或 prod")
return
}
packageType, err := parseApkPackageType(c.PostForm("packageType"))
if err != nil {
failed(c, err.Error())
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
failed(c, "获取 apk 文件失败: "+err.Error())
return
}
if strings.ToLower(filepath.Ext(fileHeader.Filename)) != ".apk" {
failed(c, "仅支持上传 .apk 文件")
return
}
if err := os.MkdirAll(apkPackageDir(env, packageType), 0755); err != nil {
failed(c, "创建 apk 目录失败: "+err.Error())
return
}
targetPath := apkFilePath(env, packageType)
if err := c.SaveUploadedFile(fileHeader, targetPath); err != nil {
failed(c, "保存 apk 文件失败: "+err.Error())
return
}
if err := os.Chmod(filepath.Dir(targetPath), 0777); err != nil {
failed(c, "设置 apk 目录权限失败: "+err.Error())
return
}
if err := os.Chmod(targetPath, 0777); err != nil {
failed(c, "设置 apk 文件权限失败: "+err.Error())
return
}
version := strings.TrimSpace(c.PostForm("version"))
if version == "" {
version = strings.TrimSuffix(fileHeader.Filename, filepath.Ext(fileHeader.Filename))
}
meta := apkPackageMeta{
DownloadPath: apkDownloadPath(env, packageType),
Env: env,
PackageType: packageType,
Exists: true,
FileName: fileHeader.Filename,
Size: fileHeader.Size,
UploadedAt: time.Now().Format(uploadedAtTimeFormat),
Version: version,
}
manifest, err := loadApkManifest()
if err != nil {
failed(c, "读取 apk 清单失败: "+err.Error())
return
}
envPackages := ensureEnvPackages(manifest, env)
envPackages.Variants[packageType] = meta
manifest.Packages[env] = envPackages
if err := saveApkManifest(manifest); err != nil {
failed(c, "保存 apk 清单失败: "+err.Error())
return
}
log.Printf("apk uploaded env=%s packageType=%s file=%s size=%d", env, packageType, fileHeader.Filename, fileHeader.Size)
success(c, meta)
}
func GetApkPackages(c *gin.Context) {
manifest, err := loadApkManifest()
if err != nil {
failed(c, "读取 apk 清单失败: "+err.Error())
return
}
packages := make([]apkEnvPackages, 0, len(apkEnvironments))
for _, env := range apkEnvironments {
envPackages := ensureEnvPackages(manifest, env)
for _, packageType := range apkPackageTypes {
meta := envPackages.Variants[packageType]
if meta.Env == "" {
meta = apkPackageMeta{Env: env, PackageType: packageType}
}
meta.Exists = fileExists(apkFilePath(env, packageType))
meta.DownloadPath = apkDownloadPath(env, packageType)
meta.PackageType = packageType
envPackages.Variants[packageType] = meta
}
packages = append(packages, envPackages)
}
success(c, packages)
}
func DownloadApkPackage(c *gin.Context) {
env := c.Param("env")
if !isValidApkEnv(env) {
failed(c, "无效的 apk 环境")
return
}
packageType, err := parseApkPackageType(c.Query("packageType"))
if err != nil {
failed(c, err.Error())
return
}
manifest, err := loadApkManifest()
if err != nil {
failed(c, "读取 apk 清单失败: "+err.Error())
return
}
targetPath := apkFilePath(env, packageType)
if !fileExists(targetPath) {
failed(c, fmt.Sprintf("%s 环境暂无可下载的 %s apk 包", env, packageType))
return
}
envPackages := ensureEnvPackages(manifest, env)
meta := envPackages.Variants[packageType]
downloadName := meta.FileName
if strings.TrimSpace(downloadName) == "" {
downloadName = fmt.Sprintf("%s-%s.apk", env, packageType)
}
util.AddAdminLog(c, "下载客户端APK", gin.H{"env": env, "packageType": packageType, "file": downloadName})
c.FileAttachment(targetPath, downloadName)
}
func validateApkUploadToken(token string) bool {
expectedToken := strings.TrimSpace(os.Getenv(apkUploadTokenEnvName))
if expectedToken == "" {
expectedToken = defaultApkUploadToken
}
return strings.TrimSpace(token) != "" && token == expectedToken
}
func apkEnvDir(env string) string {
return filepath.Join(apkStorageRoot, env)
}
func apkPackageDir(env string, packageType string) string {
return filepath.Join(apkEnvDir(env), packageType)
}
func apkFilePath(env string, packageType string) string {
return filepath.Join(apkPackageDir(env, packageType), defaultApkStorageName)
}
func apkManifestPath() string {
return filepath.Join(apkStorageRoot, apkManifestName)
}
func apkDownloadPath(env string, packageType string) string {
return fmt.Sprintf("%s/%s/%s/%s", apkStaticURLPrefix, env, packageType, defaultApkStorageName)
}
func isValidApkEnv(env string) bool {
for _, item := range apkEnvironments {
if item == env {
return true
}
}
return false
}
func parseApkPackageType(rawType string) (string, error) {
value := strings.TrimSpace(strings.ToLower(rawType))
if value == "" {
return defaultApkPackageType, nil
}
aliases := map[string]string{
"sdk": "with_sdk",
"with-sdk": "with_sdk",
"with_sdk": "with_sdk",
"withsdk": "with_sdk",
"nosdk": "without_sdk",
"no-sdk": "without_sdk",
"no_sdk": "without_sdk",
"without-sdk": "without_sdk",
"without_sdk": "without_sdk",
"withoutsdk": "without_sdk",
}
if alias, ok := aliases[value]; ok {
return alias, nil
}
for _, item := range apkPackageTypes {
if item == value {
return value, nil
}
}
return "", fmt.Errorf("无效的 apk 包类型,必须是 with_sdk 或 without_sdk")
}
func ensureEnvPackages(manifest *apkManifest, env string) apkEnvPackages {
envPackages, ok := manifest.Packages[env]
if !ok || envPackages.Env == "" {
envPackages = apkEnvPackages{Env: env, Variants: map[string]apkPackageMeta{}}
}
if envPackages.Variants == nil {
envPackages.Variants = map[string]apkPackageMeta{}
}
return envPackages
}
func loadApkManifest() (*apkManifest, error) {
manifest := &apkManifest{Packages: map[string]apkEnvPackages{}}
if err := os.MkdirAll(apkStorageRoot, 0755); err != nil {
return nil, err
}
data, err := os.ReadFile(apkManifestPath())
if err != nil {
if os.IsNotExist(err) {
return manifest, nil
}
return nil, err
}
if len(data) == 0 {
return manifest, nil
}
var raw struct {
Packages map[string]json.RawMessage `json:"packages"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
for env, rawPackage := range raw.Packages {
var envPackages apkEnvPackages
if err := json.Unmarshal(rawPackage, &envPackages); err == nil && len(envPackages.Variants) > 0 {
if envPackages.Env == "" {
envPackages.Env = env
}
for packageType, meta := range envPackages.Variants {
meta.Env = env
meta.PackageType = packageType
envPackages.Variants[packageType] = meta
}
manifest.Packages[env] = envPackages
continue
}
var legacyMeta apkPackageMeta
if err := json.Unmarshal(rawPackage, &legacyMeta); err != nil {
return nil, err
}
legacyMeta.Env = env
legacyMeta.PackageType = defaultApkPackageType
manifest.Packages[env] = apkEnvPackages{
Env: env,
Variants: map[string]apkPackageMeta{
defaultApkPackageType: legacyMeta,
},
}
}
return manifest, nil
}
func saveApkManifest(manifest *apkManifest) error {
data, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return err
}
return os.WriteFile(apkManifestPath(), data, 0644)
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}