Files
YMhut-box-C-/server/unified-management/internal/releases/releases.go
T

877 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package releases
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"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
hashMu sync.Mutex
hashes map[string]cachedFileHash
hashFile func(string) string
packageMu sync.RWMutex
packages []Package
reconcileMu sync.Mutex
callbackMu sync.RWMutex
onChange func()
stop chan struct{}
stopOnce sync.Once
}
type cachedFileHash struct {
size int64
modifiedAt int64
sha256 string
}
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"`
}
type UploadOptions struct {
FileName string
Version string
Platform string
Arch string
Channel string
Notes string
UpdateManifest bool
}
type UploadedPackageFile struct {
TempPath string
Size int64
SHA256 string
}
var (
ErrUploadedPackageMissing = errors.New("uploaded file is missing")
ErrUploadedPackageEmpty = errors.New("uploaded file is empty")
ErrUploadedPackageStorageFailed = errors.New("upload storage failed")
ErrUploadedPackageManifestFailed = errors.New("manifest update failed")
ErrUploadedPackageIndexFailed = errors.New("package index update failed")
ErrUnsupportedPackage = errors.New("unsupported package extension")
ErrUnsafePackageName = errors.New("unsafe package file name")
)
func NewService(cfg *config.Config, store *db.Store, noticeService ...*notices.Service) *Service {
return newService(cfg, store, sha256File, noticeService...)
}
func newService(cfg *config.Config, store *db.Store, hashFile func(string) string, noticeService ...*notices.Service) *Service {
service := &Service{cfg: cfg, store: store, hashes: map[string]cachedFileHash{}, hashFile: hashFile, stop: make(chan struct{})}
if len(noticeService) > 0 {
service.notices = noticeService[0]
}
_ = service.Reconcile(context.Background())
return service
}
func (s *Service) Start(ctx context.Context) {
go func() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_ = s.Reconcile(context.Background())
case <-ctx.Done():
return
case <-s.stop:
return
}
}
}()
}
func (s *Service) Stop() {
s.stopOnce.Do(func() { close(s.stop) })
}
func (s *Service) SetChangeCallback(callback func()) {
s.callbackMu.Lock()
s.onChange = callback
s.callbackMu.Unlock()
}
func (s *Service) notifyChanged() {
s.callbackMu.RLock()
callback := s.onChange
s.callbackMu.RUnlock()
if callback != nil {
callback()
}
}
func (s *Service) LegacyUpdateInfo(r *http.Request) map[string]any {
payload := s.legacyUpdateBase()
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 := s.legacyUpdateBase()
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["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)
}
}
preferredVersion := manifestString(payload, "app_version")
packages = prioritizePackages(packages, preferredVersion)
payload["packages"] = packages
if len(packages) > 0 {
latest := packages[0]
if preferredVersion == "" {
preferredVersion = latest.Version
payload["app_version"] = preferredVersion
}
if sameVersion(latest.Version, preferredVersion) {
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 manifestString(payload map[string]any, key string) string {
value, _ := payload[key].(string)
return strings.TrimSpace(value)
}
func prioritizePackages(items []Package, preferredVersion string) []Package {
if strings.TrimSpace(preferredVersion) == "" {
return items
}
sort.SliceStable(items, func(i, j int) bool {
left := sameVersion(items[i].Version, preferredVersion)
right := sameVersion(items[j].Version, preferredVersion)
return left && !right
})
return items
}
func sameVersion(a, b string) bool {
a = strings.TrimSpace(a)
b = strings.TrimSpace(b)
if a == "" || b == "" {
return false
}
if a == b {
return true
}
return versionPattern.FindString(a) == a && versionPattern.FindString(b) == b && compareVersion(a, b) == 0
}
func (s *Service) PublishLegacyUpdateInfo(r *http.Request, actor string) error {
payload := s.LegacyUpdateInfo(r)
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return err
}
path := filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")
if err := atomicWrite(path, append(data, '\n')); err != nil {
return err
}
_, _ = s.store.SaveLegacyRevision("update-info", string(append(data, '\n')), "generated from release database", firstNonEmpty(actor, "system"))
return nil
}
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 {
base := firstNonEmpty(strings.TrimRight(s.cfg.CDNBaseURL, "/"), requestBaseURL(r, s.cfg.BaseURL))
s.packageMu.RLock()
items := append([]Package(nil), s.packages...)
s.packageMu.RUnlock()
for index := range items {
items[index].URL = base + "/downloads/" + items[index].FileName
}
return items
}
// Reconcile keeps request-time manifest generation independent from package I/O.
// Hashes are reused from the persistent index whenever size and mtime still match.
func (s *Service) Reconcile(ctx context.Context) error {
s.reconcileMu.Lock()
defer s.reconcileMu.Unlock()
if err := os.MkdirAll(s.cfg.DownloadsDir, 0o750); err != nil {
return err
}
indexed := map[string]db.ReleasePackage{}
var firstErr error
if s.store != nil {
rows, err := s.store.ListReleasePackages()
if err != nil {
firstErr = err
} else {
for _, item := range rows {
indexed[item.FileName] = item
}
}
}
entries, err := os.ReadDir(s.cfg.DownloadsDir)
if err != nil {
return err
}
items := make([]Package, 0, len(entries))
seen := map[string]bool{}
for _, entry := range entries {
if err := ctx.Err(); err != nil {
return err
}
if entry.IsDir() || !isSupportedPackageName(entry.Name()) {
continue
}
info, err := entry.Info()
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
name := entry.Name()
seen[name] = true
modifiedAt := info.ModTime().UTC().Format(time.RFC3339Nano)
record, cached := indexed[name]
var item Package
if cached && record.SizeBytes == info.Size() && record.UpdatedAt == modifiedAt && strings.TrimSpace(record.SHA256) != "" {
item = packageFromRecord(record)
s.hashMu.Lock()
s.hashes[filepath.Join(s.cfg.DownloadsDir, name)] = cachedFileHash{size: info.Size(), modifiedAt: info.ModTime().UnixNano(), sha256: record.SHA256}
s.hashMu.Unlock()
} else {
version := detectVersion(name)
platform, arch := detectPlatform(name)
product := detectProduct(name)
hashValue := s.cachedSHA256(filepath.Join(s.cfg.DownloadsDir, name), info)
if strings.TrimSpace(hashValue) == "" {
if firstErr == nil {
firstErr = fmt.Errorf("hash package %q: empty SHA-256", name)
}
continue
}
item = Package{
ID: packageID(product, platform, arch, version),
Name: product,
Version: version,
Platform: platform,
Arch: arch,
SHA256: hashValue,
Size: info.Size(),
Required: strings.Contains(strings.ToLower(product), "ymhut"),
Enabled: true,
FileName: name,
UpdatedAt: modifiedAt,
}
if s.store != nil {
if _, err := s.store.UpsertReleasePackage(releasePackageRecord(item)); err != nil && firstErr == nil {
firstErr = err
}
}
}
items = append(items, item)
}
if s.store != nil {
for name := range indexed {
if !seen[name] {
if err := s.store.DeleteReleasePackage(name); err != nil && firstErr == nil {
firstErr = err
}
}
}
}
if firstErr != nil {
return firstErr
}
sort.Slice(items, func(i, j int) bool { return compareVersion(items[i].Version, items[j].Version) > 0 })
s.packageMu.Lock()
changed := !reflect.DeepEqual(s.packages, items)
s.packages = items
s.packageMu.Unlock()
if changed {
s.notifyChanged()
}
return firstErr
}
func packageFromRecord(item db.ReleasePackage) Package {
return Package{
ID: packageID(item.Product, item.Platform, item.Arch, item.Version), Name: item.Product, Version: item.Version,
Platform: item.Platform, Arch: item.Arch, URL: item.URL, SHA256: item.SHA256, Size: item.SizeBytes,
Required: strings.Contains(strings.ToLower(item.Product), "ymhut"), Enabled: item.Enabled,
FileName: item.FileName, UpdatedAt: item.UpdatedAt,
}
}
func releasePackageRecord(item Package) db.ReleasePackage {
return db.ReleasePackage{
Product: item.Name, Version: item.Version, Platform: item.Platform, Arch: item.Arch, FileName: item.FileName,
URL: item.URL, SHA256: item.SHA256, SizeBytes: item.Size, Enabled: item.Enabled, UpdatedAt: item.UpdatedAt,
}
}
func packageID(product, platform, arch, version string) string {
return strings.ToLower(strings.ReplaceAll(product+"-"+platform+"-"+arch+"-"+version, " ", "-"))
}
func isSupportedPackageName(name string) bool {
lower := strings.ToLower(name)
for _, suffix := range []string{".exe", ".msix", ".appinstaller", ".msi", ".zip", ".7z"} {
if strings.HasSuffix(lower, suffix) {
return true
}
}
return false
}
func (s *Service) cachedSHA256(path string, info os.FileInfo) string {
modifiedAt := info.ModTime().UnixNano()
s.hashMu.Lock()
if cached, ok := s.hashes[path]; ok && cached.size == info.Size() && cached.modifiedAt == modifiedAt {
s.hashMu.Unlock()
return cached.sha256
}
s.hashMu.Unlock()
value := s.hashFile(path)
s.hashMu.Lock()
s.hashes[path] = cachedFileHash{size: info.Size(), modifiedAt: modifiedAt, sha256: value}
for cachedPath := range s.hashes {
if cachedPath != path {
if _, err := os.Stat(cachedPath); errors.Is(err, os.ErrNotExist) {
delete(s.hashes, cachedPath)
}
}
}
s.hashMu.Unlock()
return value
}
func (s *Service) StaticJSON(name string) map[string]any {
return readJSON(filepath.Join(s.cfg.UpdatePublicDir, name))
}
func (s *Service) SaveUploadedPackage(r *http.Request, reader io.Reader, opts UploadOptions, actor string) (Package, error) {
name, err := safePackageName(opts.FileName)
if err != nil {
return Package{}, err
}
if err := os.MkdirAll(s.cfg.DownloadsDir, 0o750); err != nil {
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
}
tmp, err := os.CreateTemp(s.cfg.DownloadsDir, "."+name+".*.upload")
if err != nil {
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
hash := sha256.New()
written, err := io.Copy(io.MultiWriter(tmp, hash), reader)
if err == nil {
err = tmp.Sync()
}
if closeErr := tmp.Close(); err == nil {
err = closeErr
}
if err != nil {
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
}
if written <= 0 {
return Package{}, ErrUploadedPackageEmpty
}
opts.FileName = name
return s.SavePreparedPackage(r, UploadedPackageFile{TempPath: tmpName, Size: written, SHA256: hex.EncodeToString(hash.Sum(nil))}, opts, actor)
}
func (s *Service) SavePreparedPackage(r *http.Request, uploaded UploadedPackageFile, opts UploadOptions, actor string) (Package, error) {
name, err := safePackageName(opts.FileName)
if err != nil {
return Package{}, err
}
if uploaded.TempPath == "" {
return Package{}, ErrUploadedPackageMissing
}
if uploaded.Size <= 0 {
return Package{}, ErrUploadedPackageEmpty
}
if err := os.MkdirAll(s.cfg.DownloadsDir, 0o750); err != nil {
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
}
target := filepath.Join(s.cfg.DownloadsDir, name)
resolved, err := filepath.Abs(target)
if err != nil {
return Package{}, err
}
base, _ := filepath.Abs(s.cfg.DownloadsDir)
if resolved != base && !strings.HasPrefix(resolved, base+string(os.PathSeparator)) {
return Package{}, errors.New("path escape rejected")
}
if err := os.Chmod(uploaded.TempPath, 0o640); err != nil {
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
}
backup, err := backupExistingPackage(target)
if err != nil {
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
}
if err := os.Rename(uploaded.TempPath, target); err != nil {
_ = restorePackageBackup(target, backup)
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
}
rollbackFile := func(cause error) error {
if rollbackErr := restorePackageBackup(target, backup); rollbackErr != nil {
return fmt.Errorf("%w; rollback failed: %v", cause, rollbackErr)
}
return cause
}
info, err := os.Stat(target)
if err != nil {
return Package{}, rollbackFile(fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err))
}
if info.Size() != uploaded.Size {
return Package{}, rollbackFile(fmt.Errorf("%w: uploaded size changed during commit", ErrUploadedPackageStorageFailed))
}
version := firstNonEmpty(opts.Version, detectVersion(name))
platform, arch := detectPlatform(name)
platform = firstNonEmpty(opts.Platform, platform)
arch = firstNonEmpty(opts.Arch, arch)
product := detectProduct(name)
pkg := Package{
ID: packageID(product, platform, arch, version),
Name: product,
Version: version,
Platform: platform,
Arch: arch,
URL: firstNonEmpty(strings.TrimRight(s.cfg.CDNBaseURL, "/"), requestBaseURL(r, s.cfg.BaseURL)) + "/downloads/" + name,
SHA256: strings.ToLower(uploaded.SHA256),
Size: uploaded.Size,
Required: strings.Contains(strings.ToLower(product), "ymhut"),
Enabled: true,
FileName: name,
UpdatedAt: info.ModTime().UTC().Format(time.RFC3339Nano),
}
var previousManifest fileSnapshot
if opts.UpdateManifest {
previousManifest, err = captureFile(filepath.Join(s.cfg.UpdatePublicDir, "update-info.json"))
if err != nil {
return Package{}, rollbackFile(fmt.Errorf("%w: %v", ErrUploadedPackageManifestFailed, err))
}
if err := s.updateLegacyManifest(pkg, opts); err != nil {
return Package{}, rollbackFile(fmt.Errorf("%w: %v", ErrUploadedPackageManifestFailed, err))
}
}
if s.store != nil {
if _, err := s.store.UpsertReleasePackage(releasePackageRecord(pkg)); err != nil {
cause := fmt.Errorf("%w: %v", ErrUploadedPackageIndexFailed, err)
if opts.UpdateManifest {
if restoreErr := previousManifest.restore(filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")); restoreErr != nil {
cause = fmt.Errorf("%w; manifest rollback failed: %v", cause, restoreErr)
}
}
return Package{}, rollbackFile(cause)
}
}
if backup != "" {
_ = os.Remove(backup)
}
s.upsertPackageSnapshot(pkg)
if s.store != nil {
_ = s.store.InsertAudit(db.AuditLog{Actor: firstNonEmpty(actor, "admin"), Type: "release.package_uploaded", Target: name, Message: fmt.Sprintf("已上传发布包 %s%s%s", name, version, formatBytes(uploaded.Size))})
}
return pkg, nil
}
type fileSnapshot struct {
data []byte
mode os.FileMode
existed bool
}
func captureFile(path string) (fileSnapshot, error) {
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
return fileSnapshot{}, nil
}
if err != nil {
return fileSnapshot{}, err
}
data, err := os.ReadFile(path)
if err != nil {
return fileSnapshot{}, err
}
return fileSnapshot{data: data, mode: info.Mode(), existed: true}, nil
}
func (s fileSnapshot) restore(path string) error {
if !s.existed {
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return err
}
mode := s.mode
if mode == 0 {
mode = 0o640
}
return os.WriteFile(path, s.data, mode)
}
func (s *Service) upsertPackageSnapshot(item Package) {
s.packageMu.Lock()
replaced := false
for index := range s.packages {
if s.packages[index].FileName == item.FileName {
s.packages[index] = item
replaced = true
break
}
}
if !replaced {
s.packages = append(s.packages, item)
}
sort.Slice(s.packages, func(i, j int) bool { return compareVersion(s.packages[i].Version, s.packages[j].Version) > 0 })
s.packageMu.Unlock()
s.notifyChanged()
}
func backupExistingPackage(target string) (string, error) {
if _, err := os.Stat(target); errors.Is(err, os.ErrNotExist) {
return "", nil
} else if err != nil {
return "", err
}
file, err := os.CreateTemp(filepath.Dir(target), "."+filepath.Base(target)+".backup-*")
if err != nil {
return "", err
}
backup := file.Name()
if err := file.Close(); err != nil {
_ = os.Remove(backup)
return "", err
}
if err := os.Remove(backup); err != nil {
return "", err
}
if err := os.Rename(target, backup); err != nil {
return "", err
}
return backup, nil
}
func restorePackageBackup(target, backup string) error {
if err := os.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if backup == "" {
return nil
}
return os.Rename(backup, target)
}
func (s *Service) updateLegacyManifest(pkg Package, opts UploadOptions) error {
path := filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")
payload := s.legacyUpdateBase()
payload["app_version"] = pkg.Version
payload["download_url"] = pkg.URL
payload["package_sha256"] = pkg.SHA256
payload["package_size"] = pkg.Size
payload["updated_at"] = time.Now().UTC().Format(time.RFC3339)
payload["download_mirrors"] = []map[string]any{{
"id": "primary",
"name": "官方直连",
"url": pkg.URL,
"type": "direct",
"sha256": pkg.SHA256,
"enabled": true,
}}
if strings.TrimSpace(opts.Notes) != "" {
payload["release_notes"] = opts.Notes
}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return err
}
return atomicWrite(path, append(data, '\n'))
}
func (s *Service) legacyUpdateBase() map[string]any {
payload := map[string]any{}
for _, path := range []string{
filepath.Join(s.cfg.LegacyUpdateDir, "public", "update-info.json"),
filepath.Join(s.cfg.UpdatePublicDir, "update-info.json"),
} {
for key, value := range readJSON(path) {
payload[key] = value
}
}
if payload["app_version"] == nil {
if value, ok := payload["appVersion"]; ok {
payload["app_version"] = value
} else if value, ok := payload["latestVersion"]; ok {
payload["app_version"] = value
}
}
if payload["manifest_version"] == nil {
if value, ok := payload["manifestVersion"]; ok {
payload["manifest_version"] = value
}
}
return payload
}
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 atomicWrite(path string, data []byte) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o750); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp")
if err != nil {
return err
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Chmod(tmpName, 0o640); err != nil {
return err
}
return os.Rename(tmpName, path)
}
func requestBaseURL(r *http.Request, fallback string) string {
if r != nil {
scheme := firstForwardedHeader(r.Header.Get("X-Forwarded-Proto"))
if scheme != "http" && scheme != "https" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
host := firstForwardedHeader(r.Header.Get("X-Forwarded-Host"))
if !validForwardedHost(host) {
host = strings.TrimSpace(r.Host)
}
if validForwardedHost(host) {
return scheme + "://" + host
}
}
return strings.TrimRight(fallback, "/")
}
func firstForwardedHeader(value string) string {
return strings.ToLower(strings.TrimSpace(strings.Split(value, ",")[0]))
}
func validForwardedHost(value string) bool {
if value == "" || strings.TrimSpace(value) != value || strings.ContainsAny(value, "\\\\\r\n\t ") {
return false
}
parsed, err := url.Parse("http://" + value)
if err != nil || parsed.Host != value || parsed.User != nil || parsed.Hostname() == "" || parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" {
return false
}
if port := parsed.Port(); port != "" {
value, err := strconv.Atoi(port)
if err != nil || value < 1 || value > 65535 {
return false
}
}
return true
}
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))
}
func safePackageName(name string) (string, error) {
original := strings.TrimSpace(name)
if original == "" || original == "." || original == ".." || strings.ContainsAny(original, `/\`) {
return "", ErrUnsafePackageName
}
name = filepath.Base(original)
if isSupportedPackageName(name) {
return name, nil
}
return "", ErrUnsupportedPackage
}
func formatBytes(value int64) string {
if value < 1024 {
return fmt.Sprintf("%d B", value)
}
next := float64(value) / 1024
for _, unit := range []string{"KB", "MB", "GB"} {
if next < 1024 || unit == "GB" {
return fmt.Sprintf("%.1f %s", next, unit)
}
next /= 1024
}
return fmt.Sprintf("%d B", value)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}