package releases import ( "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "os" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "ymhut-box/server/unified-management/internal/config" "ymhut-box/server/unified-management/internal/db" "ymhut-box/server/unified-management/internal/notices" ) type Service struct { cfg *config.Config store *db.Store notices *notices.Service } type Package struct { ID string `json:"id"` Name string `json:"name"` Version string `json:"version"` Platform string `json:"platform"` Arch string `json:"arch"` URL string `json:"url"` SHA256 string `json:"sha256"` Size int64 `json:"size"` Required bool `json:"required"` Enabled bool `json:"enabled"` FileName string `json:"fileName"` UpdatedAt string `json:"updatedAt"` } func NewService(cfg *config.Config, store *db.Store, noticeService ...*notices.Service) *Service { service := &Service{cfg: cfg, store: store} if len(noticeService) > 0 { service.notices = noticeService[0] } return service } func (s *Service) LegacyUpdateInfo(r *http.Request) map[string]any { payload := readJSON(filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")) manifest := s.Manifest(r) for _, key := range []string{"app_version", "download_url", "download_mirrors", "detected_product", "detected_packages", "packages", "modules", "manifest_version", "release_notes", "release_notes_md", "message", "message_md", "notices", "latest_notice"} { if value, ok := manifest[key]; ok { payload[key] = value } } return payload } func (s *Service) Manifest(r *http.Request) map[string]any { payload := readJSON(filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")) packages := s.ScanPackages(r) modules := readJSON(filepath.Join(s.cfg.UpdatePublicDir, "modules.json"))["modules"] if modules == nil { modules = []any{} } payload["manifest_version"] = 2 payload["service_version"] = config.Version payload["packages"] = packages payload["modules"] = modules payload["assets"] = []any{} payload["generated_at"] = time.Now().UTC().Format(time.RFC3339) if s.notices != nil { if items, err := s.notices.List(50); err == nil && len(items) > 0 { publicNotices := notices.PublicList(items) payload["notices"] = publicNotices payload["latest_notice"] = publicNotices[0] latestNotice := items[0] setIfMissing(payload, "app_version", latestNotice.Version) setIfMissing(payload, "title", latestNotice.Title) setIfMissing(payload, "message", latestNotice.Message) setIfMissing(payload, "message_md", latestNotice.MessageMD) setIfMissing(payload, "release_notes", latestNotice.ReleaseNotes) setIfMissing(payload, "release_notes_md", latestNotice.ReleaseNotesMD) setIfMissing(payload, "download_url", latestNotice.DownloadURL) } } if len(packages) > 0 { latest := packages[0] payload["app_version"] = latest.Version payload["download_url"] = latest.URL payload["download_mirrors"] = []map[string]any{{ "id": "primary", "name": "官方直连", "url": latest.URL, "type": "direct", "sha256": latest.SHA256, "enabled": true, }} payload["detected_product"] = latest.Name } return payload } func setIfMissing(payload map[string]any, key, value string) { if strings.TrimSpace(value) == "" { return } if existing, ok := payload[key].(string); !ok || strings.TrimSpace(existing) == "" { payload[key] = value } } func (s *Service) ScanPackages(r *http.Request) []Package { entries, err := os.ReadDir(s.cfg.DownloadsDir) if err != nil { return []Package{} } base := requestBaseURL(r, s.cfg.BaseURL) items := []Package{} for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() lower := strings.ToLower(name) if !(strings.HasSuffix(lower, ".exe") || strings.HasSuffix(lower, ".msix") || strings.HasSuffix(lower, ".appinstaller") || strings.HasSuffix(lower, ".msi")) { continue } info, err := entry.Info() if err != nil { continue } version := detectVersion(name) platform, arch := detectPlatform(name) product := detectProduct(name) url := base + "/downloads/" + name items = append(items, Package{ ID: strings.ToLower(strings.ReplaceAll(product+"-"+platform+"-"+arch+"-"+version, " ", "-")), Name: product, Version: version, Platform: platform, Arch: arch, URL: url, SHA256: sha256File(filepath.Join(s.cfg.DownloadsDir, name)), Size: info.Size(), Required: strings.Contains(strings.ToLower(product), "ymhut"), Enabled: true, FileName: name, UpdatedAt: info.ModTime().UTC().Format(time.RFC3339), }) } sort.Slice(items, func(i, j int) bool { return compareVersion(items[i].Version, items[j].Version) > 0 }) return items } func (s *Service) StaticJSON(name string) map[string]any { return readJSON(filepath.Join(s.cfg.UpdatePublicDir, name)) } func readJSON(path string) map[string]any { data, err := os.ReadFile(path) if err != nil { return map[string]any{} } var payload map[string]any if err := json.Unmarshal(data, &payload); err != nil { return map[string]any{} } return payload } func requestBaseURL(r *http.Request, fallback string) string { if r != nil { scheme := r.Header.Get("X-Forwarded-Proto") if scheme == "" { if r.TLS != nil { scheme = "https" } else { scheme = "http" } } if r.Host != "" { return scheme + "://" + r.Host } } return strings.TrimRight(fallback, "/") } var versionPattern = regexp.MustCompile(`\d+\.\d+\.\d+(?:\.\d+)?`) func detectVersion(name string) string { match := versionPattern.FindString(name) if match == "" { return "0.0.0" } return match } func detectPlatform(name string) (string, string) { lower := strings.ToLower(name) platform := "windows" if strings.Contains(lower, "appinstaller") || strings.HasSuffix(lower, ".msix") || strings.HasSuffix(lower, ".exe") || strings.HasSuffix(lower, ".msi") { platform = "windows" } arch := "x64" if strings.Contains(lower, "arm64") { arch = "arm64" } else if strings.Contains(lower, "x86") && !strings.Contains(lower, "x64") { arch = "x86" } return platform, arch } func detectProduct(name string) string { if strings.Contains(strings.ToLower(name), "ymhut") { return "YMhut Box" } return "YMhut Package" } func compareVersion(a, b string) int { as := strings.Split(a, ".") bs := strings.Split(b, ".") for len(as) < 4 { as = append(as, "0") } for len(bs) < 4 { bs = append(bs, "0") } for i := 0; i < 4; i++ { ai, _ := strconv.Atoi(as[i]) bi, _ := strconv.Atoi(bs[i]) if ai > bi { return 1 } if ai < bi { return -1 } } return 0 } func sha256File(path string) string { file, err := os.Open(path) if err != nil { return "" } defer file.Close() hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { return "" } return hex.EncodeToString(hash.Sum(nil)) }