4baf24f6f8
GET /version(公开)读 /etc/pangolin/version.yaml(每请求重载,免重启);缺省回退。 download_urls 接 /downloads。deploy.sh 装清单(存在则不覆盖, 保住线上 bump)。含 4 测试。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
138 lines
5.4 KiB
Go
138 lines
5.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// defaultVersionManifestPath is VERSION_MANIFEST's default when unset — mirrors
|
|
// how server.env wires DOWNLOADS_DIR alongside DownloadsHandler's own built-in
|
|
// default (see downloads.go / deploy/single-node/deploy.sh).
|
|
const defaultVersionManifestPath = "/etc/pangolin/version.yaml"
|
|
|
|
// Fallback values used ONLY when the manifest file itself is missing (fresh
|
|
// box that hasn't run deploy/single-node/deploy.sh's manifest-install step
|
|
// yet, or a local dev server). These are hand-set, not auto-derived — once a
|
|
// box is deployed, the real source of truth is the on-disk manifest that
|
|
// scripts/ci/release-client.sh overwrites on every client-v* release.
|
|
const (
|
|
defaultManifestVersion = "1.0.48"
|
|
defaultManifestBuildNumber = 10048
|
|
)
|
|
|
|
// changelogSection / changelogEntry mirror jiu's backend/config/version.yaml
|
|
// shape (~/code/jiu/backend/internal/handler/version.go) so any future shared
|
|
// tooling (e.g. a website changelog widget) can treat both services'
|
|
// /version responses identically. Pangolin's release pipeline doesn't
|
|
// populate Changelog yet (no CHANGELOG-client.md parsing wired in
|
|
// scripts/ci/release-client.sh) — the field exists for shape-compatibility
|
|
// and always serializes as [] rather than null.
|
|
type changelogSection struct {
|
|
Type string `yaml:"type" json:"type"`
|
|
Items []string `yaml:"items" json:"items"`
|
|
}
|
|
|
|
type changelogEntry struct {
|
|
Version string `yaml:"version" json:"version"`
|
|
Date string `yaml:"date" json:"date"`
|
|
Intro string `yaml:"intro" json:"intro"`
|
|
Sections []changelogSection `yaml:"sections" json:"sections"`
|
|
}
|
|
|
|
// versionManifest is the on-disk (and wire) shape of the auto-update
|
|
// manifest. download_urls keys in practice: macos, windows, ios, android
|
|
// (web is intentionally not used — pangolin's client is native-only).
|
|
type versionManifest struct {
|
|
Version string `yaml:"version" json:"version"`
|
|
BuildNumber int `yaml:"build_number" json:"build_number"`
|
|
ForceUpdate bool `yaml:"force_update" json:"force_update"`
|
|
ReleaseNotes string `yaml:"release_notes" json:"release_notes"`
|
|
DownloadURLs map[string]string `yaml:"download_urls" json:"download_urls"`
|
|
Changelog []changelogEntry `yaml:"changelog" json:"changelog"`
|
|
}
|
|
|
|
// VersionHandler serves GET /version (public, no auth — mounted directly in
|
|
// main.go next to /healthz and /downloads/*). Unlike most handlers in this
|
|
// package it re-reads its manifest file from disk on EVERY request rather
|
|
// than caching it in memory: scripts/ci/release-client.sh rewrites
|
|
// /etc/pangolin/version.yaml's version/build_number on each client-v*
|
|
// release, and that must take effect immediately without a control-plane
|
|
// restart or redeploy (mirrors jiu's loadVersionConfig()-per-request).
|
|
type VersionHandler struct {
|
|
path string
|
|
}
|
|
|
|
// NewVersionHandler builds a VersionHandler reading the manifest at path.
|
|
// path=="" falls back to defaultVersionManifestPath (in production this is
|
|
// overridden via the VERSION_MANIFEST env var — see main.go).
|
|
func NewVersionHandler(path string) *VersionHandler {
|
|
if path == "" {
|
|
path = defaultVersionManifestPath
|
|
}
|
|
return &VersionHandler{path: path}
|
|
}
|
|
|
|
// defaultManifest is returned when the manifest file doesn't exist yet, so
|
|
// GET /version still answers usefully (client update-checks shouldn't hard
|
|
// fail just because deploy/single-node/deploy.sh hasn't run on this box).
|
|
func defaultManifest() versionManifest {
|
|
return versionManifest{
|
|
Version: defaultManifestVersion,
|
|
BuildNumber: defaultManifestBuildNumber,
|
|
ForceUpdate: false,
|
|
ReleaseNotes: "",
|
|
DownloadURLs: map[string]string{
|
|
"android": "https://api.yanmeiai.com/downloads/pangolin-android.apk",
|
|
"windows": "https://api.yanmeiai.com/downloads/pangolin-windows-x64-setup.exe",
|
|
"macos": "",
|
|
"ios": "",
|
|
},
|
|
Changelog: []changelogEntry{},
|
|
}
|
|
}
|
|
|
|
// loadManifest reads+parses h.path. A missing file is NOT an error (falls
|
|
// back to defaultManifest()); any other read/parse failure IS, so ServeHTTP
|
|
// can 500 instead of silently masking a corrupt manifest written by a bad
|
|
// release-client.sh run.
|
|
func (h *VersionHandler) loadManifest() (versionManifest, error) {
|
|
data, err := os.ReadFile(h.path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return defaultManifest(), nil
|
|
}
|
|
return versionManifest{}, err
|
|
}
|
|
var m versionManifest
|
|
if err := yaml.Unmarshal(data, &m); err != nil {
|
|
return versionManifest{}, err
|
|
}
|
|
// Keep the JSON response shape stable (empty object/array, never null)
|
|
// regardless of what the manifest on disk happens to omit.
|
|
if m.DownloadURLs == nil {
|
|
m.DownloadURLs = map[string]string{}
|
|
}
|
|
if m.Changelog == nil {
|
|
m.Changelog = []changelogEntry{}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// Serve handles GET /version. Named Serve (not ServeHTTP) to match
|
|
// DownloadsHandler's convention in this package (see downloads.go).
|
|
func (h *VersionHandler) Serve(w http.ResponseWriter, r *http.Request) {
|
|
m, err := h.loadManifest()
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": "version manifest unavailable"})
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(m)
|
|
}
|