Files
QWQLwToo 079ee4eaeb
build-winui / winui (push) Has been cancelled
Add server components
2026-06-26 13:28:09 +08:00

352 lines
8.0 KiB
Go

package utils
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
type FileInfo struct {
Version string `json:"version"`
Extension string `json:"extension"`
FileName string `json:"fileName"`
DownloadPath string `json:"downloadPath"`
Size string `json:"size"`
SizeBytes int64 `json:"sizeBytes"`
UpdateDate string `json:"updateDate"`
UpdateTime string `json:"updateTime"`
}
type ProductsInfo map[string][]FileInfo
var (
supportedPackageExtOrder = []string{
".tar.gz",
".appimage",
".msi",
".exe",
".zip",
".pkg",
".dmg",
".apk",
".deb",
".rpm",
}
versionPattern = regexp.MustCompile(`(?i)(?:^|[ _-])v?(\d+(?:\.\d+){1,4})(?:$|[ _-])`)
)
func FormatBytes(bytes int64, precision int) string {
if bytes == 0 {
return "0 B"
}
units := []string{"B", "KB", "MB", "GB", "TB"}
if precision == 0 {
precision = 2
}
bytesFloat := float64(bytes)
if bytesFloat < 0 {
bytesFloat = 0
}
pow := 0
if bytesFloat > 0 {
pow = int(float64(len(fmt.Sprintf("%.0f", bytesFloat))-1) / 3.321928)
}
if pow >= len(units) {
pow = len(units) - 1
}
divisor := int64(1)
for i := 0; i < pow; i++ {
divisor *= 1024
}
converted := bytesFloat / float64(divisor)
return fmt.Sprintf("%.*f %s", precision, converted, units[pow])
}
func GetProductsInfo(downloadDir string, logger *Logger) ProductsInfo {
products := make(ProductsInfo)
if _, err := os.Stat(downloadDir); os.IsNotExist(err) {
logger.Error(fmt.Sprintf("读取下载目录失败: %s - 错误: %s", downloadDir, err.Error()))
return nil
}
entries, err := os.ReadDir(downloadDir)
if err != nil {
logger.Error(fmt.Sprintf("读取下载目录失败: %s - 错误: %s", downloadDir, err.Error()))
return nil
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
fileName := entry.Name()
productName, version, ext, ok := parsePackageFileName(fileName)
if !ok {
continue
}
filePath := filepath.Join(downloadDir, fileName)
stat, err := os.Stat(filePath)
if err != nil {
logger.Warn(fmt.Sprintf("获取文件统计信息失败: %s - 错误: %s", fileName, err.Error()))
continue
}
fileInfoData := FileInfo{
Version: version,
Extension: strings.TrimPrefix(ext, "."),
FileName: fileName,
DownloadPath: "/downloads/" + fileName,
Size: FormatBytes(stat.Size(), 2),
SizeBytes: stat.Size(),
UpdateDate: stat.ModTime().Format("2006-01-02"),
UpdateTime: stat.ModTime().Format("2006-01-02 15:04:05"),
}
products[productName] = append(products[productName], fileInfoData)
}
for productName := range products {
sort.Slice(products[productName], func(i, j int) bool {
v1 := products[productName][i].Version
v2 := products[productName][j].Version
if compareVersions(v1, v2) == 0 {
return products[productName][i].UpdateTime > products[productName][j].UpdateTime
}
return compareVersions(v1, v2) > 0
})
}
return products
}
func GetLatestProductRelease(products ProductsInfo, preferredProduct string) (string, *FileInfo) {
if len(products) == 0 {
return "", nil
}
var selectedProduct string
var selectedRelease *FileInfo
if preferredProduct != "" {
for productName, releases := range products {
if !IsSameProduct(productName, preferredProduct) || len(releases) == 0 {
continue
}
candidate := releases[0]
if selectedRelease == nil || isNewerRelease(candidate, *selectedRelease) {
selectedProduct = productName
selectedRelease = &candidate
}
}
if selectedRelease != nil {
return selectedProduct, selectedRelease
}
}
for productName, releases := range products {
if len(releases) == 0 {
continue
}
candidate := releases[0]
if selectedRelease == nil || isNewerRelease(candidate, *selectedRelease) {
selectedProduct = productName
selectedRelease = &candidate
}
}
return selectedProduct, selectedRelease
}
func IsSameProduct(productName string, preferredProduct string) bool {
return productAliasKey(productName) == productAliasKey(preferredProduct)
}
func productAliasKey(name string) string {
key := strings.ToLower(strings.TrimSpace(name))
key = strings.NewReplacer(" ", "", "_", "", "-", "", ".", "").Replace(key)
switch key {
case "ymhutbox", "ymhut":
return "ymhutbox"
default:
return key
}
}
func isNewerRelease(candidate FileInfo, current FileInfo) bool {
versionCompare := compareVersions(candidate.Version, current.Version)
if versionCompare != 0 {
return versionCompare > 0
}
return candidate.UpdateTime > current.UpdateTime
}
func parsePackageFileName(fileName string) (string, string, string, bool) {
ext, stem, ok := splitPackageExtension(fileName)
if !ok {
return "", "", "", false
}
version := extractVersion(stem)
productName := normalizeProductName(stem, version)
if productName == "" {
productName = stem
}
if version == "" {
version = "未标注"
}
return productName, version, ext, true
}
func splitPackageExtension(fileName string) (string, string, bool) {
lower := strings.ToLower(fileName)
for _, ext := range supportedPackageExtOrder {
if strings.HasSuffix(lower, ext) {
return ext, fileName[:len(fileName)-len(ext)], true
}
}
return "", "", false
}
func extractVersion(stem string) string {
matches := versionPattern.FindStringSubmatch(stem)
if len(matches) > 1 {
return matches[1]
}
return ""
}
func normalizeProductName(stem string, version string) string {
name := stem
if version != "" {
name = versionPattern.ReplaceAllString(name, " ")
}
replacements := []string{
"setup", "installer", "install", "release", "portable",
"windows", "window", "win", "linux", "android", "macos", "darwin",
"x64", "x86", "arm64", "amd64", "universal", "desktop",
}
for _, token := range replacements {
re := regexp.MustCompile(`(?i)(^|[ _-])` + regexp.QuoteMeta(token) + `($|[ _-])`)
name = re.ReplaceAllString(name, " ")
}
name = strings.NewReplacer("_", " ", "-", " ").Replace(name)
name = strings.Join(strings.Fields(name), " ")
return strings.TrimSpace(name)
}
func compareVersions(v1, v2 string) int {
if v1 == "未标注" && v2 == "未标注" {
return 0
}
if v1 == "未标注" {
return -1
}
if v2 == "未标注" {
return 1
}
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
maxLen := len(parts1)
if len(parts2) > maxLen {
maxLen = len(parts2)
}
for i := 0; i < maxLen; i++ {
num1 := 0
num2 := 0
if i < len(parts1) {
fmt.Sscanf(parts1[i], "%d", &num1)
}
if i < len(parts2) {
fmt.Sscanf(parts2[i], "%d", &num2)
}
if num1 > num2 {
return 1
}
if num1 < num2 {
return -1
}
}
return 0
}
func FileExists(filePath string) bool {
_, err := os.Stat(filePath)
return !os.IsNotExist(err)
}
func ReadJSONFile(filePath string) (map[string]interface{}, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return result, nil
}
func GetMimeType(filePath string) string {
ext := strings.ToLower(filepath.Ext(filePath))
mimeTypes := map[string]string{
".json": "application/json; charset=utf-8",
".ttf": "font/ttf",
".otf": "font/otf",
".woff": "font/woff",
".woff2": "font/woff2",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".html": "text/html; charset=utf-8",
".txt": "text/plain; charset=utf-8",
".exe": "application/octet-stream",
".zip": "application/zip",
".pkg": "application/octet-stream",
".dmg": "application/x-apple-diskimage",
".msi": "application/x-msi",
".apk": "application/vnd.android.package-archive",
".deb": "application/vnd.debian.binary-package",
".rpm": "application/x-rpm",
}
if mime, ok := mimeTypes[ext]; ok {
return mime
}
if strings.HasSuffix(strings.ToLower(filePath), ".tar.gz") {
return "application/gzip"
}
return "application/octet-stream"
}