feat(apk): 增加公开APK管理接口和CI/CD工作流
Some checks failed
Build Updates Server / build (push) Failing after 10s
Build Updates Server / build-docker (push) Has been skipped

- 新增获取最新APK版本信息和下载APK的公开接口
- 更新README文档,添加APK管理接口的使用说明
- 实现CI/CD工作流,支持自动构建和部署
- 优化APK存储逻辑,确保只保留最新APK文件
This commit is contained in:
lafay
2026-03-26 03:29:44 +08:00
parent fc09fdf8f2
commit 3ac1a2ccf5
8 changed files with 289 additions and 1 deletions

View File

@@ -10,7 +10,9 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
"expo-updates-server-go/internal/model"
"expo-updates-server-go/internal/service"
"expo-updates-server-go/internal/storage"
)
@@ -284,6 +286,50 @@ func (h *Handler) AdminListAPKs(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"apks": apks})
}
func (h *Handler) PublicLatestAPK(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "Expected GET."})
return
}
apk, err := h.svc.GetLatestAPK()
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
return
}
if apk == nil {
writeJSON(w, http.StatusNotFound, map[string]any{"error": "No APK available"})
return
}
resp := model.LatestAPKResponse{
RuntimeVersion: apk.RuntimeVersion,
VersionName: apk.RuntimeVersion,
Size: apk.Size,
DownloadURL: fmt.Sprintf("%s/api/apk/download?runtimeVersion=%s", h.svc.Hostname(), apk.RuntimeVersion),
CreatedAt: apk.CreatedAt.Format(time.RFC3339),
}
writeJSON(w, http.StatusOK, resp)
}
func (h *Handler) PublicDownloadAPK(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "Expected GET."})
return
}
runtimeVersion := r.URL.Query().Get("runtimeVersion")
if runtimeVersion == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "runtimeVersion is required"})
return
}
apkPath, err := h.svc.GetAPKPath(runtimeVersion)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]any{"error": fmt.Sprintf("APK for runtimeVersion %s not found", runtimeVersion)})
return
}
w.Header().Set("Content-Type", "application/vnd.android.package-archive")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=carrot-bbs-%s.apk", runtimeVersion))
http.ServeFile(w, r, apkPath)
}
func (h *Handler) authorizeAdmin(w http.ResponseWriter, r *http.Request) bool {
if h.adminToken == "" {
writeJSON(w, http.StatusForbidden, map[string]any{"error": "ADMIN_TOKEN is not configured"})
@@ -328,7 +374,7 @@ func firstNonEmpty(values ...string) string {
return ""
}
func writeJSON(w http.ResponseWriter, code int, data map[string]any) {
func writeJSON(w http.ResponseWriter, code int, data any) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(data)