Merge branch 'ClozyA_GoUpdater_dev' into dev

This commit is contained in:
DLmaster361
2025-07-31 20:42:52 +08:00
36 changed files with 8295 additions and 0 deletions

View File

@@ -95,6 +95,17 @@ jobs:
output-file: AUTO_MAA
output-dir: AUTO_MAA
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Build go updater
shell: pwsh
run: |
go install github.com/akavel/rsrc@latest
Go_Updater/build.ps1
- name: Upload unsigned main program
id: upload-unsigned-main-program
uses: actions/upload-artifact@v4
@@ -122,6 +133,8 @@ jobs:
$ver = "${{ steps.get_version.outputs.main_version }}"
Copy-Item "$root/app" "$root/AUTO_MAA/app" -Recurse
Copy-Item "$root/resources" "$root/AUTO_MAA/resources" -Recurse
Copy-Item "$root/Go_Updater" "$root/AUTO_MAA/Go_Updater" -Recurse
Move-Item "$root/AUTO_MAA/Go_Updater/build/AUTO_MAA_Go_Updater.exe" "$root/AUTO_MAA/AUTO_MAA_Go_Updater_install.exe"
Copy-Item "$root/main.py" "$root/AUTO_MAA/"
Copy-Item "$root/requirements.txt" "$root/AUTO_MAA/"
Copy-Item "$root/README.md" "$root/AUTO_MAA/"

116
Go_Updater/Makefile Normal file
View File

@@ -0,0 +1,116 @@
# AUTO_MAA_Go_Updater Makefile
# Build variables
VERSION ?= 1.0.0
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
OUTPUT_NAME := AUTO_MAA_Go_Updater
BUILD_DIR := build
DIST_DIR := dist
# Go build flags
LDFLAGS := -s -w -X AUTO_MAA_Go_Updater/version.Version=$(VERSION) -X AUTO_MAA_Go_Updater/version.BuildTime=$(BUILD_TIME) -X AUTO_MAA_Go_Updater/version.GitCommit=$(GIT_COMMIT)
# Default target
.PHONY: all
all: clean build
# Clean build artifacts
.PHONY: clean
clean:
@echo "Cleaning build artifacts..."
@rm -rf $(BUILD_DIR) $(DIST_DIR)
@mkdir -p $(BUILD_DIR) $(DIST_DIR)
# Build for Windows 64-bit
.PHONY: build
build: clean
@echo "========================================="
@echo "Building AUTO_MAA_Go_Updater"
@echo "========================================="
@echo "Version: $(VERSION)"
@echo "Build Time: $(BUILD_TIME)"
@echo "Git Commit: $(GIT_COMMIT)"
@echo "Target: Windows 64-bit"
@echo ""
@echo "Building application..."
@GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(OUTPUT_NAME).exe .
@echo "Build completed successfully!"
@echo ""
@echo "Build Results:"
@ls -lh $(BUILD_DIR)/$(OUTPUT_NAME).exe
@cp $(BUILD_DIR)/$(OUTPUT_NAME).exe $(DIST_DIR)/$(OUTPUT_NAME).exe
@echo "Copied to: $(DIST_DIR)/$(OUTPUT_NAME).exe"
# Build with UPX compression
.PHONY: build-compressed
build-compressed: build
@echo ""
@echo "Compressing with UPX..."
@if command -v upx >/dev/null 2>&1; then \
upx --best $(BUILD_DIR)/$(OUTPUT_NAME).exe; \
echo "Compression completed!"; \
ls -lh $(BUILD_DIR)/$(OUTPUT_NAME).exe; \
cp $(BUILD_DIR)/$(OUTPUT_NAME).exe $(DIST_DIR)/$(OUTPUT_NAME).exe; \
else \
echo "UPX not found. Skipping compression."; \
fi
# Run tests
.PHONY: test
test:
@echo "Running tests..."
@go test -v ./...
# Run with version flag
.PHONY: version
version: build
@echo ""
@echo "Testing version information:"
@$(BUILD_DIR)/$(OUTPUT_NAME).exe -version
# Install dependencies
.PHONY: deps
deps:
@echo "Installing dependencies..."
@go mod tidy
@go mod download
# Format code
.PHONY: fmt
fmt:
@echo "Formatting code..."
@go fmt ./...
# Lint code
.PHONY: lint
lint:
@echo "Linting code..."
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run; \
else \
echo "golangci-lint not found. Install it with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
fi
# Development build (faster, no optimizations)
.PHONY: dev
dev:
@echo "Building development version..."
@go build -o $(BUILD_DIR)/$(OUTPUT_NAME)-dev.exe .
@echo "Development build completed: $(BUILD_DIR)/$(OUTPUT_NAME)-dev.exe"
# Help
.PHONY: help
help:
@echo "Available targets:"
@echo " all - Clean and build (default)"
@echo " build - Build for Windows 64-bit"
@echo " build-compressed - Build and compress with UPX"
@echo " clean - Clean build artifacts"
@echo " test - Run tests"
@echo " version - Build and show version"
@echo " deps - Install dependencies"
@echo " fmt - Format code"
@echo " lint - Lint code"
@echo " dev - Development build"
@echo " help - Show this help"

15
Go_Updater/README.MD Normal file
View File

@@ -0,0 +1,15 @@
# 用Go语言实现的一个AUTO_MAA下载器
用于直接下载AUTO_MAA软件本体在Python版本出现问题时使用。
## 使用方法
1. 下载并安装Go语言环境需要配置环境变量
2. 运行 `go mod tidy` 命令,安装依赖包。
3. 运行 `go run main.go` 命令程序会自动下载并安装AUTO_MAA软件。
## 构建
运行 `.\build.ps1` 脚本即可完成构建。
参数说明:
-Version指定要构建的版本号
运行命令: `.\build.ps1 -Version "1.0.8"`

290
Go_Updater/api/client.go Normal file
View File

@@ -0,0 +1,290 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// MirrorResponse 表示 MirrorChyan API 的响应结构
type MirrorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
} `json:"data"`
}
// UpdateCheckParams 表示更新检查的参数
type UpdateCheckParams struct {
ResourceID string
CurrentVersion string
Channel string
UserAgent string
}
// MirrorClient 定义 Mirror API 客户端的接口方法
type MirrorClient interface {
CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error)
IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool
GetDownloadURL(versionName string) string
}
// Client 实现 MirrorClient 接口
type Client struct {
httpClient *http.Client
baseURL string
downloadURL string
}
// NewClient 创建新的 Mirror API 客户端
func NewClient() *Client {
return &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
baseURL: "https://mirrorchyan.com/api/resources",
downloadURL: "http://221.236.27.82:10197/d/AUTO_MAA",
}
}
// CheckUpdate 调用 MirrorChyan API 检查更新
func (c *Client) CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error) {
// 构建 API URL
apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, params.ResourceID)
// 解析 URL 并添加查询参数
u, err := url.Parse(apiURL)
if err != nil {
return nil, fmt.Errorf("解析 API URL 失败: %w", err)
}
// 添加查询参数
q := u.Query()
q.Set("current_version", params.CurrentVersion)
q.Set("channel", params.Channel)
q.Set("os", "") // 跨平台为空
q.Set("arch", "") // 跨平台为空
u.RawQuery = q.Encode()
// 创建 HTTP 请求
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
}
// 设置 User-Agent 头
if params.UserAgent != "" {
req.Header.Set("User-Agent", params.UserAgent)
} else {
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
}
// 发送 HTTP 请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送 HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
// 检查 HTTP 状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API 返回非 200 状态码: %d", resp.StatusCode)
}
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %w", err)
}
// 解析 JSON 响应
var mirrorResp MirrorResponse
if err := json.Unmarshal(body, &mirrorResp); err != nil {
return nil, fmt.Errorf("解析 JSON 响应失败: %w", err)
}
return &mirrorResp, nil
}
// IsUpdateAvailable 比较当前版本与 API 响应中的最新版本
func (c *Client) IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool {
// 检查 API 响应是否成功
if response.Code != 0 {
return false
}
// 从响应中获取最新版本
latestVersion := response.Data.VersionName
if latestVersion == "" {
return false
}
// 转换版本格式以便比较
currentVersionNormalized := c.normalizeVersionForComparison(currentVersion)
latestVersionNormalized := c.normalizeVersionForComparison(latestVersion)
// 调试输出
// fmt.Printf("Current: %s -> %s\n", currentVersion, currentVersionNormalized)
// fmt.Printf("Latest: %s -> %s\n", latestVersion, latestVersionNormalized)
// fmt.Printf("Compare result: %d\n", compareVersions(currentVersionNormalized, latestVersionNormalized))
// 使用语义版本比较
return compareVersions(currentVersionNormalized, latestVersionNormalized) < 0
}
// normalizeVersionForComparison 将不同版本格式转换为可比较格式
func (c *Client) normalizeVersionForComparison(version string) string {
// 处理 AUTO_MAA 版本格式: "4.4.1.3" -> "v4.4.1-beta3"
if !strings.HasPrefix(version, "v") && strings.Count(version, ".") == 3 {
parts := strings.Split(version, ".")
if len(parts) == 4 {
major, minor, patch, beta := parts[0], parts[1], parts[2], parts[3]
if beta == "0" {
return fmt.Sprintf("v%s.%s.%s", major, minor, patch)
} else {
return fmt.Sprintf("v%s.%s.%s-beta%s", major, minor, patch, beta)
}
}
}
// 如果已经是标准格式则直接返回
return version
}
// compareVersions 比较两个语义版本字符串
// 返回值: -1 如果 v1 < v2, 0 如果 v1 == v2, 1 如果 v1 > v2
func compareVersions(v1, v2 string) int {
// 通过移除 'v' 前缀来标准化版本
v1 = normalizeVersion(v1)
v2 = normalizeVersion(v2)
// 解析版本组件
parts1 := parseVersionParts(v1)
parts2 := parseVersionParts(v2)
// 比较每个组件
maxLen := len(parts1)
if len(parts2) > maxLen {
maxLen = len(parts2)
}
for i := 0; i < maxLen; i++ {
var p1, p2 int
if i < len(parts1) {
p1 = parts1[i]
}
if i < len(parts2) {
p2 = parts2[i]
}
if p1 < p2 {
return -1
} else if p1 > p2 {
return 1
}
}
return 0
}
// normalizeVersion 移除 'v' 前缀并处理常见版本格式
func normalizeVersion(version string) string {
if len(version) > 0 && (version[0] == 'v' || version[0] == 'V') {
return version[1:]
}
return version
}
// parseVersionParts 将版本字符串解析为数字组件包括beta版本号
func parseVersionParts(version string) []int {
if version == "" {
return []int{0}
}
parts := make([]int, 0, 4)
current := 0
// 先检查是否包含 -beta
betaIndex := strings.Index(version, "-beta")
var mainVersion, betaVersion string
if betaIndex != -1 {
mainVersion = version[:betaIndex]
betaVersion = version[betaIndex+5:] // 跳过 "-beta"
} else {
mainVersion = version
betaVersion = ""
}
// 解析主版本号 (major.minor.patch)
for _, char := range mainVersion {
if char >= '0' && char <= '9' {
current = current*10 + int(char-'0')
} else if char == '.' {
parts = append(parts, current)
current = 0
} else {
// 遇到非数字非点字符,停止解析
break
}
}
// 添加最后一个主版本组件
parts = append(parts, current)
// 确保至少有 3 个组件 (major.minor.patch)
for len(parts) < 3 {
parts = append(parts, 0)
}
// 解析beta版本号
if betaVersion != "" {
// 跳过可能的点号
if strings.HasPrefix(betaVersion, ".") {
betaVersion = betaVersion[1:]
}
betaNum := 0
for _, char := range betaVersion {
if char >= '0' && char <= '9' {
betaNum = betaNum*10 + int(char-'0')
} else {
break
}
}
parts = append(parts, betaNum)
} else {
// 非beta版本添加0作为beta版本号
parts = append(parts, 0)
}
return parts
}
// GetDownloadURL 根据版本名生成下载站的下载 URL
func (c *Client) GetDownloadURL(versionName string) string {
// 将版本名转换为文件名格式
// 例如: "v4.4.0" -> "AUTO_MAA_v4.4.0.zip"
// 例如: "v4.4.1-beta3" -> "AUTO_MAA_v4.4.1-beta.3.zip"
filename := fmt.Sprintf("AUTO_MAA_%s.zip", versionName)
// 处理 beta 版本: 将 "beta3" 转换为 "beta.3"
if strings.Contains(filename, "-beta") && !strings.Contains(filename, "-beta.") {
filename = strings.Replace(filename, "-beta", "-beta.", 1)
}
return fmt.Sprintf("%s/%s", c.downloadURL, filename)
}

View File

@@ -0,0 +1,186 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestNewClient(t *testing.T) {
client := NewClient()
if client == nil {
t.Fatal("NewClient() 返回 nil")
}
if client.httpClient == nil {
t.Fatal("HTTP 客户端为 nil")
}
if client.baseURL != "https://mirrorchyan.com/api/resources" {
t.Errorf("期望基础 URL 'https://mirrorchyan.com/api/resources',得到 '%s'", client.baseURL)
}
if client.downloadURL != "http://221.236.27.82:10197/d/AUTO_MAA" {
t.Errorf("期望下载 URL 'http://221.236.27.82:10197/d/AUTO_MAA',得到 '%s'", client.downloadURL)
}
}
func TestGetDownloadURL(t *testing.T) {
client := NewClient()
tests := []struct {
versionName string
expected string
}{
{"v4.4.0", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v4.4.0.zip"},
{"v4.4.1-beta3", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v4.4.1-beta.3.zip"},
{"v1.2.3", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v1.2.3.zip"},
}
for _, test := range tests {
result := client.GetDownloadURL(test.versionName)
if result != test.expected {
t.Errorf("版本 %s期望 %s得到 %s", test.versionName, test.expected, result)
}
}
}
func TestCheckUpdate(t *testing.T) {
// 创建测试服务器
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response := MirrorResponse{
Code: 0,
Msg: "success",
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
}{
VersionName: "v4.4.1",
VersionNumber: 48,
Channel: "stable",
ReleaseNote: "测试发布说明",
},
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
if err != nil {
return
}
}))
defer server.Close()
// 使用测试服务器 URL 创建客户端
client := &Client{
httpClient: &http.Client{},
baseURL: server.URL,
downloadURL: "http://221.236.27.82:10197/d/AUTO_MAA",
}
// 测试更新检查
params := UpdateCheckParams{
ResourceID: "AUTO_MAA",
CurrentVersion: "4.4.0.0",
Channel: "stable",
UserAgent: "TestAgent/1.0",
}
response, err := client.CheckUpdate(params)
if err != nil {
t.Fatalf("CheckUpdate 失败: %v", err)
}
if response.Code != 0 {
t.Errorf("期望代码 0得到 %d", response.Code)
}
if response.Data.VersionName != "v4.4.1" {
t.Errorf("期望版本 v4.4.1,得到 %s", response.Data.VersionName)
}
}
func TestIsUpdateAvailable(t *testing.T) {
client := NewClient()
tests := []struct {
name string
response *MirrorResponse
currentVersion string
expected bool
}{
{
name: "有可用更新",
response: &MirrorResponse{
Code: 0,
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
}{VersionName: "v4.4.1"},
},
currentVersion: "4.4.0.0",
expected: true,
},
{
name: "无可用更新",
response: &MirrorResponse{
Code: 0,
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
}{VersionName: "v4.4.0"},
},
currentVersion: "4.4.0.0",
expected: false,
},
{
name: "beta版本有更新",
response: &MirrorResponse{
Code: 0,
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
}{VersionName: "v4.4.1-beta.4"},
},
currentVersion: "4.4.1.3",
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := client.IsUpdateAvailable(test.response, test.currentVersion)
if result != test.expected {
t.Errorf("期望 %t得到 %t", test.expected, result)
}
})
}
}

