package admin import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "time" ) type UpdateInfo struct { Version string `json:"version"` Platform string `json:"platform"` FileName string `json:"filename"` SHA256 string `json:"sha256"` Size int64 `json:"size"` ReleaseNotes string `json:"release_notes"` Mandatory bool `json:"mandatory"` PublishedAt string `json:"published_at"` } var updatesDir string func initUpdatesDir() { exe, _ := os.Executable() updatesDir = filepath.Join(filepath.Dir(exe), "data", "updates") os.MkdirAll(updatesDir, 0755) } func RegisterUpdateRoutes(mux *http.ServeMux) { initUpdatesDir() mux.HandleFunc("/api/update/check", handleUpdateCheck) mux.Handle("/updates/", http.StripPrefix("/updates/", http.FileServer(http.Dir(updatesDir)))) mux.HandleFunc("/api/admin/publish_update", authWrap(handlePublishUpdate)) mux.HandleFunc("/api/admin/update_info", handleGetUpdateInfo) mux.HandleFunc("/api/admin/delete_update", authWrap(handleDeleteUpdate)) } func getUpdateInfoPath() string { return filepath.Join(updatesDir, "latest.json") } func loadUpdateInfo() *UpdateInfo { data, err := os.ReadFile(getUpdateInfoPath()) if err != nil { return nil } var info UpdateInfo if json.Unmarshal(data, &info) != nil { return nil } return &info } func saveUpdateInfo(info *UpdateInfo) error { data, _ := json.MarshalIndent(info, "", " ") return os.WriteFile(getUpdateInfoPath(), data, 0644) } func handleUpdateCheck(w http.ResponseWriter, r *http.Request) { clientVersion := r.URL.Query().Get("version") info := loadUpdateInfo() w.Header().Set("Content-Type", "application/json") if info == nil || info.Version == clientVersion { json.NewEncoder(w).Encode(map[string]interface{}{ "update_available": false, "current_version": clientVersion, }) return } json.NewEncoder(w).Encode(map[string]interface{}{ "update_available": true, "latest_version": info.Version, "download_url": "/updates/" + info.FileName, "sha256": info.SHA256, "size": info.Size, "release_notes": info.ReleaseNotes, "mandatory": info.Mandatory, }) } func handleGetUpdateInfo(w http.ResponseWriter, r *http.Request) { info := loadUpdateInfo() w.Header().Set("Content-Type", "application/json") if info == nil { json.NewEncoder(w).Encode(map[string]interface{}{"published": false}) return } json.NewEncoder(w).Encode(map[string]interface{}{ "published": true, "info": info, }) } func handleDeleteUpdate(w http.ResponseWriter, r *http.Request) { info := loadUpdateInfo() if info != nil { os.Remove(filepath.Join(updatesDir, info.FileName)) os.Remove(getUpdateInfoPath()) } json.NewEncoder(w).Encode(map[string]interface{}{"ok": true}) } func handlePublishUpdate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", 405) return } r.ParseMultipartForm(200 << 20) version := r.FormValue("version") notes := r.FormValue("release_notes") mandatory := r.FormValue("mandatory") == "true" file, header, err := r.FormFile("file") if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "文件上传失败"}) return } defer file.Close() dstPath := filepath.Join(updatesDir, header.Filename) dst, err := os.Create(dstPath) if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "无法创建文件"}) return } hasher := sha256.New() size, _ := io.Copy(io.MultiWriter(dst, hasher), file) dst.Close() hash := hex.EncodeToString(hasher.Sum(nil)) info := &UpdateInfo{ Version: version, Platform: "win64", FileName: header.Filename, SHA256: hash, Size: size, ReleaseNotes: notes, Mandatory: mandatory, PublishedAt: time.Now().Format("2006-01-02 15:04:05"), } saveUpdateInfo(info) log.Printf("Update published: v%s (%s, %d bytes)", version, header.Filename, size) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "info": info}) } func FormatSize(b int64) string { if b < 1024 { return fmt.Sprintf("%d B", b) } else if b < 1024*1024 { return fmt.Sprintf("%.1f KB", float64(b)/1024) } return fmt.Sprintf("%.1f MB", float64(b)/1024/1024) }