@@ -0,0 +1,259 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user