34
Go_Updater/app.rc Normal file
View File

@@ -0,0 +1,34 @@
#include <windows.h>
// Application icon
IDI_ICON1 ICON "icon/AUTO_MAA_Go_Updater.ico"
// Version information
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "AUTO MAA Team"
VALUE "FileDescription", "AUTO MAA Go Updater"
VALUE "FileVersion", "1.0.0.0"
VALUE "InternalName", "AUTO_MAA_Go_Updater"
VALUE "LegalCopyright", "Copyright (C) 2025"
VALUE "OriginalFilename", "AUTO_MAA_Go_Updater.exe"
VALUE "ProductName", "AUTO MAA Go Updater"
VALUE "ProductVersion", "1.0.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

BIN
Go_Updater/app.syso Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
package assets
import (
"embed"
"io/fs"
)
//go:embed config_template.yaml
var EmbeddedAssets embed.FS
// GetConfigTemplate 返回嵌入的配置模板
func GetConfigTemplate() ([]byte, error) {
return EmbeddedAssets.ReadFile("config_template.yaml")
}
// GetAssetFS 返回嵌入的文件系统
func GetAssetFS() fs.FS {
return EmbeddedAssets
}
// ListAssets 返回所有嵌入资源的列表
func ListAssets() ([]string, error) {
var assets []string
err := fs.WalkDir(EmbeddedAssets, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
assets = append(assets, path)
}
return nil
})
return assets, err
}

View File

@@ -0,0 +1,100 @@
package assets
import (
"testing"
)
func TestGetConfigTemplate(t *testing.T) {
data, err := GetConfigTemplate()
if err != nil {
t.Fatalf("Failed to get config template: %v", err)
}
if len(data) == 0 {
t.Fatal("Config template is empty")
}
// Check that it contains expected content
content := string(data)
if !contains(content, "resource_id") {
t.Error("Config template should contain 'resource_id'")
}
if !contains(content, "current_version") {
t.Error("Config template should contain 'current_version'")
}
if !contains(content, "user_agent") {
t.Error("Config template should contain 'user_agent'")
}
}
func TestListAssets(t *testing.T) {
assets, err := ListAssets()
if err != nil {
t.Fatalf("Failed to list assets: %v", err)
}
if len(assets) == 0 {
t.Fatal("No assets found")
}
// Check that config template is in the list
found := false
for _, asset := range assets {
if asset == "config_template.yaml" {
found = true
break
}
}
if !found {
t.Error("config_template.yaml should be in the assets list")
}
}
func TestGetAssetFS(t *testing.T) {
fs := GetAssetFS()
if fs == nil {
t.Fatal("Asset filesystem should not be nil")
}
// Try to open the config template
file, err := fs.Open("config_template.yaml")
if err != nil {
t.Fatalf("Failed to open config template from filesystem: %v", err)
}
defer file.Close()
// Check that we can read from it
buffer := make([]byte, 100)
n, err := file.Read(buffer)
if err != nil && err.Error() != "EOF" {
t.Fatalf("Failed to read from config template: %v", err)
}
if n == 0 {
t.Fatal("Config template appears to be empty")
}
}
// Helper function to check if string contains substring
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > len(substr) && (s[:len(substr)] == substr ||
s[len(s)-len(substr):] == substr ||
containsAt(s, substr, 1))))
}
func containsAt(s, substr string, start int) bool {
if start >= len(s) {
return false
}
if start+len(substr) > len(s) {
return containsAt(s, substr, start+1)
}
if s[start:start+len(substr)] == substr {
return true
}
return containsAt(s, substr, start+1)
}

View File

@@ -0,0 +1,7 @@
resource_id: "AUTO_MAA"
current_version: "v1.0.0"
user_agent: "AUTO_MAA_Go_Updater/1.0"
backup_url: "https://backup-download-site.com/releases"
log_level: "info"
auto_check: true
check_interval: 3600 # seconds

View File

@@ -0,0 +1,55 @@
# Build Configuration for AUTO_MAA_Go_Updater
project:
name: "AUTO_MAA_Go_Updater"
module: "AUTO_MAA_Go_Updater"
description: "AUTO_MAA_Go版本更新器"
version:
default: "1.0.0"
build_time_format: "2006-01-02T15:04:05Z"
targets:
- name: "windows-amd64"
goos: "windows"
goarch: "amd64"
cgo_enabled: true
output: "AUTO_MAA_Go_Updater.exe"
build:
flags:
ldflags: "-s -w"
tags: []
optimization:
strip_debug: true
strip_symbols: true
upx_compression: false # Optional, requires UPX
size_requirements:
max_size_mb: 10
warn_size_mb: 8
assets:
embed:
- "assets/config_template.yaml"
directories:
build: "build"
dist: "dist"
temp: "temp"
version_injection:
package: "AUTO_MAA_Go_Updater/version"
variables:
- name: "Version"
source: "version"
- name: "BuildTime"
source: "build_time"
- name: "GitCommit"
source: "git_commit"
quality:
run_tests: true
run_lint: false # Optional
format_code: true

93
Go_Updater/build.bat Normal file
View File

@@ -0,0 +1,93 @@
@echo off
setlocal enabledelayedexpansion
echo ========================================
echo AUTO_MAA_Go_Updater Build Script
echo ========================================
:: Set build variables
set OUTPUT_NAME=AUTO_MAA_Go_Updater.exe
set BUILD_DIR=build
set DIST_DIR=dist
:: Get current datetime for build time
for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a"
set "YYYY=%dt:~0,4%" & set "MM=%dt:~4,2%" & set "DD=%dt:~6,2%"
set "HH=%dt:~8,2%" & set "Min=%dt:~10,2%" & set "Sec=%dt:~12,2%"
set "BUILD_TIME=%YYYY%-%MM%-%DD%T%HH%:%Min%:%Sec%Z"
:: Get git commit hash (if available)
git rev-parse --short HEAD > temp_commit.txt 2>nul
if exist temp_commit.txt (
set /p GIT_COMMIT=<temp_commit.txt
del temp_commit.txt
) else (
set GIT_COMMIT=unknown
)
:: Use commit hash as version
set VERSION=%GIT_COMMIT%
echo Build Information:
echo - Version: %VERSION%
echo - Build Time: %BUILD_TIME%
echo - Git Commit: %GIT_COMMIT%
echo - Target: Windows 64-bit
echo.
:: Create build directories
if not exist %BUILD_DIR% mkdir %BUILD_DIR%
if not exist %DIST_DIR% mkdir %DIST_DIR%
:: Set build flags
set LDFLAGS=-s -w -X AUTO_MAA_Go_Updater/version.Version=%VERSION% -X AUTO_MAA_Go_Updater/version.BuildTime=%BUILD_TIME% -X AUTO_MAA_Go_Updater/version.GitCommit=%GIT_COMMIT%
echo Building application...
:: Ensure icon resource is compiled
if not exist app.syso (
echo Compiling icon resource...
where rsrc >nul 2>&1
if !ERRORLEVEL! equ 0 (
rsrc -ico icon\AUTO_MAA_Go_Updater.ico -o app.syso
if !ERRORLEVEL! equ 0 (
echo Icon resource compiled successfully
) else (
echo Warning: Failed to compile icon resource
)
) else (
echo Warning: rsrc not found. Install with: go install github.com/akavel/rsrc@latest
)
)
:: Set environment variables for Go build
set GOOS=windows
set GOARCH=amd64
set CGO_ENABLED=1
:: Build the application
go build -ldflags="%LDFLAGS%" -o %BUILD_DIR%\%OUTPUT_NAME% .
if %ERRORLEVEL% neq 0 (
echo Build failed!
exit /b 1
)
echo Build completed successfully!
:: Get file size
for %%A in (%BUILD_DIR%\%OUTPUT_NAME%) do set FILE_SIZE=%%~zA
set /a FILE_SIZE_MB=%FILE_SIZE%/1024/1024
echo.
echo Build Results:
echo - Output: %BUILD_DIR%\%OUTPUT_NAME%
echo - Size: %FILE_SIZE% bytes (~%FILE_SIZE_MB% MB)
:: Copy to dist directory
copy %BUILD_DIR%\%OUTPUT_NAME% %DIST_DIR%\%OUTPUT_NAME% >nul
echo - Copied to: %DIST_DIR%\%OUTPUT_NAME%
echo.
echo Build script completed successfully!
echo ========================================

105
Go_Updater/build.ps1 Normal file
View File

