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" }