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() }