diff --git a/data/data.go b/data/data.go new file mode 100644 index 0000000..88b749a --- /dev/null +++ b/data/data.go @@ -0,0 +1,8 @@ +package bin + +import ( + _ "embed" +) + +//go:embed records.json +var RecordsJson []byte diff --git a/internal/config/config.go b/internal/config/config.go index f588f6e..25b3d89 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,8 +23,15 @@ type Config struct { EnableOfflineMode bool } +var ( + GlobalConfig *Config = nil +) + // LoadConfig 加载配置,优先级:命令行参数 > 环境变量 > 默认值 func LoadConfig() *Config { + if GlobalConfig != nil { + return GlobalConfig + } cfg := &Config{} homeDir, err := os.UserHomeDir() if err != nil { @@ -43,5 +50,6 @@ func LoadConfig() *Config { flag.BoolVar(&cfg.EnableOfflineMode, "offline", utils.GetBoolEnv("MT_OFFLINE", false), "Enable offline mode") flag.Parse() + GlobalConfig = cfg return cfg } diff --git a/internal/models/const.go b/internal/models/const.go deleted file mode 100644 index fa05e00..0000000 --- a/internal/models/const.go +++ /dev/null @@ -1,5 +0,0 @@ -package models - -const ( - recordsUrl = "https://remote-settings.mozilla.org/v1/buckets/main/collections/translations-models/records" -) diff --git a/internal/models/records.go b/internal/models/records.go new file mode 100644 index 0000000..b7615c2 --- /dev/null +++ b/internal/models/records.go @@ -0,0 +1,29 @@ +package models + +const ( + recordsUrl = "https://remote-settings.mozilla.org/v1/buckets/main/collections/translations-models/records" + attachmentsBaseUrl = "https://firefox-settings-attachments.cdn.mozilla.net" +) + +// 检测默认配置目录下是否存在 records.json +// 存在则解析本地 records.json +// 不存在则写出默认内嵌的 records.json 到配置目录然后解析 +func initRecords() error { + return nil +} + +// 更新 records.json,从远程下载 records.json 到配置目录 +// 然后解析本地 records.json +func downloadRecords() error { + return nil +} + +// 解析 records.json,找到对应的模型属性 +// 检查配置目录下是否存在对应的模型文件,可通过 sha256 校验 +// 不存在则下载到配置目录 +// 参数:toLang 目标语言,fromLang 源语言,version 模型版本 +// records 里同一个 fromLang 和 toLang 会存在多个 version 的模型 +// 需要根据 version 下载对应的模型,未指定 version 则下载最新版本 +func downloadModel(toLang string, fromLang string, version string) error { + return nil +} diff --git a/internal/models/records_test.go b/internal/models/records_test.go new file mode 100644 index 0000000..2640e7f --- /dev/null +++ b/internal/models/records_test.go @@ -0,0 +1 @@ +package models diff --git a/internal/utils/downloader/downloader.go b/internal/utils/downloader/downloader.go index 833cb2a..bdbe1e8 100644 --- a/internal/utils/downloader/downloader.go +++ b/internal/utils/downloader/downloader.go @@ -2,14 +2,16 @@ package downloader import ( "context" - "crypto/sha256" - "encoding/hex" + "crypto/tls" "fmt" - "io" + "net/http" + "net/url" "os" "path/filepath" + "time" "github.com/hashicorp/go-getter" + "github.com/xxnuo/MTranServer/internal/utils" ) // Downloader 下载器结构 @@ -43,7 +45,7 @@ func (d *Downloader) SetProgressFunc(fn getter.ProgressTracker) { } // Download 下载文件到指定目录 -func (d *Downloader) Download(url, filename string, opts *DownloadOptions) error { +func (d *Downloader) Download(urlStr, filename string, opts *DownloadOptions) error { if opts == nil { opts = &DownloadOptions{ Context: context.Background(), @@ -66,7 +68,7 @@ func (d *Downloader) Download(url, filename string, opts *DownloadOptions) error if _, err := os.Stat(dst); err == nil { // 文件存在,检查 SHA256 if opts.SHA256 != "" { - if err := verifySHA256(dst, opts.SHA256); err == nil { + if err := utils.VerifySHA256(dst, opts.SHA256); err == nil { // 文件已存在且校验通过,跳过下载 return nil } @@ -78,9 +80,59 @@ func (d *Downloader) Download(url, filename string, opts *DownloadOptions) error tmpFile := dst + ".tmp" defer os.Remove(tmpFile) + // 创建 HTTP 客户端,支持代理和重定向 + httpClient := &http.Client{ + Timeout: 30 * time.Minute, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + // 允许最多 10 次重定向 + if len(via) >= 10 { + return fmt.Errorf("stopped after 10 redirects") + } + return nil + }, + } + + // 配置代理 + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, + } + + // 从环境变量读取代理设置 + if proxyURL := os.Getenv("HTTP_PROXY"); proxyURL != "" { + if parsedURL, err := url.Parse(proxyURL); err == nil { + transport.Proxy = http.ProxyURL(parsedURL) + } + } else if proxyURL := os.Getenv("http_proxy"); proxyURL != "" { + if parsedURL, err := url.Parse(proxyURL); err == nil { + transport.Proxy = http.ProxyURL(parsedURL) + } + } + + // 支持 HTTPS_PROXY + if proxyURL := os.Getenv("HTTPS_PROXY"); proxyURL != "" { + if parsedURL, err := url.Parse(proxyURL); err == nil { + transport.Proxy = http.ProxyURL(parsedURL) + } + } else if proxyURL := os.Getenv("https_proxy"); proxyURL != "" { + if parsedURL, err := url.Parse(proxyURL); err == nil { + transport.Proxy = http.ProxyURL(parsedURL) + } + } + + httpClient.Transport = transport + + // 配置 HttpGetter + httpGetter := &getter.HttpGetter{ + Client: httpClient, + } + // 配置 getter 客户端选项 clientOpts := []getter.ClientOption{ getter.WithContext(opts.Context), + getter.WithGetters(map[string]getter.Getter{ + "http": httpGetter, + "https": httpGetter, + }), } if d.ProgressFunc != nil { @@ -89,7 +141,7 @@ func (d *Downloader) Download(url, filename string, opts *DownloadOptions) error // 创建 getter 客户端 client := &getter.Client{ - Src: url, + Src: urlStr, Dst: tmpFile, Mode: getter.ClientModeFile, } @@ -106,7 +158,7 @@ func (d *Downloader) Download(url, filename string, opts *DownloadOptions) error // 校验 SHA256 if opts.SHA256 != "" { - if err := verifySHA256(tmpFile, opts.SHA256); err != nil { + if err := utils.VerifySHA256(tmpFile, opts.SHA256); err != nil { return fmt.Errorf("Failed to verify SHA256: %w", err) } } @@ -130,40 +182,3 @@ func DownloadFile(url, destPath, sha256sum string) error { Context: context.Background(), }) } - -// verifySHA256 校验文件的 SHA256 -func verifySHA256(filepath, expectedHash string) error { - file, err := os.Open(filepath) - if err != nil { - return fmt.Errorf("Failed to open file: %w", err) - } - defer file.Close() - - hash := sha256.New() - if _, err := io.Copy(hash, file); err != nil { - return fmt.Errorf("Failed to calculate SHA256: %w", err) - } - - actualHash := hex.EncodeToString(hash.Sum(nil)) - if actualHash != expectedHash { - return fmt.Errorf("SHA256 mismatch: expected %s, actual %s", expectedHash, actualHash) - } - - return nil -} - -// CalculateSHA256 计算文件的 SHA256 -func CalculateSHA256(filepath string) (string, error) { - file, err := os.Open(filepath) - if err != nil { - return "", fmt.Errorf("Failed to open file: %w", err) - } - defer file.Close() - - hash := sha256.New() - if _, err := io.Copy(hash, file); err != nil { - return "", fmt.Errorf("Failed to calculate SHA256: %w", err) - } - - return hex.EncodeToString(hash.Sum(nil)), nil -} diff --git a/internal/utils/downloader/downloader_test.go b/internal/utils/downloader/downloader_test.go index 9051193..d2089ae 100644 --- a/internal/utils/downloader/downloader_test.go +++ b/internal/utils/downloader/downloader_test.go @@ -208,33 +208,6 @@ func TestDownloadWithContext(t *testing.T) { } } -func TestCalculateSHA256(t *testing.T) { - // 创建临时文件 - tempDir, err := os.MkdirTemp("", "downloader-test-*") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - testContent := []byte("Hello, World!") - expectedSHA256 := "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f" - - filePath := filepath.Join(tempDir, "test.txt") - if err := os.WriteFile(filePath, testContent, 0644); err != nil { - t.Fatal(err) - } - - // 计算 SHA256 - hash, err := CalculateSHA256(filePath) - if err != nil { - t.Fatalf("计算 SHA256 失败: %v", err) - } - - if hash != expectedSHA256 { - t.Fatalf("SHA256 不匹配: 期望 %s, 实际 %s", expectedSHA256, hash) - } -} - func TestDownloadFile(t *testing.T) { // 创建测试 HTTP 服务器 testContent := []byte("Hello, World!") diff --git a/internal/utils/file.go b/internal/utils/file.go new file mode 100644 index 0000000..8e93251 --- /dev/null +++ b/internal/utils/file.go @@ -0,0 +1,46 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" +) + +// VerifySHA256 校验文件的 SHA256 +func VerifySHA256(filepath, expectedHash string) error { + file, err := os.Open(filepath) + if err != nil { + return fmt.Errorf("Failed to open file: %w", err) + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return fmt.Errorf("Failed to calculate SHA256: %w", err) + } + + actualHash := hex.EncodeToString(hash.Sum(nil)) + if actualHash != expectedHash { + return fmt.Errorf("SHA256 mismatch: expected %s, actual %s", expectedHash, actualHash) + } + + return nil +} + +// ComputeSHA256 计算文件的 SHA256 +func ComputeSHA256(filepath string) (string, error) { + file, err := os.Open(filepath) + if err != nil { + return "", fmt.Errorf("Failed to open file: %w", err) + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "", fmt.Errorf("Failed to calculate SHA256: %w", err) + } + + return hex.EncodeToString(hash.Sum(nil)), nil +} diff --git a/internal/utils/file_test.go b/internal/utils/file_test.go new file mode 100644 index 0000000..5e192ec --- /dev/null +++ b/internal/utils/file_test.go @@ -0,0 +1,34 @@ +package utils + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCalculateSHA256(t *testing.T) { + // 创建临时文件 + tempDir, err := os.MkdirTemp("", "file-test-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + testContent := []byte("Hello, World!") + expectedSHA256 := "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f" + + filePath := filepath.Join(tempDir, "test.txt") + if err := os.WriteFile(filePath, testContent, 0644); err != nil { + t.Fatal(err) + } + + // 计算 SHA256 + hash, err := ComputeSHA256(filePath) + if err != nil { + t.Fatalf("计算 SHA256 失败: %v", err) + } + + if hash != expectedSHA256 { + t.Fatalf("SHA256 不匹配: 期望 %s, 实际 %s", expectedSHA256, hash) + } +} diff --git a/internal/utils/version.go b/internal/utils/version.go new file mode 100644 index 0000000..2fdd1a6 --- /dev/null +++ b/internal/utils/version.go @@ -0,0 +1,135 @@ +package utils + +import ( + "strconv" + "strings" +) + +// 比较版本号,返回最大的版本号 +// 支持 semver 格式,但不完全符合 semver 规范 +// 样例: +// - 1.0.0, 1.0.1, 1.1.0 返回 1.1.0 +// - 1.0.0-alpha.1, 1.0.0-alpha.2, 1.0.0-alpha.3 返回 1.0.0-alpha.3 +func GetLargestVersion(versions []string) string { + if len(versions) == 0 { + return "" + } + + largest := versions[0] + for _, v := range versions[1:] { + if compareVersions(v, largest) > 0 { + largest = v + } + } + return largest +} + +// compareVersions 比较两个版本号 +// 返回值: v1 > v2 返回 1, v1 < v2 返回 -1, v1 == v2 返回 0 +func compareVersions(v1, v2 string) int { + // 分割版本号和预发布标识 + parts1 := strings.Split(v1, "-") + parts2 := strings.Split(v2, "-") + + version1 := parts1[0] + version2 := parts2[0] + + // 比较主版本号 + cmp := compareNumericVersions(version1, version2) + if cmp != 0 { + return cmp + } + + // 版本号相同,比较预发布标识 + // 无预发布标识 > 有预发布标识 + if len(parts1) == 1 && len(parts2) > 1 { + return 1 + } + if len(parts1) > 1 && len(parts2) == 1 { + return -1 + } + if len(parts1) == 1 && len(parts2) == 1 { + return 0 + } + + // 比较预发布标识 + return comparePrerelease(parts1[1], parts2[1]) +} + +// compareNumericVersions 比较数字版本号 (如 1.0.0) +func compareNumericVersions(v1, v2 string) int { + segments1 := strings.Split(v1, ".") + segments2 := strings.Split(v2, ".") + + maxLen := len(segments1) + if len(segments2) > maxLen { + maxLen = len(segments2) + } + + for i := 0; i < maxLen; i++ { + var num1, num2 int + + if i < len(segments1) { + num1, _ = strconv.Atoi(segments1[i]) + } + if i < len(segments2) { + num2, _ = strconv.Atoi(segments2[i]) + } + + if num1 > num2 { + return 1 + } + if num1 < num2 { + return -1 + } + } + + return 0 +} + +// comparePrerelease 比较预发布标识 +func comparePrerelease(p1, p2 string) int { + // 分割预发布标识的各个部分 + parts1 := strings.Split(p1, ".") + parts2 := strings.Split(p2, ".") + + maxLen := len(parts1) + if len(parts2) > maxLen { + maxLen = len(parts2) + } + + for i := 0; i < maxLen; i++ { + if i >= len(parts1) { + return -1 + } + if i >= len(parts2) { + return 1 + } + + part1 := parts1[i] + part2 := parts2[i] + + // 尝试作为数字比较 + num1, err1 := strconv.Atoi(part1) + num2, err2 := strconv.Atoi(part2) + + if err1 == nil && err2 == nil { + if num1 > num2 { + return 1 + } + if num1 < num2 { + return -1 + } + } else { + // 字符串比较 + if part1 > part2 { + return 1 + } + if part1 < part2 { + return -1 + } + } + } + + return 0 +} diff --git a/internal/utils/version_test.go b/internal/utils/version_test.go new file mode 100644 index 0000000..c6c2151 --- /dev/null +++ b/internal/utils/version_test.go @@ -0,0 +1,80 @@ +package utils + +import "testing" + +func TestGetLargestVersion(t *testing.T) { + tests := []struct { + name string + versions []string + want string + }{ + { + name: "empty versions", + versions: []string{}, + want: "", + }, + { + name: "single version", + versions: []string{"1.0.0"}, + want: "1.0.0", + }, + { + name: "short versions", + versions: []string{"1.0", "1.1", "1.2"}, + want: "1.2", + }, + { + name: "mixed short versions", + versions: []string{"1", "1.1.0", "1.2", "2"}, + want: "2", + }, + { + name: "basic versions", + versions: []string{"1.0.0", "1.0.1", "1.1.0"}, + want: "1.1.0", + }, + { + name: "prerelease versions", + versions: []string{"1.0.0-alpha.1", "1.0.0-alpha.2", "1.0.0-alpha.3"}, + want: "1.0.0-alpha.3", + }, + { + name: "mixed versions", + versions: []string{"1.0.0-alpha.1", "1.0.0", "1.0.1"}, + want: "1.0.1", + }, + { + name: "release vs prerelease", + versions: []string{"1.0.0-beta.1", "1.0.0"}, + want: "1.0.0", + }, + { + name: "complex versions", + versions: []string{"2.0.0", "1.9.9", "1.10.0", "1.9.10"}, + want: "2.0.0", + }, + { + name: "single version", + versions: []string{"1.0.0"}, + want: "1.0.0", + }, + { + name: "empty versions", + versions: []string{}, + want: "", + }, + { + name: "different prerelease identifiers", + versions: []string{"1.0.0-alpha.1", "1.0.0-beta.1", "1.0.0-rc.1"}, + want: "1.0.0-rc.1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetLargestVersion(tt.versions); got != tt.want { + t.Errorf("GetLargestVersion() = %v, want %v", got, tt.want) + } + }) + } +}