@@ -0,0 +1,105 @@
# AUTO_MAA_Go_Updater Build Script (PowerShell)
param(
[string]$OutputName = "AUTO_MAA_Go_Updater.exe",
[switch]$Compress = $false
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "AUTO_MAA_Go_Updater Build Script" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Set build variables
$BuildDir = "build"
$DistDir = "dist"
$BuildTime = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")
# Get git commit hash
try {
$GitCommit = (git rev-parse --short HEAD 2>$null).Trim()
if (-not $GitCommit) { $GitCommit = "unknown" }
} catch {
$GitCommit = "unknown"
}
Write-Host "Build Information:" -ForegroundColor Yellow
Write-Host "- Version: $GitCommit"
Write-Host "- Build Time: $BuildTime"
Write-Host "- Git Commit: $GitCommit"
Write-Host "- Target: Windows 64-bit"
Write-Host ""
# Create build directories
if (-not (Test-Path $BuildDir)) { New-Item -ItemType Directory -Path $BuildDir | Out-Null }
if (-not (Test-Path $DistDir)) { New-Item -ItemType Directory -Path $DistDir | Out-Null }
# Set environment variables
$env:GOOS = "windows"
$env:GOARCH = "amd64"
$env:CGO_ENABLED = "1"
# Set build flags
$LdFlags = "-s -w -X AUTO_MAA_Go_Updater/version.Version=$Version -X AUTO_MAA_Go_Updater/version.BuildTime=$BuildTime -X AUTO_MAA_Go_Updater/version.GitCommit=$GitCommit"
Write-Host "Building application..." -ForegroundColor Green
# Ensure icon resource is compiled
if (-not (Test-Path "app.syso")) {
Write-Host "Compiling icon resource..." -ForegroundColor Yellow
if (Get-Command rsrc -ErrorAction SilentlyContinue) {
rsrc -ico icon/AUTO_MAA_Go_Updater.ico -o app.syso
if ($LASTEXITCODE -ne 0) {
Write-Host "Warning: Failed to compile icon resource" -ForegroundColor Yellow
} else {
Write-Host "Icon resource compiled successfully" -ForegroundColor Green
}
} else {
Write-Host "Warning: rsrc not found. Install with: go install github.com/akavel/rsrc@latest" -ForegroundColor Yellow
}
}
# Build the application
$BuildCommand = "go build -ldflags=`"$LdFlags`" -o $BuildDir\$OutputName ."
Invoke-Expression $BuildCommand
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed!" -ForegroundColor Red
exit 1
}
Write-Host "Build completed successfully!" -ForegroundColor Green
# Get file information
$OutputFile = Get-Item "$BuildDir\$OutputName"
$FileSizeMB = [math]::Round($OutputFile.Length / 1MB, 2)
Write-Host ""
Write-Host "Build Results:" -ForegroundColor Yellow
Write-Host "- Output: $($OutputFile.FullName)"
Write-Host "- Size: $($OutputFile.Length) bytes (~$FileSizeMB MB)"
# Optional UPX compression
if ($Compress) {
Write-Host ""
Write-Host "Compressing with UPX..." -ForegroundColor Yellow
if (Get-Command upx -ErrorAction SilentlyContinue) {
upx --best "$BuildDir\$OutputName"
$CompressedFile = Get-Item "$BuildDir\$OutputName"
$CompressedSizeMB = [math]::Round($CompressedFile.Length / 1MB, 2)
Write-Host "- Compressed Size: $($CompressedFile.Length) bytes (~$CompressedSizeMB MB)" -ForegroundColor Green
} else {
Write-Host "UPX not found. Skipping compression." -ForegroundColor Yellow
}
}
# Copy to dist directory
Copy-Item "$BuildDir\$OutputName" "$DistDir\$OutputName" -Force
Write-Host "- Copied to: $DistDir\$OutputName"
Write-Host ""
Write-Host "Build script completed successfully!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

198
Go_Updater/config/config.go Normal file
View File

@@ -0,0 +1,198 @@
package config
import (
"fmt"
"os"
"path/filepath"
"AUTO_MAA_Go_Updater/assets"
"gopkg.in/yaml.v3"
)
// Config 表示应用程序配置
type Config struct {
ResourceID string `yaml:"resource_id"`
CurrentVersion string `yaml:"current_version"`
UserAgent string `yaml:"user_agent"`
BackupURL string `yaml:"backup_url"`
LogLevel string `yaml:"log_level"`
AutoCheck bool `yaml:"auto_check"`
CheckInterval int `yaml:"check_interval"` // 秒
}
// ConfigManager 定义配置管理的接口方法
type ConfigManager interface {
Load() (*Config, error)
Save(config *Config) error
GetConfigPath() string
}
// DefaultConfigManager 实现 ConfigManager 接口
type DefaultConfigManager struct {
configPath string
}
// NewConfigManager 创建新的配置管理器
func NewConfigManager() ConfigManager {
configDir := getConfigDir()
configPath := filepath.Join(configDir, "config.yaml")
return &DefaultConfigManager{
configPath: configPath,
}
}
// GetConfigPath 返回配置文件的路径
func (cm *DefaultConfigManager) GetConfigPath() string {
return cm.configPath
}
// Load 读取并解析配置文件
func (cm *DefaultConfigManager) Load() (*Config, error) {
// 如果配置目录不存在则创建
configDir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, fmt.Errorf("创建配置目录失败: %w", err)
}
// 如果配置文件不存在,创建默认配置
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
defaultConfig := getDefaultConfig()
if err := cm.Save(defaultConfig); err != nil {
return nil, fmt.Errorf("创建默认配置失败: %w", err)
}
return defaultConfig, nil
}
// 读取现有配置文件
data, err := os.ReadFile(cm.configPath)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
// 验证并应用缺失字段的默认值
if err := validateAndApplyDefaults(&config); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err)
}
return &config, nil
}
// Save 将配置写入文件
func (cm *DefaultConfigManager) Save(config *Config) error {
// 保存前验证配置
if err := validateConfig(config); err != nil {
return fmt.Errorf("配置验证失败: %w", err)
}
// 如果配置目录不存在则创建
configDir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("创建配置目录失败: %w", err)
}
// 将配置序列化为 YAML
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("序列化配置失败: %w", err)
}
// 写入文件
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
return fmt.Errorf("写入配置文件失败: %w", err)
}
return nil
}
// getDefaultConfig 返回带有默认值的配置
func getDefaultConfig() *Config {
// 首先尝试从嵌入模板加载
if templateData, err := assets.GetConfigTemplate(); err == nil {
var config Config
if err := yaml.Unmarshal(templateData, &config); err == nil {
return &config
}
}
// 如果模板加载失败则回退到硬编码默认值
return &Config{
ResourceID: "M9A", // 默认资源 ID
CurrentVersion: "v1.0.0",
UserAgent: "AUTO_MAA_Go_Updater/1.0",
BackupURL: "",
LogLevel: "info",
AutoCheck: true,
CheckInterval: 3600, // 1 小时
}
}
// validateConfig 验证配置值
func validateConfig(config *Config) error {
if config == nil {
return fmt.Errorf("配置不能为空")
}
if config.ResourceID == "" {
return fmt.Errorf("resource_id 不能为空")
}
if config.CurrentVersion == "" {
return fmt.Errorf("current_version 不能为空")
}
if config.UserAgent == "" {
return fmt.Errorf("user_agent 不能为空")
}
validLogLevels := map[string]bool{
"debug": true,
"info": true,
"warn": true,
"error": true,
}
if !validLogLevels[config.LogLevel] {
return fmt.Errorf("无效的 log_level: %s (必须是 debug, info, warn 或 error)", config.LogLevel)
}
if config.CheckInterval < 60 {
return fmt.Errorf("check_interval 必须至少为 60 秒")
}
return nil
}
// validateAndApplyDefaults 验证配置并为缺失字段应用默认值
func validateAndApplyDefaults(config *Config) error {
defaults := getDefaultConfig()
// 为空字段应用默认值
if config.UserAgent == "" {
config.UserAgent = defaults.UserAgent
}
if config.LogLevel == "" {
config.LogLevel = defaults.LogLevel
}
if config.CheckInterval == 0 {
config.CheckInterval = defaults.CheckInterval
}
if config.CurrentVersion == "" {
config.CurrentVersion = defaults.CurrentVersion
}
// 应用默认值后进行验证
return validateConfig(config)
}
// getConfigDir 返回配置目录路径
func getConfigDir() string {
// 在 Windows 上使用 APPDATA回退到当前目录
if appData := os.Getenv("APPDATA"); appData != "" {
return filepath.Join(appData, "AUTO_MAA_Go_Updater")
}
return "."
}

View File

@@ -0,0 +1,55 @@
{
"Function": {
"BossKey": "",
"HistoryRetentionTime": 0,
"HomeImageMode": "默认",
"IfAgreeBilibili": true,
"IfAllowSleep": false,
"IfSilence": false,
"IfSkipMumuSplashAds": false,
"UnattendedMode": false
},
"Notify": {
"AuthorizationCode": "",
"CompanyWebHookBotUrl": "",
"FromAddress": "",
"IfCompanyWebHookBot": false,
"IfPushPlyer": false,
"IfSendMail": false,
"IfSendSixStar": false,
"IfSendStatistic": false,
"IfServerChan": false,
"SMTPServerAddress": "",
"SendTaskResultTime": "不推送",
"ServerChanChannel": "",
"ServerChanKey": "",
"ServerChanTag": "",
"ToAddress": ""
},
"Start": {
"IfMinimizeDirectly": false,
"IfRunDirectly": false,
"IfSelfStart": false
},
"QFluentWidgets": {
"ThemeColor": "#ff009faa",
"ThemeMode": "Dark"
},
"UI": {
"IfShowTray": false,
"IfToTray": false,
"location": "100x100",
"maximized": false,
"size": "1200x700"
},
"Update": {
"IfAutoUpdate": false,
"ProxyUrlList": [],
"ThreadNumb": 8,
"UpdateType": "stable"
},
"Voice": {
"Enabled": false,
"Type": "simple"
}
}

View File

@@ -0,0 +1,153 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestConfigManagerLoadSave(t *testing.T) {
// 为测试创建临时目录
tempDir := t.TempDir()
// 使用临时路径创建配置管理器
cm := &DefaultConfigManager{
configPath: filepath.Join(tempDir, "test-config.yaml"),
}
// 测试加载不存在的配置(应创建默认配置)
config, err := cm.Load()
if err != nil {
t.Errorf("加载配置失败: %v", err)
}
if config == nil {
t.Errorf("配置不应为 nil")
}
// 验证默认值
if config.CurrentVersion != "v1.0.0" {
t.Errorf("期望默认版本 v1.0.0,得到 %s", config.CurrentVersion)
}
if config.UserAgent != "AUTO_MAA_Go_Updater/1.0" {
t.Errorf("期望默认用户代理,得到 %s", config.UserAgent)
}
// 设置一些值
config.ResourceID = "TEST123"
// 保存配置
err = cm.Save(config)
if err != nil {
t.Errorf("保存配置失败: %v", err)
}
// 再次加载配置
loadedConfig, err := cm.Load()
if err != nil {
t.Errorf("加载已保存配置失败: %v", err)
}
// 验证值
if loadedConfig.ResourceID != "TEST123" {
t.Errorf("期望 ResourceID TEST123得到 %s", loadedConfig.ResourceID)
}
}
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
config *Config
expectError bool
}{
{
name: "空配置",
config: nil,
expectError: true,
},
{
name: "空 ResourceID",
config: &Config{
ResourceID: "",
CurrentVersion: "v1.0.0",
UserAgent: "Test/1.0",
LogLevel: "info",
CheckInterval: 3600,
},
expectError: true,
},
{
name: "有效配置",
config: &Config{
ResourceID: "TEST",
CurrentVersion: "v1.0.0",
UserAgent: "Test/1.0",
LogLevel: "info",
CheckInterval: 3600,
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateConfig(tt.config)
if tt.expectError && err == nil {
t.Errorf("期望错误但没有得到")
}
if !tt.expectError && err != nil {
t.Errorf("期望无错误但得到: %v", err)
}
})
}
}
func TestGetDefaultConfig(t *testing.T) {
config := getDefaultConfig()
if config == nil {
t.Fatal("getDefaultConfig() 返回 nil")
}
// 验证默认值
if config.ResourceID != "AUTO_MAA" {
t.Errorf("期望 ResourceID 'AUTO_MAA',得到 %s", config.ResourceID)
}
if config.CurrentVersion != "v1.0.0" {
t.Errorf("期望 CurrentVersion 'v1.0.0',得到 %s", config.CurrentVersion)
}
if config.UserAgent != "AUTO_MAA_Go_Updater/1.0" {
t.Errorf("期望 UserAgent 'AUTO_MAA_Go_Updater/1.0',得到 %s", config.UserAgent)
}
if config.LogLevel != "info" {
t.Errorf("期望 LogLevel 'info',得到 %s", config.LogLevel)
}
if config.CheckInterval != 3600 {
t.Errorf("期望 CheckInterval 3600得到 %d", config.CheckInterval)
}
if !config.AutoCheck {
t.Errorf("期望 AutoCheck true得到 %v", config.AutoCheck)
}
}
func TestGetConfigDir(t *testing.T) {
// 保存原始 APPDATA
originalAppData := os.Getenv("APPDATA")
defer os.Setenv("APPDATA", originalAppData)
// 测试设置了 APPDATA
os.Setenv("APPDATA", "C:\\Users\\Test\\AppData\\Roaming")
dir := getConfigDir()
expected := "C:\\Users\\Test\\AppData\\Roaming\\AUTO_MAA_Go_Updater"
if dir != expected {
t.Errorf("期望 %s得到 %s", expected, dir)
}
// 测试没有 APPDATA
os.Unsetenv("APPDATA")
dir = getConfigDir()
if dir != "." {
t.Errorf("期望当前目录,得到 %s", dir)
}
}

View File

@@ -0,0 +1,224 @@
package download
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
// DownloadProgress 表示当前下载进度
type DownloadProgress struct {
BytesDownloaded int64
TotalBytes int64
Percentage float64
Speed int64 // 每秒字节数
}
// ProgressCallback 在下载过程中调用以报告进度
type ProgressCallback func(DownloadProgress)
// DownloadManager 定义下载操作的接口
type DownloadManager interface {
Download(url, destination string, progressCallback ProgressCallback) error
DownloadWithResume(url, destination string, progressCallback ProgressCallback) error
ValidateChecksum(filePath, expectedChecksum string) error
SetTimeout(timeout time.Duration)
}
// Manager 实现 DownloadManager 接口
type Manager struct {
client *http.Client
timeout time.Duration
}
// NewManager 创建新的下载管理器
func NewManager() *Manager {
return &Manager{
client: &http.Client{
Timeout: 30 * time.Second,
},
timeout: 30 * time.Second,
}
}
// Download 从给定 URL 下载文件到目标路径
func (m *Manager) Download(url, destination string, progressCallback ProgressCallback) error {
return m.downloadWithContext(context.Background(), url, destination, progressCallback, false)
}
// DownloadWithResume 下载文件并支持断点续传
func (m *Manager) DownloadWithResume(url, destination string, progressCallback ProgressCallback) error {
return m.downloadWithContext(context.Background(), url, destination, progressCallback, true)
}
// downloadWithContext 执行实际的下载并支持上下文
func (m *Manager) downloadWithContext(ctx context.Context, url, destination string, progressCallback ProgressCallback, resume bool) error {
// 如果目标目录不存在则创建
if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
return fmt.Errorf("创建目标目录失败: %w", err)
}
// 检查文件是否存在以支持断点续传
var existingSize int64
if resume {
if stat, err := os.Stat(destination); err == nil {
existingSize = stat.Size()
}
}
// 创建 HTTP 请求
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("创建请求失败: %w", err)
}
// 为断点续传添加范围头
if resume && existingSize > 0 {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", existingSize))
}
// 执行请求
resp, err := m.client.Do(req)
if err != nil {
return fmt.Errorf("执行请求失败: %w", err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return fmt.Errorf("意外的状态码: %d", resp.StatusCode)
}
// 获取总大小
totalSize := existingSize
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
if size, err := strconv.ParseInt(contentLength, 10, 64); err == nil {
totalSize += size
}
}
// 打开目标文件
var file *os.File
if resume && existingSize > 0 {
file, err = os.OpenFile(destination, os.O_WRONLY|os.O_APPEND, 0644)
} else {
file, err = os.Create(destination)
existingSize = 0
}
if err != nil {
return fmt.Errorf("创建目标文件失败: %w", err)
}
defer file.Close()
// 下载并跟踪进度
return m.copyWithProgress(resp.Body, file, existingSize, totalSize, progressCallback)
}
// copyWithProgress 复制数据并跟踪进度
func (m *Manager) copyWithProgress(src io.Reader, dst io.Writer, startBytes, totalBytes int64, progressCallback ProgressCallback) error {
buffer := make([]byte, 32*1024) // 32KB 缓冲区
downloaded := startBytes
startTime := time.Now()
lastUpdate := startTime
for {
n, err := src.Read(buffer)
if n > 0 {
if _, writeErr := dst.Write(buffer[:n]); writeErr != nil {
return fmt.Errorf("写入目标失败: %w", writeErr)
}
downloaded += int64(n)
// 每 100ms 更新一次进度
now := time.Now()
if progressCallback != nil && now.Sub(lastUpdate) >= 100*time.Millisecond {
elapsed := now.Sub(startTime).Seconds()
speed := int64(0)
if elapsed > 0 {
speed = int64(float64(downloaded-startBytes) / elapsed)
}
percentage := float64(0)
if totalBytes > 0 {
percentage = float64(downloaded) / float64(totalBytes) * 100
}
progressCallback(DownloadProgress{
BytesDownloaded: downloaded,
TotalBytes: totalBytes,
Percentage: percentage,
Speed: speed,
})
lastUpdate = now
}
}
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("从源读取失败: %w", err)
}
}
// 最终进度更新
if progressCallback != nil {
elapsed := time.Since(startTime).Seconds()
speed := int64(0)
if elapsed > 0 {
speed = int64(float64(downloaded-startBytes) / elapsed)
}
percentage := float64(100)
if totalBytes > 0 {
percentage = float64(downloaded) / float64(totalBytes) * 100
}
progressCallback(DownloadProgress{
BytesDownloaded: downloaded,
TotalBytes: totalBytes,
Percentage: percentage,
Speed: speed,
})
}
return nil
}
// ValidateChecksum 验证文件的 SHA256 校验和
func (m *Manager) ValidateChecksum(filePath, expectedChecksum string) error {
if expectedChecksum == "" {
return nil // 没有校验和需要验证
}
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("打开文件进行校验和验证失败: %w", err)
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return fmt.Errorf("计算校验和失败: %w", err)
}
actualChecksum := hex.EncodeToString(hash.Sum(nil))
if actualChecksum != expectedChecksum {
return fmt.Errorf("校验和不匹配: 期望 %s得到 %s", expectedChecksum, actualChecksum)
}
return nil
}
// SetTimeout 设置下载操作的超时时间
func (m *Manager) SetTimeout(timeout time.Duration) {
m.timeout = timeout
m.client.Timeout = timeout
}

File diff suppressed because it is too large Load Diff

219
Go_Updater/errors/errors.go Normal file
View File

@@ -0,0 +1,219 @@
package errors
import (
"fmt"
"time"
)
// ErrorType 定义错误类型枚举
type ErrorType int
const (
NetworkError ErrorType = iota
APIError
FileError
ConfigError
InstallError
)
// String 返回错误类型的字符串表示
func (et ErrorType) String() string {
switch et {
case NetworkError:
return "NetworkError"
case APIError:
return "APIError"
case FileError:
return "FileError"
case ConfigError:
return "ConfigError"
case InstallError:
return "InstallError"
default:
return "UnknownError"
}
}
// UpdaterError 统一的错误结构体
type UpdaterError struct {
Type ErrorType
Message string
Cause error
Timestamp time.Time
Context map[string]interface{}
}
// Error 实现error接口
func (ue *UpdaterError) Error() string {
if ue.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", ue.Type, ue.Message, ue.Cause)
}
return fmt.Sprintf("[%s] %s", ue.Type, ue.Message)
}
// Unwrap 支持错误链
func (ue *UpdaterError) Unwrap() error {
return ue.Cause
}
// NewUpdaterError 创建新的UpdaterError
func NewUpdaterError(errorType ErrorType, message string, cause error) *UpdaterError {
return &UpdaterError{
Type: errorType,
Message: message,
Cause: cause,
Timestamp: time.Now(),
Context: make(map[string]interface{}),
}
}
// WithContext 添加上下文信息
func (ue *UpdaterError) WithContext(key string, value interface{}) *UpdaterError {
ue.Context[key] = value
return ue
}
// GetUserFriendlyMessage 获取用户友好的错误消息
func (ue *UpdaterError) GetUserFriendlyMessage() string {
switch ue.Type {
case NetworkError:
return "网络连接失败,请检查网络连接后重试"
case APIError:
return "服务器响应异常,请稍后重试或联系技术支持"
case FileError:
return "文件操作失败,请检查文件权限和磁盘空间"
case ConfigError:
return "配置文件错误,程序将使用默认配置"
case InstallError:
return "安装过程中出现错误,程序将尝试回滚更改"
default:
return "发生未知错误,请联系技术支持"
}
}
// RetryConfig 重试配置
type RetryConfig struct {
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
BackoffFactor float64
RetryableErrors []ErrorType
}
// DefaultRetryConfig 默认重试配置
func DefaultRetryConfig() *RetryConfig {
return &RetryConfig{
MaxRetries: 3,
InitialDelay: time.Second,
MaxDelay: 30 * time.Second,
BackoffFactor: 2.0,
RetryableErrors: []ErrorType{NetworkError, APIError},
}
}
// IsRetryable 检查错误是否可重试
func (rc *RetryConfig) IsRetryable(err error) bool {
if ue, ok := err.(*UpdaterError); ok {
for _, retryableType := range rc.RetryableErrors {
if ue.Type == retryableType {
return true
}
}
}
return false
}
// CalculateDelay 计算重试延迟时间
func (rc *RetryConfig) CalculateDelay(attempt int) time.Duration {
delay := time.Duration(float64(rc.InitialDelay) * pow(rc.BackoffFactor, float64(attempt)))
if delay > rc.MaxDelay {
delay = rc.MaxDelay
}
return delay
}
// pow 简单的幂运算实现
func pow(base, exp float64) float64 {
result := 1.0
for i := 0; i < int(exp); i++ {
result *= base
}
return result
}
// RetryableOperation 可重试的操作函数类型
type RetryableOperation func() error
// ExecuteWithRetry 执行带重试的操作
func ExecuteWithRetry(operation RetryableOperation, config *RetryConfig) error {
var lastErr error
for attempt := 0; attempt <= config.MaxRetries; attempt++ {
err := operation()
if err == nil {
return nil
}
lastErr = err
// 如果不是可重试的错误,直接返回
if !config.IsRetryable(err) {
return err
}
// 如果已经是最后一次尝试,不再等待
if attempt == config.MaxRetries {
break
}
// 计算延迟时间并等待
delay := config.CalculateDelay(attempt)
time.Sleep(delay)
}
return lastErr
}
// ErrorHandler 错误处理器接口
type ErrorHandler interface {
HandleError(err error) error
ShouldRetry(err error) bool
GetUserMessage(err error) string
}
// DefaultErrorHandler 默认错误处理器
type DefaultErrorHandler struct {
retryConfig *RetryConfig
}
// NewDefaultErrorHandler 创建默认错误处理器
func NewDefaultErrorHandler() *DefaultErrorHandler {
return &DefaultErrorHandler{
retryConfig: DefaultRetryConfig(),
}
}
// HandleError 处理错误
func (h *DefaultErrorHandler) HandleError(err error) error {
if ue, ok := err.(*UpdaterError); ok {
// 记录错误上下文
ue.WithContext("handled_at", time.Now())
return ue
}
// 将普通错误包装为UpdaterError
return NewUpdaterError(NetworkError, "未分类错误", err)
}
// ShouldRetry 判断是否应该重试
func (h *DefaultErrorHandler) ShouldRetry(err error) bool {
return h.retryConfig.IsRetryable(err)
}
// GetUserMessage 获取用户友好的错误消息
func (h *DefaultErrorHandler) GetUserMessage(err error) string {
if ue, ok := err.(*UpdaterError); ok {
return ue.GetUserFriendlyMessage()
}
return "发生未知错误,请联系技术支持"
}

View File

@@ -0,0 +1,287 @@
package errors
import (
"fmt"
"testing"
"time"
)
func TestUpdaterError_Error(t *testing.T) {
tests := []struct {
name string
err *UpdaterError
expected string
}{
{
name: "error with cause",
err: &UpdaterError{
Type: NetworkError,
Message: "connection failed",
Cause: fmt.Errorf("timeout"),
},
expected: "[NetworkError] connection failed: timeout",
},
{
name: "error without cause",
err: &UpdaterError{
Type: APIError,
Message: "invalid response",
Cause: nil,
},
expected: "[APIError] invalid response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.expected {
t.Errorf("UpdaterError.Error() = %v, want %v", got, tt.expected)
}
})
}
}
func TestNewUpdaterError(t *testing.T) {
cause := fmt.Errorf("original error")
err := NewUpdaterError(FileError, "test message", cause)
if err.Type != FileError {
t.Errorf("Expected type %v, got %v", FileError, err.Type)
}
if err.Message != "test message" {
t.Errorf("Expected message 'test message', got '%v'", err.Message)
}
if err.Cause != cause {
t.Errorf("Expected cause %v, got %v", cause, err.Cause)
}
if err.Context == nil {
t.Error("Expected context to be initialized")
}
}
func TestUpdaterError_WithContext(t *testing.T) {
err := NewUpdaterError(ConfigError, "test", nil)
err.WithContext("key1", "value1").WithContext("key2", 42)
if err.Context["key1"] != "value1" {
t.Errorf("Expected context key1 to be 'value1', got %v", err.Context["key1"])
}
if err.Context["key2"] != 42 {
t.Errorf("Expected context key2 to be 42, got %v", err.Context["key2"])
}
}
func TestUpdaterError_GetUserFriendlyMessage(t *testing.T) {
tests := []struct {
errorType ErrorType
expected string
}{
{NetworkError, "网络连接失败,请检查网络连接后重试"},
{APIError, "服务器响应异常,请稍后重试或联系技术支持"},
{FileError, "文件操作失败,请检查文件权限和磁盘空间"},
{ConfigError, "配置文件错误,程序将使用默认配置"},
{InstallError, "安装过程中出现错误,程序将尝试回滚更改"},
}
for _, tt := range tests {
t.Run(tt.errorType.String(), func(t *testing.T) {
err := NewUpdaterError(tt.errorType, "test", nil)
if got := err.GetUserFriendlyMessage(); got != tt.expected {
t.Errorf("GetUserFriendlyMessage() = %v, want %v", got, tt.expected)
}
})
}
}
func TestRetryConfig_IsRetryable(t *testing.T) {
config := DefaultRetryConfig()
tests := []struct {
name string
err error
expected bool
}{
{
name: "retryable network error",
err: NewUpdaterError(NetworkError, "test", nil),
expected: true,
},
{
name: "retryable api error",
err: NewUpdaterError(APIError, "test", nil),
expected: true,
},
{
name: "non-retryable file error",
err: NewUpdaterError(FileError, "test", nil),
expected: false,
},
{
name: "regular error",
err: fmt.Errorf("regular error"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := config.IsRetryable(tt.err); got != tt.expected {
t.Errorf("IsRetryable() = %v, want %v", got, tt.expected)
}
})
}
}
func TestRetryConfig_CalculateDelay(t *testing.T) {
config := DefaultRetryConfig()
tests := []struct {
attempt int
expected time.Duration
}{
{0, time.Second},
{1, 2 * time.Second},
{2, 4 * time.Second},
{10, 30 * time.Second}, // should be capped at MaxDelay
}
for _, tt := range tests {
t.Run(fmt.Sprintf("attempt_%d", tt.attempt), func(t *testing.T) {
if got := config.CalculateDelay(tt.attempt); got != tt.expected {
t.Errorf("CalculateDelay(%d) = %v, want %v", tt.attempt, got, tt.expected)
}
})
}
}
func TestExecuteWithRetry(t *testing.T) {
config := DefaultRetryConfig()
config.InitialDelay = time.Millisecond // 加快测试速度
t.Run("success on first try", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return nil
}
err := ExecuteWithRetry(operation, config)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if attempts != 1 {
t.Errorf("Expected 1 attempt, got %d", attempts)
}
})
t.Run("success after retries", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
if attempts < 3 {
return NewUpdaterError(NetworkError, "temporary failure", nil)
}
return nil
}
err := ExecuteWithRetry(operation, config)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if attempts != 3 {
t.Errorf("Expected 3 attempts, got %d", attempts)
}
})
t.Run("non-retryable error", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return NewUpdaterError(FileError, "file not found", nil)
}
err := ExecuteWithRetry(operation, config)
if err == nil {
t.Error("Expected error, got nil")
}
if attempts != 1 {
t.Errorf("Expected 1 attempt, got %d", attempts)
}
})
t.Run("max retries exceeded", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return NewUpdaterError(NetworkError, "persistent failure", nil)
}
err := ExecuteWithRetry(operation, config)
if err == nil {
t.Error("Expected error, got nil")
}
expectedAttempts := config.MaxRetries + 1
if attempts != expectedAttempts {
t.Errorf("Expected %d attempts, got %d", expectedAttempts, attempts)
}
})
}
func TestDefaultErrorHandler(t *testing.T) {
handler := NewDefaultErrorHandler()
t.Run("handle updater error", func(t *testing.T) {
originalErr := NewUpdaterError(NetworkError, "test", nil)
handledErr := handler.HandleError(originalErr)
if handledErr != originalErr {
t.Error("Expected same error instance")
}
if originalErr.Context["handled_at"] == nil {
t.Error("Expected handled_at context to be set")
}
})
t.Run("handle regular error", func(t *testing.T) {
originalErr := fmt.Errorf("regular error")
handledErr := handler.HandleError(originalErr)
if ue, ok := handledErr.(*UpdaterError); ok {
if ue.Type != NetworkError {
t.Errorf("Expected NetworkError, got %v", ue.Type)
}
if ue.Cause != originalErr {
t.Error("Expected original error as cause")
}
} else {
t.Error("Expected UpdaterError")
}
})
t.Run("should retry", func(t *testing.T) {
retryableErr := NewUpdaterError(NetworkError, "test", nil)
nonRetryableErr := NewUpdaterError(FileError, "test", nil)
if !handler.ShouldRetry(retryableErr) {
t.Error("Expected network error to be retryable")
}
if handler.ShouldRetry(nonRetryableErr) {
t.Error("Expected file error to not be retryable")
}
})
t.Run("get user message", func(t *testing.T) {
updaterErr := NewUpdaterError(NetworkError, "test", nil)
regularErr := fmt.Errorf("regular error")
userMsg1 := handler.GetUserMessage(updaterErr)
userMsg2 := handler.GetUserMessage(regularErr)
if userMsg1 != "网络连接失败,请检查网络连接后重试" {
t.Errorf("Unexpected user message: %s", userMsg1)
}
if userMsg2 != "发生未知错误,请联系技术支持" {
t.Errorf("Unexpected user message: %s", userMsg2)
}
})
}

42
Go_Updater/go.mod Normal file
View File

@@ -0,0 +1,42 @@
module AUTO_MAA_Go_Updater
go 1.24.5
require (
fyne.io/fyne/v2 v2.6.1
gopkg.in/yaml.v3 v3.0.1
)
require (
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fyne-io/gl-js v0.1.0 // indirect
github.com/fyne-io/glfw-js v0.2.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.1 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

80
Go_Updater/go.sum Normal file
View File

@@ -0,0 +1,80 @@
fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is=
fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc=
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

513
Go_Updater/gui/manager.go Normal file
View File

@@ -0,0 +1,513 @@
package gui
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// UpdateStatus 表示更新过程的当前状态
type UpdateStatus int
const (
StatusChecking UpdateStatus = iota
StatusUpdateAvailable
StatusDownloading
StatusInstalling
StatusCompleted
StatusError
)
// Config 表示 GUI 的配置结构
type Config struct {
ResourceID string
CurrentVersion string
UserAgent string
BackupURL string
}
// GUIManager 定义 GUI 管理的接口方法
type GUIManager interface {
ShowMainWindow()
UpdateStatus(status UpdateStatus, message string)
ShowProgress(percentage float64)
ShowError(errorMsg string)
ShowConfigDialog() (*Config, error)
Close()
}
// Manager 实现 GUIManager 接口
type Manager struct {
app fyne.App
window fyne.Window
statusLabel *widget.Label
progressBar *widget.ProgressBar
actionButton *widget.Button
versionLabel *widget.Label
releaseNotes *widget.RichText
currentStatus UpdateStatus
onCheckUpdate func()
onCancel func()
}
// NewManager creates a new GUI manager instance
func NewManager() *Manager {
a := app.New()
a.SetIcon(theme.ComputerIcon())
w := a.NewWindow("AUTO_MAA_Go_Updater")
w.Resize(fyne.NewSize(500, 400))
w.SetFixedSize(false)
w.CenterOnScreen()
return &Manager{
app: a,
window: w,
}
}
// SetCallbacks sets the callback functions for user actions
func (m *Manager) SetCallbacks(onCheckUpdate, onCancel func()) {
m.onCheckUpdate = onCheckUpdate
m.onCancel = onCancel
}
// ShowMainWindow displays the main application window
func (m *Manager) ShowMainWindow() {
// Create UI components
m.createUIComponents()
// Create main layout
content := m.createMainLayout()
m.window.SetContent(content)
m.window.ShowAndRun()
}
// createUIComponents initializes all UI components
func (m *Manager) createUIComponents() {
// Status label
m.statusLabel = widget.NewLabel("准备检查更新...")
m.statusLabel.Alignment = fyne.TextAlignCenter
// Progress bar
m.progressBar = widget.NewProgressBar()
m.progressBar.Hide()
// Version label
m.versionLabel = widget.NewLabel("当前版本: 未知")
m.versionLabel.TextStyle = fyne.TextStyle{Italic: true}
// Release notes
m.releaseNotes = widget.NewRichText()
m.releaseNotes.Hide()
// Action button
m.actionButton = widget.NewButton("检查更新", func() {
if m.onCheckUpdate != nil {
m.onCheckUpdate()
}
})
m.actionButton.Importance = widget.HighImportance
}
// createMainLayout creates the main window layout
func (m *Manager) createMainLayout() *fyne.Container {
// Header section
header := container.NewVBox(
widget.NewCard("", "", container.NewVBox(
widget.NewLabelWithStyle("AUTO_MAA_Go_Updater", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
m.versionLabel,
)),
)
// Status section
statusSection := container.NewVBox(
m.statusLabel,
m.progressBar,
)
// Release notes section
releaseNotesCard := widget.NewCard("更新日志", "", container.NewScroll(m.releaseNotes))
releaseNotesCard.Hide()
// Button section
buttonSection := container.NewHBox(
widget.NewButton("配置", func() {
m.showConfigDialog()
}),
widget.NewSpacer(),
m.actionButton,
)
// Main layout
return container.NewVBox(
header,
widget.NewSeparator(),
statusSection,
releaseNotesCard,
widget.NewSeparator(),
buttonSection,
)
}
// UpdateStatus updates the current status and UI accordingly
func (m *Manager) UpdateStatus(status UpdateStatus, message string) {
m.currentStatus = status
m.statusLabel.SetText(message)
switch status {
case StatusChecking:
m.actionButton.SetText("检查中...")
m.actionButton.Disable()
m.progressBar.Hide()
case StatusUpdateAvailable:
m.actionButton.SetText("开始更新")
m.actionButton.Enable()
m.progressBar.Hide()
case StatusDownloading:
m.actionButton.SetText("下载中...")
m.actionButton.Disable()
m.progressBar.Show()
case StatusInstalling:
m.actionButton.SetText("安装中...")
m.actionButton.Disable()
m.progressBar.Show()
case StatusCompleted:
m.actionButton.SetText("完成")
m.actionButton.Enable()
m.progressBar.Hide()
case StatusError:
m.actionButton.SetText("重试")
m.actionButton.Enable()
m.progressBar.Hide()
}
}
// ShowProgress updates the progress bar
func (m *Manager) ShowProgress(percentage float64) {
if percentage < 0 {
percentage = 0
}
if percentage > 100 {
percentage = 100
}
m.progressBar.SetValue(percentage / 100.0)
m.progressBar.Show()
}
// ShowError displays an error dialog
func (m *Manager) ShowError(errorMsg string) {
dialog.ShowError(fmt.Errorf(errorMsg), m.window)
}
// ShowConfigDialog displays the configuration dialog
func (m *Manager) ShowConfigDialog() (*Config, error) {
return m.showConfigDialog()
}
// showConfigDialog creates and shows the configuration dialog
func (m *Manager) showConfigDialog() (*Config, error) {
// Create form entries
resourceIDEntry := widget.NewEntry()
resourceIDEntry.SetPlaceHolder("例如: M9A")
versionEntry := widget.NewEntry()
versionEntry.SetPlaceHolder("例如: v1.0.0")
userAgentEntry := widget.NewEntry()
userAgentEntry.SetText("AUTO_MAA_Go_Updater/1.0")
backupURLEntry := widget.NewEntry()
backupURLEntry.SetPlaceHolder("备用下载地址(可选)")
// Create form
form := &widget.Form{
Items: []*widget.FormItem{
{Text: "资源ID:", Widget: resourceIDEntry},
{Text: "当前版本:", Widget: versionEntry},
{Text: "用户代理:", Widget: userAgentEntry},
{Text: "备用下载地址:", Widget: backupURLEntry},
},
}
// Create result channel
resultChan := make(chan *Config, 1)
errorChan := make(chan error, 1)
// Create dialog
configDialog := dialog.NewCustomConfirm(
"配置设置",
"保存",
"取消",
form,
func(confirmed bool) {
if confirmed {
config := &Config{
ResourceID: resourceIDEntry.Text,
CurrentVersion: versionEntry.Text,
UserAgent: userAgentEntry.Text,
BackupURL: backupURLEntry.Text,
}
// Basic validation
if config.ResourceID == "" {
errorChan <- fmt.Errorf("资源ID不能为空")
return
}
if config.CurrentVersion == "" {
errorChan <- fmt.Errorf("当前版本不能为空")
return
}
resultChan <- config
} else {
errorChan <- fmt.Errorf("用户取消了配置")
}
},
m.window,
)
// Add help text
helpText := widget.NewRichTextFromMarkdown(`
**配置说明:**
- **资源ID**: Mirror酱服务中的资源标识符
- **当前版本**: 当前软件的版本号
- **用户代理**: HTTP请求的用户代理字符串
- **备用下载地址**: 当Mirror酱服务不可用时的备用下载地址
`)
// Create container with help text
dialogContent := container.NewVBox(
form,
widget.NewSeparator(),
helpText,
)
configDialog.SetContent(dialogContent)
configDialog.Resize(fyne.NewSize(600, 500))
configDialog.Show()
// Wait for result
select {
case config := <-resultChan:
return config, nil
case err := <-errorChan:
return nil, err
}
}
// SetVersionInfo updates the version display
func (m *Manager) SetVersionInfo(version string) {
m.versionLabel.SetText(fmt.Sprintf("当前版本: %s", version))
}
// ShowReleaseNotes displays the release notes
func (m *Manager) ShowReleaseNotes(notes string) {
if notes != "" {
m.releaseNotes.ParseMarkdown(notes)
// Find the release notes card and show it
if parent := m.window.Content().(*container.VBox); parent != nil {
for _, obj := range parent.Objects {
if card, ok := obj.(*widget.Card); ok && card.Title == "更新日志" {
card.Show()
break
}
}
}
}
}
// UpdateStatusWithDetails updates status with detailed information
func (m *Manager) UpdateStatusWithDetails(status UpdateStatus, message string, details map[string]string) {
m.UpdateStatus(status, message)
// Update version info if provided
if version, ok := details["version"]; ok {
m.SetVersionInfo(version)
}
// Show release notes if provided
if notes, ok := details["release_notes"]; ok {
m.ShowReleaseNotes(notes)
}
// Update progress if provided
if progress, ok := details["progress"]; ok {
if p, err := fmt.Sscanf(progress, "%f", new(float64)); err == nil && p == 1 {
var progressValue float64
fmt.Sscanf(progress, "%f", &progressValue)
m.ShowProgress(progressValue)
}
}
}
// ShowProgressWithSpeed shows progress with download speed information
func (m *Manager) ShowProgressWithSpeed(percentage float64, speed int64, eta string) {
m.ShowProgress(percentage)
// Update status with speed and ETA information
speedText := m.formatSpeed(speed)
statusText := fmt.Sprintf("下载中... %.1f%% (%s)", percentage, speedText)
if eta != "" {
statusText += fmt.Sprintf(" - 剩余时间: %s", eta)
}
m.statusLabel.SetText(statusText)
}
// formatSpeed formats the download speed for display
func (m *Manager) formatSpeed(bytesPerSecond int64) string {
if bytesPerSecond < 1024 {
return fmt.Sprintf("%d B/s", bytesPerSecond)
} else if bytesPerSecond < 1024*1024 {
return fmt.Sprintf("%.1f KB/s", float64(bytesPerSecond)/1024)
} else {
return fmt.Sprintf("%.1f MB/s", float64(bytesPerSecond)/(1024*1024))
}
}
// ShowConfirmDialog shows a confirmation dialog
func (m *Manager) ShowConfirmDialog(title, message string, callback func(bool)) {
dialog.ShowConfirm(title, message, callback, m.window)
}
// ShowInfoDialog shows an information dialog
func (m *Manager) ShowInfoDialog(title, message string) {
dialog.ShowInformation(title, message, m.window)
}
// ShowUpdateAvailableDialog shows a dialog when update is available
func (m *Manager) ShowUpdateAvailableDialog(currentVersion, newVersion, releaseNotes string, onConfirm func()) {
content := container.NewVBox(
widget.NewLabel(fmt.Sprintf("发现新版本: %s", newVersion)),
widget.NewLabel(fmt.Sprintf("当前版本: %s", currentVersion)),
widget.NewSeparator(),
)
if releaseNotes != "" {
notesWidget := widget.NewRichText()
notesWidget.ParseMarkdown(releaseNotes)
notesScroll := container.NewScroll(notesWidget)
notesScroll.SetMinSize(fyne.NewSize(400, 200))
content.Add(widget.NewLabel("更新内容:"))
content.Add(notesScroll)
}
dialog.ShowCustomConfirm(
"发现新版本",
"立即更新",
"稍后提醒",
content,
func(confirmed bool) {
if confirmed && onConfirm != nil {
onConfirm()
}
},
m.window,
)
}
// SetActionButtonCallback sets the callback for the main action button
func (m *Manager) SetActionButtonCallback(callback func()) {
if m.actionButton != nil {
m.actionButton.OnTapped = callback
}
}
// EnableActionButton enables or disables the action button
func (m *Manager) EnableActionButton(enabled bool) {
if m.actionButton != nil {
if enabled {
m.actionButton.Enable()
} else {
m.actionButton.Disable()
}
}
}
// SetActionButtonText sets the text of the action button
func (m *Manager) SetActionButtonText(text string) {
if m.actionButton != nil {
m.actionButton.SetText(text)
}
}
// ShowErrorWithRetry shows an error with retry option
func (m *Manager) ShowErrorWithRetry(errorMsg string, onRetry func()) {
dialog.ShowCustomConfirm(
"错误",
"重试",
"取消",
widget.NewLabel(errorMsg),
func(retry bool) {
if retry && onRetry != nil {
onRetry()
}
},
m.window,
)
}
// UpdateProgressBar updates the progress bar with custom styling
func (m *Manager) UpdateProgressBar(percentage float64, color string) {
m.ShowProgress(percentage)
// Note: Fyne doesn't support custom colors easily, but we keep the interface for future enhancement
}
// HideProgressBar hides the progress bar
func (m *Manager) HideProgressBar() {
if m.progressBar != nil {
m.progressBar.Hide()
}
}
// ShowProgressBar shows the progress bar
func (m *Manager) ShowProgressBar() {
if m.progressBar != nil {
m.progressBar.Show()
}
}
// SetWindowTitle sets the window title
func (m *Manager) SetWindowTitle(title string) {
if m.window != nil {
m.window.SetTitle(title)
}
}
// GetCurrentStatus returns the current update status
func (m *Manager) GetCurrentStatus() UpdateStatus {
return m.currentStatus
}
// IsWindowVisible returns whether the window is currently visible
func (m *Manager) IsWindowVisible() bool {
return m.window != nil && m.window.Content() != nil
}
// RefreshUI refreshes the user interface
func (m *Manager) RefreshUI() {
if m.window != nil && m.window.Content() != nil {
m.window.Content().Refresh()
}
}
// Close closes the application
func (m *Manager) Close() {
if m.window != nil {
m.window.Close()
}
}

View File

@@ -0,0 +1,227 @@
package gui
import (
"testing"
"time"
)
func TestNewManager(t *testing.T) {
manager := NewManager()
if manager == nil {
t.Fatal("NewManager() returned nil")
}
if manager.app == nil {
t.Error("Manager app is nil")
}
if manager.window == nil {
t.Error("Manager window is nil")
}
}
func TestUpdateStatus(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Test different status updates
testCases := []struct {
status UpdateStatus
message string
}{
{StatusChecking, "检查更新中..."},
{StatusUpdateAvailable, "发现新版本"},
{StatusDownloading, "下载中..."},
{StatusInstalling, "安装中..."},
{StatusCompleted, "更新完成"},
{StatusError, "更新失败"},
}
for _, tc := range testCases {
manager.UpdateStatus(tc.status, tc.message)
if manager.GetCurrentStatus() != tc.status {
t.Errorf("Expected status %v, got %v", tc.status, manager.GetCurrentStatus())
}
if manager.statusLabel.Text != tc.message {
t.Errorf("Expected message '%s', got '%s'", tc.message, manager.statusLabel.Text)
}
}
}
func TestShowProgress(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Test progress values
testValues := []float64{0, 25.5, 50, 75.8, 100, 150, -10}
expectedValues := []float64{0, 25.5, 50, 75.8, 100, 100, 0}
for i, value := range testValues {
manager.ShowProgress(value)
expected := expectedValues[i] / 100.0
if manager.progressBar.Value != expected {
t.Errorf("Expected progress %.2f, got %.2f", expected, manager.progressBar.Value)
}
}
}
func TestSetVersionInfo(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
version := "v1.2.3"
manager.SetVersionInfo(version)
expectedText := "当前版本: v1.2.3"
if manager.versionLabel.Text != expectedText {
t.Errorf("Expected version text '%s', got '%s'", expectedText, manager.versionLabel.Text)
}
}
func TestFormatSpeed(t *testing.T) {
manager := NewManager()
testCases := []struct {
speed int64
expected string
}{
{512, "512 B/s"},
{1536, "1.5 KB/s"},
{1048576, "1.0 MB/s"},
{2621440, "2.5 MB/s"},
}
for _, tc := range testCases {
result := manager.formatSpeed(tc.speed)
if result != tc.expected {
t.Errorf("Expected speed format '%s', got '%s'", tc.expected, result)
}
}
}
func TestShowProgressWithSpeed(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
percentage := 45.5
speed := int64(1048576) // 1 MB/s
eta := "2分钟"
manager.ShowProgressWithSpeed(percentage, speed, eta)
expectedProgress := percentage / 100.0
if manager.progressBar.Value != expectedProgress {
t.Errorf("Expected progress %.2f, got %.2f", expectedProgress, manager.progressBar.Value)
}
expectedStatus := "下载中... 45.5% (1.0 MB/s) - 剩余时间: 2分钟"
if manager.statusLabel.Text != expectedStatus {
t.Errorf("Expected status '%s', got '%s'", expectedStatus, manager.statusLabel.Text)
}
}
func TestActionButtonStates(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Test enabling/disabling
manager.EnableActionButton(false)
if !manager.actionButton.Disabled() {
t.Error("Action button should be disabled")
}
manager.EnableActionButton(true)
if manager.actionButton.Disabled() {
t.Error("Action button should be enabled")
}
// Test text setting
testText := "测试按钮"
manager.SetActionButtonText(testText)
if manager.actionButton.Text != testText {
t.Errorf("Expected button text '%s', got '%s'", testText, manager.actionButton.Text)
}
}
func TestProgressBarVisibility(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Initially hidden
if manager.progressBar.Visible() {
t.Error("Progress bar should be initially hidden")
}
// Show progress bar
manager.ShowProgressBar()
if !manager.progressBar.Visible() {
t.Error("Progress bar should be visible after ShowProgressBar()")
}
// Hide progress bar
manager.HideProgressBar()
if manager.progressBar.Visible() {
t.Error("Progress bar should be hidden after HideProgressBar()")
}
}
func TestSetCallbacks(t *testing.T) {
manager := NewManager()
checkUpdateCalled := false
cancelCalled := false
onCheckUpdate := func() {
checkUpdateCalled = true
}
onCancel := func() {
cancelCalled = true
}
manager.SetCallbacks(onCheckUpdate, onCancel)
// Verify callbacks are set
if manager.onCheckUpdate == nil {
t.Error("onCheckUpdate callback not set")
}
if manager.onCancel == nil {
t.Error("onCancel callback not set")
}
// Test callback execution
manager.onCheckUpdate()
if !checkUpdateCalled {
t.Error("onCheckUpdate callback was not called")
}
manager.onCancel()
if !cancelCalled {
t.Error("onCancel callback was not called")
}
}
// Benchmark tests for performance
func BenchmarkUpdateStatus(b *testing.B) {
manager := NewManager()
manager.createUIComponents()
b.ResetTimer()
for i := 0; i < b.N; i++ {
manager.UpdateStatus(StatusDownloading, "下载中...")
}
}
func BenchmarkShowProgress(b *testing.B) {
manager := NewManager()
manager.createUIComponents()
b.ResetTimer()
for i := 0; i < b.N; i++ {
manager.ShowProgress(float64(i % 100))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,474 @@
package install
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
)
// ChangesInfo 表示 changes.json 文件的结构
type ChangesInfo struct {
Deleted []string `json:"deleted"`
Added []string `json:"added"`
Modified []string `json:"modified"`
}
// InstallManager 定义安装操作的接口契约
type InstallManager interface {
ExtractZip(zipPath, destPath string) error
ProcessChanges(changesPath string) (*ChangesInfo, error)
ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error
HandleRunningProcess(processName string) error
CreateTempDir() (string, error)
CleanupTempDir(tempDir string) error
}
// Manager 实现 InstallManager 接口
type Manager struct {
tempDirs []string // 跟踪临时目录以便清理
}
// NewManager 创建新的安装管理器实例
func NewManager() *Manager {
return &Manager{
tempDirs: make([]string, 0),
}
}
// CreateTempDir 为解压创建临时目录
func (m *Manager) CreateTempDir() (string, error) {
tempDir, err := os.MkdirTemp("", "updater_*")
if err != nil {
return "", fmt.Errorf("创建临时目录失败: %w", err)
}
// 跟踪临时目录以便清理
m.tempDirs = append(m.tempDirs, tempDir)
return tempDir, nil
}
// CleanupTempDir 删除临时目录及其内容
func (m *Manager) CleanupTempDir(tempDir string) error {
if tempDir == "" {
return nil
}
err := os.RemoveAll(tempDir)
if err != nil {
return fmt.Errorf("清理临时目录 %s 失败: %w", tempDir, err)
}
// 从跟踪列表中删除
for i, dir := range m.tempDirs {
if dir == tempDir {
m.tempDirs = append(m.tempDirs[:i], m.tempDirs[i+1:]...)
break
}
}
return nil
}
// CleanupAllTempDirs 删除所有跟踪的临时目录
func (m *Manager) CleanupAllTempDirs() error {
var errors []string
for _, tempDir := range m.tempDirs {
if err := os.RemoveAll(tempDir); err != nil {
errors = append(errors, fmt.Sprintf("清理 %s 失败: %v", tempDir, err))
}
}
m.tempDirs = m.tempDirs[:0] // 清空切片
if len(errors) > 0 {
return fmt.Errorf("清理错误: %s", strings.Join(errors, "; "))
}
return nil
}
// ExtractZip 将 ZIP 文件解压到指定的目标目录
func (m *Manager) ExtractZip(zipPath, destPath string) error {
// 打开 ZIP 文件进行读取
reader, err := zip.OpenReader(zipPath)
if err != nil {
return fmt.Errorf("打开 ZIP 文件 %s 失败: %w", zipPath, err)
}
defer reader.Close()
// 如果目标目录不存在则创建
if err := os.MkdirAll(destPath, 0755); err != nil {
return fmt.Errorf("创建目标目录 %s 失败: %w", destPath, err)
}
// 解压文件
for _, file := range reader.File {
if err := m.extractFile(file, destPath); err != nil {
return fmt.Errorf("解压文件 %s 失败: %w", file.Name, err)
}
}
return nil
}
// extractFile 从 ZIP 归档中解压单个文件
func (m *Manager) extractFile(file *zip.File, destPath string) error {
// 清理文件路径以防止目录遍历攻击
cleanPath := filepath.Clean(file.Name)
if strings.Contains(cleanPath, "..") {
return fmt.Errorf("无效的文件路径: %s", file.Name)
}
// 创建完整的目标路径
destFile := filepath.Join(destPath, cleanPath)
// 如果需要则创建目录结构
if file.FileInfo().IsDir() {
return os.MkdirAll(destFile, file.FileInfo().Mode())
}
// 创建父目录
if err := os.MkdirAll(filepath.Dir(destFile), 0755); err != nil {
return fmt.Errorf("创建父目录失败: %w", err)
}
// 打开 ZIP 归档中的文件
rc, err := file.Open()
if err != nil {
return fmt.Errorf("打开归档中的文件失败: %w", err)
}
defer rc.Close()
// 创建目标文件
outFile, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
if err != nil {
return fmt.Errorf("创建目标文件失败: %w", err)
}
defer outFile.Close()
// 复制文件内容
_, err = io.Copy(outFile, rc)
if err != nil {
return fmt.Errorf("复制文件内容失败: %w", err)
}
return nil
}
// ProcessChanges 读取并解析 changes.json 文件
func (m *Manager) ProcessChanges(changesPath string) (*ChangesInfo, error) {
// 检查 changes.json 是否存在
if _, err := os.Stat(changesPath); os.IsNotExist(err) {
// 如果 changes.json 不存在,返回空的变更信息
return &ChangesInfo{
Deleted: []string{},
Added: []string{},
Modified: []string{},
}, nil
}
// 读取 changes.json 文件
data, err := os.ReadFile(changesPath)
if err != nil {
return nil, fmt.Errorf("读取变更文件 %s 失败: %w", changesPath, err)
}
// 解析 JSON
var changes ChangesInfo
if err := json.Unmarshal(data, &changes); err != nil {
return nil, fmt.Errorf("解析变更 JSON 失败: %w", err)
}
return &changes, nil
}
// HandleRunningProcess 通过重命名正在使用的文件来处理正在运行的进程
func (m *Manager) HandleRunningProcess(processName string) error {
// 获取当前可执行文件路径
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("获取可执行文件路径失败: %w", err)
}
exeDir := filepath.Dir(exePath)
targetFile := filepath.Join(exeDir, processName)
// 检查目标文件是否存在
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
// 文件不存在,无需处理
return nil
}
// 尝试重命名文件以指示应在下次启动时删除
oldFile := targetFile + ".old"
// 如果存在现有的 .old 文件则删除
if _, err := os.Stat(oldFile); err == nil {
if err := os.Remove(oldFile); err != nil {
return fmt.Errorf("删除现有旧文件 %s 失败: %w", oldFile, err)
}
}
// 将当前文件重命名为 .old
if err := os.Rename(targetFile, oldFile); err != nil {
// 如果重命名失败,进程可能正在运行
// 在 Windows 上,我们无法重命名正在运行的可执行文件
if isFileInUse(err) {
// 标记文件在下次重启时删除Windows 特定)
return m.markFileForDeletion(targetFile)
}
return fmt.Errorf("重命名正在运行的进程文件 %s 失败: %w", targetFile, err)
}
return nil
}
// isFileInUse 检查错误是否表示文件正在使用中
func isFileInUse(err error) bool {
if err == nil {
return false
}
// 检查 Windows 特定的"文件正在使用"错误
if pathErr, ok := err.(*os.PathError); ok {
if errno, ok := pathErr.Err.(syscall.Errno); ok {
// ERROR_SHARING_VIOLATION (32) 或 ERROR_ACCESS_DENIED (5)
return errno == syscall.Errno(32) || errno == syscall.Errno(5)
}
}
return strings.Contains(err.Error(), "being used by another process") ||
strings.Contains(err.Error(), "access is denied")
}
// markFileForDeletion 标记文件在下次系统重启时删除Windows 特定)
func (m *Manager) markFileForDeletion(filePath string) error {
// 这是 Windows 特定的实现
// 目前,我们将创建一个可由主应用程序处理的标记文件
markerFile := filePath + ".delete_on_restart"
// 创建标记文件
file, err := os.Create(markerFile)
if err != nil {
return fmt.Errorf("创建删除标记文件失败: %w", err)
}
defer file.Close()
// 将目标文件路径写入标记文件
_, err = file.WriteString(filePath)
if err != nil {
return fmt.Errorf("写入标记文件失败: %w", err)
}
return nil
}
// DeleteMarkedFiles 删除标记为删除的文件
func (m *Manager) DeleteMarkedFiles(directory string) error {
// 查找所有 .delete_on_restart 文件
pattern := filepath.Join(directory, "*.delete_on_restart")
matches, err := filepath.Glob(pattern)
if err != nil {
return fmt.Errorf("查找标记文件失败: %w", err)
}
var errors []string
for _, markerFile := range matches {
// 读取目标文件路径
data, err := os.ReadFile(markerFile)
if err != nil {
errors = append(errors, fmt.Sprintf("读取标记文件 %s 失败: %v", markerFile, err))
continue
}
targetFile := strings.TrimSpace(string(data))
// 尝试删除目标文件
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
errors = append(errors, fmt.Sprintf("删除标记文件 %s 失败: %v", targetFile, err))
}
// 删除标记文件
if err := os.Remove(markerFile); err != nil {
errors = append(errors, fmt.Sprintf("删除标记文件 %s 失败: %v", markerFile, err))
}
}
if len(errors) > 0 {
return fmt.Errorf("删除错误: %s", strings.Join(errors, "; "))
}
return nil
}
// ApplyUpdate 通过从源目录复制文件到目标目录来应用更新
func (m *Manager) ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error {
// 创建备份目录
backupDir, err := m.createBackupDir(targetPath)
if err != nil {
return fmt.Errorf("创建备份目录失败: %w", err)
}
// 在应用更新前备份现有文件
if err := m.backupFiles(targetPath, backupDir, changes); err != nil {
return fmt.Errorf("备份文件失败: %w", err)
}
// 应用更新
if err := m.applyUpdateFiles(sourcePath, targetPath, changes); err != nil {
// 失败时回滚
if rollbackErr := m.rollbackUpdate(targetPath, backupDir); rollbackErr != nil {
return fmt.Errorf("更新失败且回滚失败: 更新错误: %w, 回滚错误: %v", err, rollbackErr)
}
return fmt.Errorf("更新失败已回滚: %w", err)
}
// 成功更新后清理备份目录
if err := os.RemoveAll(backupDir); err != nil {
// 记录警告但不让更新失败
fmt.Printf("警告: 清理备份目录 %s 失败: %v\n", backupDir, err)
}
return nil
}
// createBackupDir 为更新创建备份目录
func (m *Manager) createBackupDir(targetPath string) (string, error) {
backupDir := filepath.Join(targetPath, ".backup_"+fmt.Sprintf("%d", os.Getpid()))
if err := os.MkdirAll(backupDir, 0755); err != nil {
return "", fmt.Errorf("创建备份目录失败: %w", err)
}
return backupDir, nil
}
// backupFiles 创建将被修改或删除的文件的备份
func (m *Manager) backupFiles(targetPath, backupDir string, changes *ChangesInfo) error {
// 备份将被修改的文件
for _, file := range changes.Modified {
srcFile := filepath.Join(targetPath, file)
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
continue // 文件不存在,跳过备份
}
backupFile := filepath.Join(backupDir, file)
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
return fmt.Errorf("备份修改文件 %s 失败: %w", file, err)
}
}
// 备份将被删除的文件
for _, file := range changes.Deleted {
srcFile := filepath.Join(targetPath, file)
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
continue // 文件不存在,跳过备份
}
backupFile := filepath.Join(backupDir, file)
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
return fmt.Errorf("备份删除文件 %s 失败: %w", file, err)
}
}
return nil
}
// applyUpdateFiles 应用实际的文件更改
func (m *Manager) applyUpdateFiles(sourcePath, targetPath string, changes *ChangesInfo) error {
// 删除标记为删除的文件
for _, file := range changes.Deleted {
targetFile := filepath.Join(targetPath, file)
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("删除文件 %s 失败: %w", file, err)
}
}
// 复制新文件和修改的文件
filesToCopy := append(changes.Added, changes.Modified...)
for _, file := range filesToCopy {
srcFile := filepath.Join(sourcePath, file)
targetFile := filepath.Join(targetPath, file)
// 检查源文件是否存在
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
continue // 源文件不存在,跳过
}
if err := m.copyFileWithDirs(srcFile, targetFile); err != nil {
return fmt.Errorf("复制文件 %s 失败: %w", file, err)
}
}
return nil
}
// copyFileWithDirs 复制文件并创建必要的目录
func (m *Manager) copyFileWithDirs(src, dst string) error {
// 创建父目录
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return fmt.Errorf("创建父目录失败: %w", err)
}
// 打开源文件
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("打开源文件失败: %w", err)
}
defer srcFile.Close()
// 获取源文件信息
srcInfo, err := srcFile.Stat()
if err != nil {
return fmt.Errorf("获取源文件信息失败: %w", err)
}
// 创建目标文件
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
if err != nil {
return fmt.Errorf("创建目标文件失败: %w", err)
}
defer dstFile.Close()
// 复制文件内容
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return fmt.Errorf("复制文件内容失败: %w", err)
}
return nil
}
// rollbackUpdate 在更新失败时从备份恢复文件
func (m *Manager) rollbackUpdate(targetPath, backupDir string) error {
// 遍历备份目录并恢复文件
return filepath.Walk(backupDir, func(backupFile string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil // 跳过目录
}
// 计算相对路径
relPath, err := filepath.Rel(backupDir, backupFile)
if err != nil {
return fmt.Errorf("计算相对路径失败: %w", err)
}
// 将文件恢复到目标位置
targetFile := filepath.Join(targetPath, relPath)
if err := m.copyFileWithDirs(backupFile, targetFile); err != nil {
return fmt.Errorf("恢复文件 %s 失败: %w", relPath, err)
}
return nil
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
package main
import (
"testing"
)
// 集成测试将在此处实现
// 此文件目前是占位符
func TestIntegrationPlaceholder(t *testing.T) {
t.Skip("集成测试尚未实现")
}

438
Go_Updater/logger/logger.go Normal file
View File

@@ -0,0 +1,438 @@
package logger
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// LogLevel 日志级别
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
)
// String 返回日志级别的字符串表示
func (l LogLevel) String() string {
switch l {
case DEBUG:
return "DEBUG"
case INFO:
return "INFO"
case WARN:
return "WARN"
case ERROR:
return "ERROR"
default:
return "UNKNOWN"
}
}
// Logger 日志记录器接口
type Logger interface {
Debug(msg string, fields ...interface{})
Info(msg string, fields ...interface{})
Warn(msg string, fields ...interface{})
Error(msg string, fields ...interface{})
SetLevel(level LogLevel)
Close() error
}
// FileLogger 文件日志记录器
type FileLogger struct {
mu sync.RWMutex
file *os.File
logger *log.Logger
level LogLevel
maxSize int64 // 最大文件大小(字节)
maxBackups int // 最大备份文件数
logDir string // 日志目录
filename string // 日志文件名
currentSize int64 // 当前文件大小
}
// LoggerConfig 日志配置
type LoggerConfig struct {
Level LogLevel
MaxSize int64 // 最大文件大小字节默认10MB
MaxBackups int // 最大备份文件数默认5
LogDir string // 日志目录
Filename string // 日志文件名
}
// DefaultLoggerConfig 默认日志配置
func DefaultLoggerConfig() *LoggerConfig {
// 获取当前可执行文件目录
exePath, err := os.Executable()
var logDir string
if err != nil {
logDir = "debug"
} else {
exeDir := filepath.Dir(exePath)
logDir = filepath.Join(exeDir, "debug")
}
return &LoggerConfig{
Level: INFO,
MaxSize: 10 * 1024 * 1024, // 10MB
MaxBackups: 5,
LogDir: logDir,
Filename: "AUTO_MAA_Go_Updater.log",
}
}
// NewFileLogger 创建新的文件日志记录器
func NewFileLogger(config *LoggerConfig) (*FileLogger, error) {
if config == nil {
config = DefaultLoggerConfig()
}
// 创建日志目录
if err := os.MkdirAll(config.LogDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
logPath := filepath.Join(config.LogDir, config.Filename)
// 打开或创建日志文件
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
// 获取当前文件大小
stat, err := file.Stat()
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to get file stats: %w", err)
}
logger := &FileLogger{
file: file,
logger: log.New(file, "", 0), // 我们自己处理格式
level: config.Level,
maxSize: config.MaxSize,
maxBackups: config.MaxBackups,
logDir: config.LogDir,
filename: config.Filename,
currentSize: stat.Size(),
}
return logger, nil
}
// formatMessage 格式化日志消息
func (fl *FileLogger) formatMessage(level LogLevel, msg string, fields ...interface{}) string {
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
if len(fields) > 0 {
msg = fmt.Sprintf(msg, fields...)
}
return fmt.Sprintf("[%s] %s %s\n", timestamp, level.String(), msg)
}
// writeLog 写入日志
func (fl *FileLogger) writeLog(level LogLevel, msg string, fields ...interface{}) {
fl.mu.Lock()
defer fl.mu.Unlock()
// 检查日志级别
if level < fl.level {
return
}
formattedMsg := fl.formatMessage(level, msg, fields...)
// 检查是否需要轮转
if fl.currentSize+int64(len(formattedMsg)) > fl.maxSize {
if err := fl.rotate(); err != nil {
// 轮转失败尝试写入stderr
fmt.Fprintf(os.Stderr, "Failed to rotate log: %v\n", err)
}
}
// 写入日志
n, err := fl.file.WriteString(formattedMsg)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write log: %v\n", err)
return
}
fl.currentSize += int64(n)
fl.file.Sync() // 确保写入磁盘
}
// rotate 轮转日志文件
func (fl *FileLogger) rotate() error {
// 关闭当前文件
if err := fl.file.Close(); err != nil {
return fmt.Errorf("failed to close current log file: %w", err)
}
// 轮转备份文件
if err := fl.rotateBackups(); err != nil {
return fmt.Errorf("failed to rotate backups: %w", err)
}
// 创建新的日志文件
logPath := filepath.Join(fl.logDir, fl.filename)
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to create new log file: %w", err)
}
fl.file = file
fl.logger.SetOutput(file)
fl.currentSize = 0
return nil
}
// rotateBackups 轮转备份文件
func (fl *FileLogger) rotateBackups() error {
basePath := filepath.Join(fl.logDir, fl.filename)
// 删除最老的备份文件
if fl.maxBackups > 0 {
oldestBackup := fmt.Sprintf("%s.%d", basePath, fl.maxBackups)
os.Remove(oldestBackup) // 忽略错误,文件可能不存在
}
// 重命名现有备份文件
for i := fl.maxBackups - 1; i > 0; i-- {
oldName := fmt.Sprintf("%s.%d", basePath, i)
newName := fmt.Sprintf("%s.%d", basePath, i+1)
os.Rename(oldName, newName) // 忽略错误,文件可能不存在
}
// 将当前日志文件重命名为第一个备份
if fl.maxBackups > 0 {
backupName := fmt.Sprintf("%s.1", basePath)
return os.Rename(basePath, backupName)
}
return nil
}
// Debug 记录调试级别日志
func (fl *FileLogger) Debug(msg string, fields ...interface{}) {
fl.writeLog(DEBUG, msg, fields...)
}
// Info 记录信息级别日志
func (fl *FileLogger) Info(msg string, fields ...interface{}) {
fl.writeLog(INFO, msg, fields...)
}
// Warn 记录警告级别日志
func (fl *FileLogger) Warn(msg string, fields ...interface{}) {
fl.writeLog(WARN, msg, fields...)
}
// Error 记录错误级别日志
func (fl *FileLogger) Error(msg string, fields ...interface{}) {
fl.writeLog(ERROR, msg, fields...)
}
// SetLevel 设置日志级别
func (fl *FileLogger) SetLevel(level LogLevel) {
fl.mu.Lock()
defer fl.mu.Unlock()
fl.level = level
}
// Close 关闭日志记录器
func (fl *FileLogger) Close() error {
fl.mu.Lock()
defer fl.mu.Unlock()
if fl.file != nil {
return fl.file.Close()
}
return nil
}
// MultiLogger 多输出日志记录器
type MultiLogger struct {
loggers []Logger
level LogLevel
}
// NewMultiLogger 创建多输出日志记录器
func NewMultiLogger(loggers ...Logger) *MultiLogger {
return &MultiLogger{
loggers: loggers,
level: INFO,
}
}
// Debug 记录调试级别日志
func (ml *MultiLogger) Debug(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Debug(msg, fields...)
}
}
// Info 记录信息级别日志
func (ml *MultiLogger) Info(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Info(msg, fields...)
}
}
// Warn 记录警告级别日志
func (ml *MultiLogger) Warn(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Warn(msg, fields...)
}
}
// Error 记录错误级别日志
func (ml *MultiLogger) Error(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Error(msg, fields...)
}
}
// SetLevel 设置日志级别
func (ml *MultiLogger) SetLevel(level LogLevel) {
ml.level = level
for _, logger := range ml.loggers {
logger.SetLevel(level)
}
}
// Close 关闭所有日志记录器
func (ml *MultiLogger) Close() error {
var lastErr error
for _, logger := range ml.loggers {
if err := logger.Close(); err != nil {
lastErr = err
}
}
return lastErr
}
// ConsoleLogger 控制台日志记录器
type ConsoleLogger struct {
writer io.Writer
level LogLevel
}
// NewConsoleLogger 创建控制台日志记录器
func NewConsoleLogger(writer io.Writer) *ConsoleLogger {
if writer == nil {
writer = os.Stdout
}
return &ConsoleLogger{
writer: writer,
level: INFO,
}
}
// formatMessage 格式化控制台日志消息
func (cl *ConsoleLogger) formatMessage(level LogLevel, msg string, fields ...interface{}) string {
timestamp := time.Now().Format("15:04:05")
if len(fields) > 0 {
msg = fmt.Sprintf(msg, fields...)
}
return fmt.Sprintf("[%s] %s %s\n", timestamp, level.String(), msg)
}
// writeLog 写入控制台日志
func (cl *ConsoleLogger) writeLog(level LogLevel, msg string, fields ...interface{}) {
if level < cl.level {
return
}
formattedMsg := cl.formatMessage(level, msg, fields...)
fmt.Fprint(cl.writer, formattedMsg)
}
// Debug 记录调试级别日志
func (cl *ConsoleLogger) Debug(msg string, fields ...interface{}) {
cl.writeLog(DEBUG, msg, fields...)
}
// Info 记录信息级别日志
func (cl *ConsoleLogger) Info(msg string, fields ...interface{}) {
cl.writeLog(INFO, msg, fields...)
}
// Warn 记录警告级别日志
func (cl *ConsoleLogger) Warn(msg string, fields ...interface{}) {
cl.writeLog(WARN, msg, fields...)
}
// Error 记录错误级别日志
func (cl *ConsoleLogger) Error(msg string, fields ...interface{}) {
cl.writeLog(ERROR, msg, fields...)
}
// SetLevel 设置日志级别
func (cl *ConsoleLogger) SetLevel(level LogLevel) {
cl.level = level
}
// Close 关闭控制台日志记录器(无操作)
func (cl *ConsoleLogger) Close() error {
return nil
}
// 全局日志记录器实例
var (
defaultLogger Logger
once sync.Once
)
// GetDefaultLogger 获取默认日志记录器
func GetDefaultLogger() Logger {
once.Do(func() {
fileLogger, err := NewFileLogger(DefaultLoggerConfig())
if err != nil {
// 如果文件日志创建失败,使用控制台日志
defaultLogger = NewConsoleLogger(os.Stderr)
} else {
// 同时输出到文件和控制台
consoleLogger := NewConsoleLogger(os.Stdout)
defaultLogger = NewMultiLogger(fileLogger, consoleLogger)
}
})
return defaultLogger
}
// 便捷函数
func Debug(msg string, fields ...interface{}) {
GetDefaultLogger().Debug(msg, fields...)
}
func Info(msg string, fields ...interface{}) {
GetDefaultLogger().Info(msg, fields...)
}
func Warn(msg string, fields ...interface{}) {
GetDefaultLogger().Warn(msg, fields...)
}
func Error(msg string, fields ...interface{}) {
GetDefaultLogger().Error(msg, fields...)
}
func SetLevel(level LogLevel) {
GetDefaultLogger().SetLevel(level)
}
func Close() error {
return GetDefaultLogger().Close()
}

View File

@@ -0,0 +1,300 @@
package logger
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestLogLevel_String(t *testing.T) {
tests := []struct {
level LogLevel
expected string
}{
{DEBUG, "DEBUG"},
{INFO, "INFO"},
{WARN, "WARN"},
{ERROR, "ERROR"},
{LogLevel(999), "UNKNOWN"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
if got := tt.level.String(); got != tt.expected {
t.Errorf("LogLevel.String() = %v, want %v", got, tt.expected)
}
})
}
}
func TestDefaultLoggerConfig(t *testing.T) {
config := DefaultLoggerConfig()
if config.Level != INFO {
t.Errorf("Expected default level INFO, got %v", config.Level)
}
if config.MaxSize != 10*1024*1024 {
t.Errorf("Expected default max size 10MB, got %v", config.MaxSize)
}
if config.MaxBackups != 5 {
t.Errorf("Expected default max backups 5, got %v", config.MaxBackups)
}
if config.Filename != "updater.log" {
t.Errorf("Expected default filename 'updater.log', got %v", config.Filename)
}
}
func TestConsoleLogger(t *testing.T) {
var buf bytes.Buffer
logger := NewConsoleLogger(&buf)
t.Run("log levels", func(t *testing.T) {
logger.SetLevel(DEBUG)
logger.Debug("debug message")
logger.Info("info message")
logger.Warn("warn message")
logger.Error("error message")
output := buf.String()
if !strings.Contains(output, "DEBUG debug message") {
t.Error("Expected debug message in output")
}
if !strings.Contains(output, "INFO info message") {
t.Error("Expected info message in output")
}
if !strings.Contains(output, "WARN warn message") {
t.Error("Expected warn message in output")
}
if !strings.Contains(output, "ERROR error message") {
t.Error("Expected error message in output")
}
})
t.Run("log level filtering", func(t *testing.T) {
buf.Reset()
logger.SetLevel(WARN)
logger.Debug("debug message")
logger.Info("info message")
logger.Warn("warn message")
logger.Error("error message")
output := buf.String()
if strings.Contains(output, "DEBUG") {
t.Error("Debug message should be filtered out")
}
if strings.Contains(output, "INFO") {
t.Error("Info message should be filtered out")
}
if !strings.Contains(output, "WARN warn message") {
t.Error("Expected warn message in output")
}
if !strings.Contains(output, "ERROR error message") {
t.Error("Expected error message in output")
}
})
t.Run("formatted messages", func(t *testing.T) {
buf.Reset()
logger.SetLevel(DEBUG)
logger.Info("formatted message: %s %d", "test", 42)
output := buf.String()
if !strings.Contains(output, "formatted message: test 42") {
t.Error("Expected formatted message in output")
}
})
}
func TestFileLogger(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
config := &LoggerConfig{
Level: DEBUG,
MaxSize: 1024, // 1KB for testing rotation
MaxBackups: 3,
LogDir: tempDir,
Filename: "test.log",
}
logger, err := NewFileLogger(config)
if err != nil {
t.Fatalf("Failed to create file logger: %v", err)
}
defer logger.Close()
t.Run("basic logging", func(t *testing.T) {
logger.Info("test message")
logger.Error("error message with %s", "formatting")
// 读取日志文件
logPath := filepath.Join(tempDir, "test.log")
content, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
output := string(content)
if !strings.Contains(output, "INFO test message") {
t.Error("Expected info message in log file")
}
if !strings.Contains(output, "ERROR error message with formatting") {
t.Error("Expected formatted error message in log file")
}
})
t.Run("log rotation", func(t *testing.T) {
// 写入大量数据触发轮转
longMessage := strings.Repeat("a", 200)
for i := 0; i < 10; i++ {
logger.Info("Long message %d: %s", i, longMessage)
}
// 检查是否创建了备份文件
logPath := filepath.Join(tempDir, "test.log")
backupPath := filepath.Join(tempDir, "test.log.1")
if _, err := os.Stat(logPath); os.IsNotExist(err) {
t.Error("Main log file should exist")
}
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
t.Error("Backup log file should exist after rotation")
}
})
}
func TestMultiLogger(t *testing.T) {
var buf1, buf2 bytes.Buffer
logger1 := NewConsoleLogger(&buf1)
logger2 := NewConsoleLogger(&buf2)
multiLogger := NewMultiLogger(logger1, logger2)
multiLogger.SetLevel(INFO)
multiLogger.Info("test message")
multiLogger.Error("error message")
// 检查两个logger都收到了消息
output1 := buf1.String()
output2 := buf2.String()
if !strings.Contains(output1, "INFO test message") {
t.Error("Expected info message in first logger")
}
if !strings.Contains(output1, "ERROR error message") {
t.Error("Expected error message in first logger")
}
if !strings.Contains(output2, "INFO test message") {
t.Error("Expected info message in second logger")
}
if !strings.Contains(output2, "ERROR error message") {
t.Error("Expected error message in second logger")
}
}
func TestFileLoggerRotation(t *testing.T) {
tempDir := t.TempDir()
config := &LoggerConfig{
Level: DEBUG,
MaxSize: 100, // Very small for testing
MaxBackups: 2,
LogDir: tempDir,
Filename: "rotation_test.log",
}
logger, err := NewFileLogger(config)
if err != nil {
t.Fatalf("Failed to create file logger: %v", err)
}
defer logger.Close()
// 写入足够的数据触发多次轮转
for i := 0; i < 20; i++ {
logger.Info("Message %d: %s", i, strings.Repeat("x", 50))
}
// 检查文件存在性
logPath := filepath.Join(tempDir, "rotation_test.log")
backup1Path := filepath.Join(tempDir, "rotation_test.log.1")
backup2Path := filepath.Join(tempDir, "rotation_test.log.2")
backup3Path := filepath.Join(tempDir, "rotation_test.log.3")
if _, err := os.Stat(logPath); os.IsNotExist(err) {
t.Error("Main log file should exist")
}
if _, err := os.Stat(backup1Path); os.IsNotExist(err) {
t.Error("First backup should exist")
}
if _, err := os.Stat(backup2Path); os.IsNotExist(err) {
t.Error("Second backup should exist")
}
// 第三个备份不应该存在MaxBackups=2
if _, err := os.Stat(backup3Path); !os.IsNotExist(err) {
t.Error("Third backup should not exist (exceeds MaxBackups)")
}
}
func TestGlobalLoggerFunctions(t *testing.T) {
// 这个测试比较简单主要确保全局函数不会panic
Debug("debug message")
Info("info message")
Warn("warn message")
Error("error message")
SetLevel(ERROR)
// 这些调用不应该panic
Debug("filtered debug")
Info("filtered info")
Error("visible error")
}
func TestFileLoggerErrorHandling(t *testing.T) {
t.Run("invalid directory", func(t *testing.T) {
// 使用一个真正无效的路径
config := &LoggerConfig{
Level: INFO,
MaxSize: 1024,
MaxBackups: 3,
LogDir: string([]byte{0}), // 无效的路径字符
Filename: "test.log",
}
_, err := NewFileLogger(config)
if err == nil {
t.Error("Expected error when creating logger with invalid directory")
}
})
}
func TestLoggerFormatting(t *testing.T) {
var buf bytes.Buffer
logger := NewConsoleLogger(&buf)
logger.SetLevel(DEBUG)
// 测试时间戳格式
logger.Info("test message")
output := buf.String()
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) == 0 {
t.Fatal("Expected at least one log line")
}
// 检查格式:[HH:MM:SS] LEVEL message
line := lines[0]
if !strings.Contains(line, "INFO test message") {
t.Errorf("Expected 'INFO test message' in output, got: %s", line)
}
// 检查时间戳格式(简单检查)
if !strings.HasPrefix(line, "[") {
t.Error("Expected log line to start with timestamp in brackets")
}
}

1035
Go_Updater/main.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
package version
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"AUTO_MAA_Go_Updater/logger"
)
// VersionInfo 表示来自 version.json 的版本信息
type VersionInfo struct {
MainVersion string `json:"main_version"`
VersionInfo map[string]map[string][]string `json:"version_info"`
}
// ParsedVersion 表示解析后的版本,包含主版本号、次版本号、补丁版本号和测试版本号组件
type ParsedVersion struct {
Major int
Minor int
Patch int
Beta int
}
// VersionManager 处理版本相关操作
type VersionManager struct {
executableDir string
logger logger.Logger
}
// NewVersionManager 创建新的版本管理器
func NewVersionManager() *VersionManager {
execPath, _ := os.Executable()
execDir := filepath.Dir(execPath)
return &VersionManager{
executableDir: execDir,
logger: logger.GetDefaultLogger(),
}
}
// createDefaultVersion 创建默认版本结构 v0.0.0
func (vm *VersionManager) createDefaultVersion() *VersionInfo {
return &VersionInfo{
MainVersion: "0.0.0.0", // 对应 v0.0.0
VersionInfo: make(map[string]map[string][]string),
}
}
// LoadVersionFromFile 从 resources/version.json 加载版本信息并处理回退
func (vm *VersionManager) LoadVersionFromFile() (*VersionInfo, error) {
versionPath := filepath.Join(vm.executableDir, "resources", "version.json")
data, err := os.ReadFile(versionPath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("未读取到版本信息,使用默认版本进行更新。")
return vm.createDefaultVersion(), nil
}
vm.logger.Warn("读取版本文件 %s 失败: %v将使用默认版本", versionPath, err)
return vm.createDefaultVersion(), nil
}
var versionInfo VersionInfo
if err := json.Unmarshal(data, &versionInfo); err != nil {
vm.logger.Warn("解析版本文件 %s 失败: %v将使用默认版本", versionPath, err)
return vm.createDefaultVersion(), nil
}
vm.logger.Debug("成功从 %s 加载版本信息", versionPath)
return &versionInfo, nil
}
// LoadVersionWithDefault 加载版本信息并保证回退到默认版本
func (vm *VersionManager) LoadVersionWithDefault() *VersionInfo {
versionInfo, err := vm.LoadVersionFromFile()
if err != nil {
// 这在更新的 LoadVersionFromFile 中不应该发生,但添加作为额外安全措施
vm.logger.Error("加载版本文件时出现意外错误: %v使用默认版本", err)
return vm.createDefaultVersion()
}
// 验证我们有一个有效的版本结构
if versionInfo == nil {
vm.logger.Warn("版本信息为空,使用默认版本")
return vm.createDefaultVersion()
}
if versionInfo.MainVersion == "" {
vm.logger.Warn("版本信息主版本为空,使用默认版本")
return vm.createDefaultVersion()
}
if versionInfo.VersionInfo == nil {
vm.logger.Debug("版本信息映射为空,初始化空映射")
versionInfo.VersionInfo = make(map[string]map[string][]string)
}
return versionInfo
}
// ParseVersion 解析版本字符串如 "4.4.1.3" 为组件
func ParseVersion(versionStr string) (*ParsedVersion, error) {
parts := strings.Split(versionStr, ".")
if len(parts) < 3 || len(parts) > 4 {
return nil, fmt.Errorf("无效的版本格式: %s", versionStr)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("无效的主版本号: %s", parts[0])
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的次版本号: %s", parts[1])
}
patch, err := strconv.Atoi(parts[2])
if err != nil {
return nil, fmt.Errorf("无效的补丁版本号: %s", parts[2])
}
beta := 0
if len(parts) == 4 {
beta, err = strconv.Atoi(parts[3])
if err != nil {
return nil, fmt.Errorf("无效的测试版本号: %s", parts[3])
}
}
return &ParsedVersion{
Major: major,
Minor: minor,
Patch: patch,
Beta: beta,
}, nil
}
// ToVersionString 将 ParsedVersion 转换回版本字符串格式
func (pv *ParsedVersion) ToVersionString() string {
if pv.Beta == 0 {
return fmt.Sprintf("%d.%d.%d.0", pv.Major, pv.Minor, pv.Patch)
}
return fmt.Sprintf("%d.%d.%d.%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
}
// ToDisplayVersion 将版本转换为显示格式 (v4.4.0 或 v4.4.1-beta3)
func (pv *ParsedVersion) ToDisplayVersion() string {
if pv.Beta == 0 {
return fmt.Sprintf("v%d.%d.%d", pv.Major, pv.Minor, pv.Patch)
}
return fmt.Sprintf("v%d.%d.%d-beta%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
}
// GetChannel 根据版本返回渠道 (stable 或 beta)
func (pv *ParsedVersion) GetChannel() string {
if pv.Beta == 0 {
return "stable"
}
return "beta"
}
// IsNewer 检查此版本是否比其他版本更新
func (pv *ParsedVersion) IsNewer(other *ParsedVersion) bool {
if pv.Major != other.Major {
return pv.Major > other.Major
}
if pv.Minor != other.Minor {
return pv.Minor > other.Minor
}
if pv.Patch != other.Patch {
return pv.Patch > other.Patch
}
return pv.Beta > other.Beta
}

View File

@@ -0,0 +1,366 @@
package version
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestParseVersion(t *testing.T) {
tests := []struct {
input string
expected *ParsedVersion
hasError bool
}{
{"4.4.0.0", &ParsedVersion{4, 4, 0, 0}, false},
{"4.4.1.3", &ParsedVersion{4, 4, 1, 3}, false},
{"1.2.3", &ParsedVersion{1, 2, 3, 0}, false},
{"invalid", nil, true},
{"1.2", nil, true},
{"1.2.3.4.5", nil, true},
}
for _, test := range tests {
result, err := ParseVersion(test.input)
if test.hasError {
if err == nil {
t.Errorf("Expected error for input %s, but got none", test.input)
}
continue
}
if err != nil {
t.Errorf("Unexpected error for input %s: %v", test.input, err)
continue
}
if result.Major != test.expected.Major ||
result.Minor != test.expected.Minor ||
result.Patch != test.expected.Patch ||
result.Beta != test.expected.Beta {
t.Errorf("For input %s, expected %+v, got %+v", test.input, test.expected, result)
}
}
}
func TestToDisplayVersion(t *testing.T) {
tests := []struct {
version *ParsedVersion
expected string
}{
{&ParsedVersion{4, 4, 0, 0}, "v4.4.0"},
{&ParsedVersion{4, 4, 1, 3}, "v4.4.1-beta3"},
{&ParsedVersion{1, 2, 3, 0}, "v1.2.3"},
{&ParsedVersion{1, 2, 3, 5}, "v1.2.3-beta5"},
}
for _, test := range tests {
result := test.version.ToDisplayVersion()
if result != test.expected {
t.Errorf("For version %+v, expected %s, got %s", test.version, test.expected, result)
}
}
}
func TestGetChannel(t *testing.T) {
tests := []struct {
version *ParsedVersion
expected string
}{
{&ParsedVersion{4, 4, 0, 0}, "stable"},
{&ParsedVersion{4, 4, 1, 3}, "beta"},
{&ParsedVersion{1, 2, 3, 0}, "stable"},
{&ParsedVersion{1, 2, 3, 1}, "beta"},
}
for _, test := range tests {
result := test.version.GetChannel()
if result != test.expected {
t.Errorf("For version %+v, expected channel %s, got %s", test.version, test.expected, result)
}
}
}
func TestIsNewer(t *testing.T) {
tests := []struct {
v1 *ParsedVersion
v2 *ParsedVersion
expected bool
}{
{&ParsedVersion{4, 4, 1, 0}, &ParsedVersion{4, 4, 0, 0}, true},
{&ParsedVersion{4, 4, 0, 0}, &ParsedVersion{4, 4, 1, 0}, false},
{&ParsedVersion{4, 4, 1, 3}, &ParsedVersion{4, 4, 1, 2}, true},
{&ParsedVersion{4, 4, 1, 2}, &ParsedVersion{4, 4, 1, 3}, false},
{&ParsedVersion{4, 4, 1, 0}, &ParsedVersion{4, 4, 1, 0}, false},
}
for _, test := range tests {
result := test.v1.IsNewer(test.v2)
if result != test.expected {
t.Errorf("For %+v.IsNewer(%+v), expected %t, got %t", test.v1, test.v2, test.expected, result)
}
}
}
func TestLoadVersionFromFile(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create test version file
versionData := VersionInfo{
MainVersion: "4.4.1.3",
VersionInfo: map[string]map[string][]string{
"4.4.1.3": {
"修复BUG": {"移除崩溃弹窗机制"},
},
},
}
data, err := json.Marshal(versionData)
if err != nil {
t.Fatal(err)
}
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, data, 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory and logger
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version
result, err := vm.LoadVersionFromFile()
if err != nil {
t.Fatalf("Failed to load version: %v", err)
}
if result.MainVersion != "4.4.1.3" {
t.Errorf("Expected main version 4.4.1.3, got %s", result.MainVersion)
}
if len(result.VersionInfo) != 1 {
t.Errorf("Expected 1 version info entry, got %d", len(result.VersionInfo))
}
}
func TestLoadVersionFromFileNotFound(t *testing.T) {
// Create a temporary directory without version file
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create version manager with custom executable directory and logger
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version (should now return default version instead of error)
result, err := vm.LoadVersionFromFile()
if err != nil {
t.Errorf("Expected no error with fallback mechanism, but got: %v", err)
}
// Should return default version
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map, got nil")
}
}
func TestLoadVersionWithDefault(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version with default (no file exists)
result := vm.LoadVersionWithDefault()
if result == nil {
t.Fatal("Expected non-nil result from LoadVersionWithDefault")
}
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map, got nil")
}
}
func TestLoadVersionWithDefaultValidFile(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create test version file
versionData := VersionInfo{
MainVersion: "4.4.1.3",
VersionInfo: map[string]map[string][]string{
"4.4.1.3": {
"修复BUG": {"移除崩溃弹窗机制"},
},
},
}
data, err := json.Marshal(versionData)
if err != nil {
t.Fatal(err)
}
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, data, 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version with default (valid file exists)
result := vm.LoadVersionWithDefault()
if result == nil {
t.Fatal("Expected non-nil result from LoadVersionWithDefault")
}
if result.MainVersion != "4.4.1.3" {
t.Errorf("Expected version 4.4.1.3, got %s", result.MainVersion)
}
if len(result.VersionInfo) != 1 {
t.Errorf("Expected 1 version info entry, got %d", len(result.VersionInfo))
}
}
func TestLoadVersionFromFileCorrupted(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create corrupted version file
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, []byte("invalid json content"), 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version (should return default version for corrupted file)
result, err := vm.LoadVersionFromFile()
if err != nil {
t.Errorf("Expected no error with fallback mechanism for corrupted file, but got: %v", err)
}
// Should return default version
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0 for corrupted file, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map for corrupted file, got nil")
}
}
func TestLoadVersionWithDefaultCorrupted(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create corrupted version file
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, []byte("invalid json content"), 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version with default (corrupted file)
result := vm.LoadVersionWithDefault()
if result == nil {
t.Fatal("Expected non-nil result from LoadVersionWithDefault for corrupted file")
}
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0 for corrupted file, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map for corrupted file, got nil")
}
}
func TestCreateDefaultVersion(t *testing.T) {
vm := NewVersionManager()
result := vm.createDefaultVersion()
if result == nil {
t.Fatal("Expected non-nil result from createDefaultVersion")
}
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map, got nil")
}
if len(result.VersionInfo) != 0 {
t.Errorf("Expected empty VersionInfo map, got %d entries", len(result.VersionInfo))
}
}

View File

@@ -0,0 +1,19 @@
package version
import (
"runtime"
)
var (
// Version 应用程序的当前版本
Version = "1.0.0"
// BuildTime 在构建时设置
BuildTime = "unknown"
// GitCommit 在构建时设置
GitCommit = "unknown"
// GoVersion 用于构建的 Go 版本
GoVersion = runtime.Version()
)

View File

@@ -51,6 +51,8 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
Source: "{#MyAppPath}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\app\*"; DestDir: "{app}\app"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\resources\*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\Go_Updater\*"; DestDir: "{app}\Go_Updater"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\AUTO_MAA_Go_Updater_install.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\main.py"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\requirements.txt"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\README.md"; DestDir: "{app}"; Flags: ignoreversion