refactor(updater): 重构 Go 版本更新器

- 更新项目名称为 AUTO_MAA_Go_Updater
- 重构代码结构,优化函数命名和逻辑
- 移除 CDK 相关的冗余代码
- 调整版本号为 git commit hash
- 更新构建配置和脚本
- 优化 API 客户端实现
This commit is contained in:
2025-07-22 21:51:58 +08:00
parent 747ad6387b
commit 6b646378b6
21 changed files with 887 additions and 1673 deletions

View File

@@ -1,40 +1,38 @@
package config
import (
"encoding/base64"
"fmt"
"os"
"path/filepath"
"AUTO_MAA_Go_Updater/assets"
"gopkg.in/yaml.v3"
"lightweight-updater/assets"
)
// Config represents the application configuration
// Config 表示应用程序配置
type Config struct {
ResourceID string `yaml:"resource_id"`
CurrentVersion string `yaml:"current_version"`
CDK string `yaml:"cdk,omitempty"`
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"` // seconds
CheckInterval int `yaml:"check_interval"` //
}
// ConfigManager interface defines methods for configuration management
// ConfigManager 定义配置管理的接口方法
type ConfigManager interface {
Load() (*Config, error)
Save(config *Config) error
GetConfigPath() string
}
// DefaultConfigManager implements ConfigManager interface
// DefaultConfigManager 实现 ConfigManager 接口
type DefaultConfigManager struct {
configPath string
}
// NewConfigManager creates a new configuration manager
// NewConfigManager 创建新的配置管理器
func NewConfigManager() ConfigManager {
configDir := getConfigDir()
configPath := filepath.Join(configDir, "config.yaml")
@@ -43,77 +41,77 @@ func NewConfigManager() ConfigManager {
}
}
// GetConfigPath returns the path to the configuration file
// GetConfigPath 返回配置文件的路径
func (cm *DefaultConfigManager) GetConfigPath() string {
return cm.configPath
}
// Load reads and parses the configuration file
// Load 读取并解析配置文件
func (cm *DefaultConfigManager) Load() (*Config, error) {
// Create config directory if it doesn't exist
// 如果配置目录不存在则创建
configDir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
return nil, fmt.Errorf("创建配置目录失败: %w", err)
}
// If config file doesn't exist, create default config
// 如果配置文件不存在,创建默认配置
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
defaultConfig := getDefaultConfig()
if err := cm.Save(defaultConfig); err != nil {
return nil, fmt.Errorf("failed to create default config: %w", err)
return nil, fmt.Errorf("创建默认配置失败: %w", err)
}
return defaultConfig, nil
}
// Read existing config file
// 读取现有配置文件
data, err := os.ReadFile(cm.configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
// Validate and apply defaults for missing fields
// 验证并应用缺失字段的默认值
if err := validateAndApplyDefaults(&config); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
return nil, fmt.Errorf("配置验证失败: %w", err)
}
return &config, nil
}
// Save writes the configuration to file
// Save 将配置写入文件
func (cm *DefaultConfigManager) Save(config *Config) error {
// Validate config before saving
// 保存前验证配置
if err := validateConfig(config); err != nil {
return fmt.Errorf("config validation failed: %w", err)
return fmt.Errorf("配置验证失败: %w", err)
}
// Create config directory if it doesn't exist
// 如果配置目录不存在则创建
configDir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
return fmt.Errorf("创建配置目录失败: %w", err)
}
// Marshal config to YAML
// 将配置序列化为 YAML
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
return fmt.Errorf("序列化配置失败: %w", err)
}
// Write to file
// 写入文件
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
return fmt.Errorf("写入配置文件失败: %w", err)
}
return nil
}
// getDefaultConfig returns a configuration with default values
// getDefaultConfig 返回带有默认值的配置
func getDefaultConfig() *Config {
// Try to load from embedded template first
// 首先尝试从嵌入模板加载
if templateData, err := assets.GetConfigTemplate(); err == nil {
var config Config
if err := yaml.Unmarshal(templateData, &config); err == nil {
@@ -121,35 +119,34 @@ func getDefaultConfig() *Config {
}
}
// Fallback to hardcoded defaults if template loading fails
// 如果模板加载失败则回退到硬编码默认值
return &Config{
ResourceID: "M9A", // Default resource ID
ResourceID: "M9A", // 默认资源 ID
CurrentVersion: "v1.0.0",
CDK: "",
UserAgent: "LightweightUpdater/1.0",
UserAgent: "AUTO_MAA_Go_Updater/1.0",
BackupURL: "",
LogLevel: "info",
AutoCheck: true,
CheckInterval: 3600, // 1 hour
CheckInterval: 3600, // 1 小时
}
}
// validateConfig validates the configuration values
// validateConfig 验证配置值
func validateConfig(config *Config) error {
if config == nil {
return fmt.Errorf("config cannot be nil")
return fmt.Errorf("配置不能为空")
}
if config.ResourceID == "" {
return fmt.Errorf("resource_id cannot be empty")
return fmt.Errorf("resource_id 不能为空")
}
if config.CurrentVersion == "" {
return fmt.Errorf("current_version cannot be empty")
return fmt.Errorf("current_version 不能为空")
}
if config.UserAgent == "" {
return fmt.Errorf("user_agent cannot be empty")
return fmt.Errorf("user_agent 不能为空")
}
validLogLevels := map[string]bool{
@@ -159,21 +156,21 @@ func validateConfig(config *Config) error {
"error": true,
}
if !validLogLevels[config.LogLevel] {
return fmt.Errorf("invalid log_level: %s (must be debug, info, warn, or error)", config.LogLevel)
return fmt.Errorf("无效的 log_level: %s (必须是 debug, info, warn error)", config.LogLevel)
}
if config.CheckInterval < 60 {
return fmt.Errorf("check_interval must be at least 60 seconds")
return fmt.Errorf("check_interval 必须至少为 60 秒")
}
return nil
}
// validateAndApplyDefaults validates config and applies defaults for missing fields
// validateAndApplyDefaults 验证配置并为缺失字段应用默认值
func validateAndApplyDefaults(config *Config) error {
defaults := getDefaultConfig()
// Apply defaults for empty fields
// 为空字段应用默认值
if config.UserAgent == "" {
config.UserAgent = defaults.UserAgent
}
@@ -187,62 +184,15 @@ func validateAndApplyDefaults(config *Config) error {
config.CurrentVersion = defaults.CurrentVersion
}
// Validate after applying defaults
// 应用默认值后进行验证
return validateConfig(config)
}
// getConfigDir returns the configuration directory path
// getConfigDir 返回配置目录路径
func getConfigDir() string {
// Use APPDATA on Windows, fallback to current directory
// 在 Windows 上使用 APPDATA回退到当前目录
if appData := os.Getenv("APPDATA"); appData != "" {
return filepath.Join(appData, "LightweightUpdater")
return filepath.Join(appData, "AUTO_MAA_Go_Updater")
}
return "."
}
// encryptCDK encrypts the CDK using XOR encryption with a static key
func encryptCDK(cdk string) string {
if cdk == "" {
return ""
}
key := []byte("updater-key-2024")
encrypted := make([]byte, len(cdk))
for i, b := range []byte(cdk) {
encrypted[i] = b ^ key[i%len(key)]
}
return base64.StdEncoding.EncodeToString(encrypted)
}
// decryptCDK decrypts the CDK using XOR decryption with a static key
func decryptCDK(encryptedCDK string) (string, error) {
if encryptedCDK == "" {
return "", nil
}
encrypted, err := base64.StdEncoding.DecodeString(encryptedCDK)
if err != nil {
return "", fmt.Errorf("failed to decode encrypted CDK: %w", err)
}
key := []byte("updater-key-2024")
decrypted := make([]byte, len(encrypted))
for i, b := range encrypted {
decrypted[i] = b ^ key[i%len(key)]
}
return string(decrypted), nil
}
// SetCDK sets the CDK in the config with encryption
func (c *Config) SetCDK(cdk string) {
c.CDK = encryptCDK(cdk)
}
// GetCDK returns the decrypted CDK from the config
func (c *Config) GetCDK() (string, error) {
return decryptCDK(c.CDK)
}

View File

@@ -44,7 +44,6 @@
},
"Update": {
"IfAutoUpdate": false,
"MirrorChyanCDK": "",
"ProxyUrlList": [],
"ThreadNumb": 8,
"UpdateType": "stable"

View File

@@ -3,163 +3,55 @@ package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEncryptDecryptCDK(t *testing.T) {
tests := []struct {
name string
original string
}{
{
name: "Empty CDK",
original: "",
},
{
name: "Simple CDK",
original: "test123",
},
{
name: "Complex CDK",
original: "ABC123-DEF456-GHI789",
},
{
name: "CDK with special characters",
original: "test@#$%^&*()_+-={}[]|\\:;\"'<>?,./",
},
{
name: "Long CDK",
original: "this-is-a-very-long-cdk-key-that-should-still-work-properly-with-encryption-and-decryption",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test encryption
encrypted := encryptCDK(tt.original)
// Empty string should remain empty
if tt.original == "" {
if encrypted != "" {
t.Errorf("Expected empty string for empty input, got %s", encrypted)
}
return
}
// Encrypted should be different from original (unless original is empty)
if encrypted == tt.original {
t.Errorf("Encrypted CDK should be different from original")
}
// Test decryption
decrypted, err := decryptCDK(encrypted)
if err != nil {
t.Errorf("Decryption failed: %v", err)
}
// Decrypted should match original
if decrypted != tt.original {
t.Errorf("Expected %s, got %s", tt.original, decrypted)
}
})
}
}
func TestConfigSetGetCDK(t *testing.T) {
config := &Config{}
testCDK := "test-cdk-123"
// Set CDK (should encrypt)
config.SetCDK(testCDK)
// CDK field should be encrypted (different from original)
if config.CDK == testCDK {
t.Errorf("CDK should be encrypted in config")
}
// Get CDK (should decrypt)
retrievedCDK, err := config.GetCDK()
if err != nil {
t.Errorf("Failed to get CDK: %v", err)
}
if retrievedCDK != testCDK {
t.Errorf("Expected %s, got %s", testCDK, retrievedCDK)
}
}
func TestDecryptInvalidCDK(t *testing.T) {
// Test with invalid base64
_, err := decryptCDK("invalid-base64!")
if err == nil {
t.Errorf("Expected error for invalid base64")
}
}
func TestConfigManagerLoadSave(t *testing.T) {
// Create temporary directory for test
// 为测试创建临时目录
tempDir := t.TempDir()
// Create config manager with temp path
// 使用临时路径创建配置管理器
cm := &DefaultConfigManager{
configPath: filepath.Join(tempDir, "test-config.yaml"),
}
// Test loading non-existent config (should create default)
// 测试加载不存在的配置(应创建默认配置)
config, err := cm.Load()
if err != nil {
t.Errorf("Failed to load config: %v", err)
t.Errorf("加载配置失败: %v", err)
}
if config == nil {
t.Errorf("Config should not be nil")
t.Errorf("配置不应为 nil")
}
// Verify default values
// 验证默认值
if config.CurrentVersion != "v1.0.0" {
t.Errorf("Expected default version v1.0.0, got %s", config.CurrentVersion)
t.Errorf("期望默认版本 v1.0.0,得到 %s", config.CurrentVersion)
}
if config.UserAgent != "LightweightUpdater/1.0" {
t.Errorf("Expected default user agent, got %s", config.UserAgent)
if config.UserAgent != "AUTO_MAA_Go_Updater/1.0" {
t.Errorf("期望默认用户代理,得到 %s", config.UserAgent)
}
// Set some values including CDK
// 设置一些值
config.ResourceID = "TEST123"
config.SetCDK("secret-cdk-key")
// Save config
// 保存配置
err = cm.Save(config)
if err != nil {
t.Errorf("Failed to save config: %v", err)
t.Errorf("保存配置失败: %v", err)
}
// Load config again
// 再次加载配置
loadedConfig, err := cm.Load()
if err != nil {
t.Errorf("Failed to load saved config: %v", err)
t.Errorf("加载已保存配置失败: %v", err)
}
// Verify values
// 验证值
if loadedConfig.ResourceID != "TEST123" {
t.Errorf("Expected ResourceID TEST123, got %s", loadedConfig.ResourceID)
}
// Verify CDK is properly encrypted/decrypted
retrievedCDK, err := loadedConfig.GetCDK()
if err != nil {
t.Errorf("Failed to get CDK from loaded config: %v", err)
}
if retrievedCDK != "secret-cdk-key" {
t.Errorf("Expected CDK secret-cdk-key, got %s", retrievedCDK)
}
// Verify CDK is encrypted in the config struct
if loadedConfig.CDK == "secret-cdk-key" {
t.Errorf("CDK should be encrypted in config file")
t.Errorf("期望 ResourceID TEST123,得到 %s", loadedConfig.ResourceID)
}
}
@@ -170,12 +62,12 @@ func TestConfigValidation(t *testing.T) {
expectError bool
}{
{
name: "Nil config",
name: "空配置",
config: nil,
expectError: true,
},
{
name: "Empty ResourceID",
name: " ResourceID",
config: &Config{
ResourceID: "",
CurrentVersion: "v1.0.0",
@@ -186,40 +78,7 @@ func TestConfigValidation(t *testing.T) {
expectError: true,
},
{
name: "Empty CurrentVersion",
config: &Config{
ResourceID: "TEST",
CurrentVersion: "",
UserAgent: "Test/1.0",
LogLevel: "info",
CheckInterval: 3600,
},
expectError: true,
},
{
name: "Invalid LogLevel",
config: &Config{
ResourceID: "TEST",
CurrentVersion: "v1.0.0",
UserAgent: "Test/1.0",
LogLevel: "invalid",
CheckInterval: 3600,
},
expectError: true,
},
{
name: "Invalid CheckInterval",
config: &Config{
ResourceID: "TEST",
CurrentVersion: "v1.0.0",
UserAgent: "Test/1.0",
LogLevel: "info",
CheckInterval: 30, // Less than 60
},
expectError: true,
},
{
name: "Valid config",
name: "有效配置",
config: &Config{
ResourceID: "TEST",
CurrentVersion: "v1.0.0",
@@ -235,112 +94,10 @@ func TestConfigValidation(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
err := validateConfig(tt.config)
if tt.expectError && err == nil {
t.Errorf("Expected error but got none")
t.Errorf("期望错误但没有得到")
}
if !tt.expectError && err != nil {
t.Errorf("Expected no error but got: %v", err)
}
})
}
}
func TestGetConfigDir(t *testing.T) {
// Save original APPDATA
originalAppData := os.Getenv("APPDATA")
defer os.Setenv("APPDATA", originalAppData)
// Test with APPDATA set
os.Setenv("APPDATA", "C:\\Users\\Test\\AppData\\Roaming")
dir := getConfigDir()
expected := "C:\\Users\\Test\\AppData\\Roaming\\LightweightUpdater"
if dir != expected {
t.Errorf("Expected %s, got %s", expected, dir)
}
// Test without APPDATA
os.Unsetenv("APPDATA")
dir = getConfigDir()
if dir != "." {
t.Errorf("Expected current directory, got %s", dir)
}
}
func TestValidateAndApplyDefaults(t *testing.T) {
tests := []struct {
name string
input *Config
expected *Config
hasError bool
}{
{
name: "Apply defaults to empty config",
input: &Config{
ResourceID: "TEST",
},
expected: &Config{
ResourceID: "TEST",
CurrentVersion: "v1.0.0",
UserAgent: "LightweightUpdater/1.0",
LogLevel: "info",
CheckInterval: 3600,
},
hasError: false,
},
{
name: "Partial config with some defaults needed",
input: &Config{
ResourceID: "TEST",
CurrentVersion: "v2.0.0",
LogLevel: "debug",
},
expected: &Config{
ResourceID: "TEST",
CurrentVersion: "v2.0.0",
UserAgent: "LightweightUpdater/1.0",
LogLevel: "debug",
CheckInterval: 3600,
},
hasError: false,
},
{
name: "Config with invalid values after defaults",
input: &Config{
ResourceID: "", // Invalid - empty
CheckInterval: 30, // Invalid - too small
},
expected: nil,
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateAndApplyDefaults(tt.input)
if tt.hasError {
if err == nil {
t.Errorf("Expected error but got none")
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
// Check that defaults were applied correctly
if tt.input.CurrentVersion != tt.expected.CurrentVersion {
t.Errorf("CurrentVersion: expected %s, got %s", tt.expected.CurrentVersion, tt.input.CurrentVersion)
}
if tt.input.UserAgent != tt.expected.UserAgent {
t.Errorf("UserAgent: expected %s, got %s", tt.expected.UserAgent, tt.input.UserAgent)
}
if tt.input.LogLevel != tt.expected.LogLevel {
t.Errorf("LogLevel: expected %s, got %s", tt.expected.LogLevel, tt.input.LogLevel)
}
if tt.input.CheckInterval != tt.expected.CheckInterval {
t.Errorf("CheckInterval: expected %d, got %d", tt.expected.CheckInterval, tt.input.CheckInterval)
t.Errorf("期望无错误但得到: %v", err)
}
})
}
@@ -350,123 +107,47 @@ func TestGetDefaultConfig(t *testing.T) {
config := getDefaultConfig()
if config == nil {
t.Fatal("getDefaultConfig() returned nil")
t.Fatal("getDefaultConfig() 返回 nil")
}
// Verify default values
if config.ResourceID != "PLACEHOLDER" {
t.Errorf("Expected ResourceID 'PLACEHOLDER', got %s", config.ResourceID)
// 验证默认值
if config.ResourceID != "AUTO_MAA" {
t.Errorf("期望 ResourceID 'AUTO_MAA',得到 %s", config.ResourceID)
}
if config.CurrentVersion != "v1.0.0" {
t.Errorf("Expected CurrentVersion 'v1.0.0', got %s", config.CurrentVersion)
t.Errorf("期望 CurrentVersion 'v1.0.0',得到 %s", config.CurrentVersion)
}
if config.UserAgent != "LightweightUpdater/1.0" {
t.Errorf("Expected UserAgent 'LightweightUpdater/1.0', got %s", config.UserAgent)
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("Expected LogLevel 'info', got %s", config.LogLevel)
t.Errorf("期望 LogLevel 'info',得到 %s", config.LogLevel)
}
if config.CheckInterval != 3600 {
t.Errorf("Expected CheckInterval 3600, got %d", config.CheckInterval)
t.Errorf("期望 CheckInterval 3600,得到 %d", config.CheckInterval)
}
if !config.AutoCheck {
t.Errorf("Expected AutoCheck true, got %v", config.AutoCheck)
t.Errorf("期望 AutoCheck true,得到 %v", config.AutoCheck)
}
}
func TestConfigManagerWithCustomPath(t *testing.T) {
tempDir := t.TempDir()
customPath := filepath.Join(tempDir, "custom-config.yaml")
func TestGetConfigDir(t *testing.T) {
// 保存原始 APPDATA
originalAppData := os.Getenv("APPDATA")
defer os.Setenv("APPDATA", originalAppData)
cm := &DefaultConfigManager{
configPath: customPath,
// 测试设置了 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)
}
// Test GetConfigPath
if cm.GetConfigPath() != customPath {
t.Errorf("Expected config path %s, got %s", customPath, cm.GetConfigPath())
}
// Test Save and Load with custom path
testConfig := &Config{
ResourceID: "CUSTOM",
CurrentVersion: "v1.5.0",
UserAgent: "CustomUpdater/1.0",
LogLevel: "debug",
CheckInterval: 7200,
AutoCheck: false,
}
// Save config
err := cm.Save(testConfig)
if err != nil {
t.Fatalf("Failed to save config: %v", err)
}
// Load config
loadedConfig, err := cm.Load()
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
// Verify loaded config matches saved config
if loadedConfig.ResourceID != testConfig.ResourceID {
t.Errorf("ResourceID mismatch: expected %s, got %s", testConfig.ResourceID, loadedConfig.ResourceID)
}
if loadedConfig.CurrentVersion != testConfig.CurrentVersion {
t.Errorf("CurrentVersion mismatch: expected %s, got %s", testConfig.CurrentVersion, loadedConfig.CurrentVersion)
}
if loadedConfig.AutoCheck != testConfig.AutoCheck {
t.Errorf("AutoCheck mismatch: expected %v, got %v", testConfig.AutoCheck, loadedConfig.AutoCheck)
}
}
func TestConfigManagerErrorHandling(t *testing.T) {
// Test with invalid directory path
invalidPath := string([]byte{0}) + "/invalid/config.yaml"
cm := &DefaultConfigManager{
configPath: invalidPath,
}
// Load should fail with invalid path
_, err := cm.Load()
if err == nil {
t.Error("Expected error when loading from invalid path")
}
// Save should fail with invalid path
testConfig := getDefaultConfig()
testConfig.ResourceID = "TEST"
err = cm.Save(testConfig)
if err == nil {
t.Error("Expected error when saving to invalid path")
}
}
func TestEncryptDecryptEdgeCases(t *testing.T) {
tests := []struct {
name string
input string
}{
{"Unicode characters", "测试CDK密钥🔑"},
{"Very long string", strings.Repeat("A", 1000)},
{"Binary-like data", string([]byte{0, 1, 2, 3, 255, 254, 253})},
{"Only spaces", " "},
{"Newlines and tabs", "line1\nline2\tindented"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
encrypted := encryptCDK(tt.input)
decrypted, err := decryptCDK(encrypted)
if err != nil {
t.Errorf("Decryption failed: %v", err)
}
if decrypted != tt.input {
t.Errorf("Encryption/decryption mismatch: expected %q, got %q", tt.input, decrypted)
}
})
// 测试没有 APPDATA
os.Unsetenv("APPDATA")
dir = getConfigDir()
if dir != "." {
t.Errorf("期望当前目录,得到 %s", dir)
}
}