mirror of
https://github.com/xxnuo/MTranServer.git
synced 2026-09-03 06:35:20 +08:00
feat: update
This commit is contained in:
8
data/data.go
Normal file
8
data/data.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package bin
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed records.json
|
||||
var RecordsJson []byte
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
recordsUrl = "https://remote-settings.mozilla.org/v1/buckets/main/collections/translations-models/records"
|
||||
)
|
||||
29
internal/models/records.go
Normal file
29
internal/models/records.go
Normal 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
|
||||
}
|
||||
1
internal/models/records_test.go
Normal file
1
internal/models/records_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package models
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
46
internal/utils/file.go
Normal 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
|
||||
}
|
||||
34
internal/utils/file_test.go
Normal file
34
internal/utils/file_test.go
Normal 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
135
internal/utils/version.go
Normal 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
|
||||
}
|
||||
80
internal/utils/version_test.go
Normal file
80
internal/utils/version_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user