341 lines
8.7 KiB
Go
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()
|
|
}
|