feat: update

This commit is contained in:
xxnuo
2025-11-02 06:01:38 +08:00
parent 3e367bddc0
commit 23f99603db
11 changed files with 400 additions and 76 deletions

8
data/data.go Normal file
View File

@@ -0,0 +1,8 @@
package bin
import (
_ "embed"
)
//go:embed records.json
var RecordsJson []byte

View File

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

View File

@@ -1,5 +0,0 @@
package models
const (
recordsUrl = "https://remote-settings.mozilla.org/v1/buckets/main/collections/translations-models/records"
)

View File

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

View File

@@ -0,0 +1 @@
package models

View File

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

View File

@@ -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!")

46
internal/utils/file.go Normal file
View File

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

View File

@@ -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)
}
}

135
internal/utils/version.go Normal file
View File

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

View File

@@ -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)
}
})
}
}