352 lines
8.0 KiB
Go
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"
|
|
}
|