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