🎨 Modify configuration migration policy

This commit is contained in:
2025-09-05 22:07:00 +08:00
parent 97ee3b0667
commit a257d30dba
4 changed files with 102 additions and 46 deletions

View File

@@ -8,7 +8,6 @@ import (
"github.com/wailsapp/wails/v3/pkg/services/log"
"os"
"path/filepath"
"strings"
"time"
)
@@ -54,7 +53,6 @@ func NewConfigMigrator(
// AutoMigrate automatically detects and migrates missing configuration fields
func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
// Load default config into temporary koanf instance
defaultKoanf := koanf.New(".")
if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
return nil, fmt.Errorf("failed to load default config: %w", err)
@@ -137,14 +135,12 @@ func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf
actuallyMerged := 0
for _, field := range missingFields {
// Use Exists() for better semantic checking
if !current.Exists(field) && defaultConfig.Exists(field) {
// Check if setting this field would conflict with existing user values
if !cm.wouldCreateTypeConflict(current, field) {
if defaultValue := defaultConfig.Get(field); defaultValue != nil {
current.Set(field, defaultValue)
actuallyMerged++
}
if defaultConfig.Exists(field) {
if defaultValue := defaultConfig.Get(field); defaultValue != nil {
// Always set the field, even if it causes type conflicts
// This allows configuration structure evolution during upgrades
current.Set(field, defaultValue)
actuallyMerged++
}
}
}
@@ -157,28 +153,6 @@ func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf
return nil
}
// wouldCreateTypeConflict checks if setting a field would overwrite existing user data
func (cm *ConfigMigrator) wouldCreateTypeConflict(current *koanf.Koanf, fieldPath string) bool {
parts := strings.Split(fieldPath, ".")
// Check each parent path to see if user has a non-map value there
for i := 1; i < len(parts); i++ {
parentPath := strings.Join(parts[:i], ".")
// Use Exists() for better semantic checking
if current.Exists(parentPath) {
if parentValue := current.Get(parentPath); parentValue != nil {
// If parent exists and is not a map, setting this field would overwrite it
if _, isMap := parentValue.(map[string]interface{}); !isMap {
return true
}
}
}
}
return false
}
// createBackup creates a backup of the configuration file
func (cm *ConfigMigrator) createBackup() (string, error) {
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {

View File

@@ -238,7 +238,7 @@ func TestConfigMigrator_NoOverwrite(t *testing.T) {
assert.Equal(t, "new section", k.String("newSection.value"), "Missing section should be added")
}
// TestConfigMigrator_TypeMismatch tests handling of type mismatches
// TestConfigMigrator_TypeMismatch tests handling of type mismatches (config structure evolution)
func TestConfigMigrator_TypeMismatch(t *testing.T) {
tempDir, err := os.MkdirTemp("", "config_migrator_type_test")
if err != nil {
@@ -290,13 +290,95 @@ func TestConfigMigrator_TypeMismatch(t *testing.T) {
result, err := migrator.AutoMigrate(defaultConfig, k)
assert.NoError(t, err)
// Should detect missing fields but refuse to merge them due to type conflicts
assert.True(t, result.Migrated, "Migration should be attempted")
assert.Greater(t, len(result.MissingFields), 0, "Should detect missing fields even with type mismatch")
// Should detect missing fields and merge them, overriding type conflicts for config evolution
assert.True(t, result.Migrated, "Migration should be performed")
assert.Greater(t, len(result.MissingFields), 0, "Should detect missing fields with type mismatch")
// Verify user's type-mismatched values are preserved (not overwritten)
assert.Equal(t, "simple_string", k.String("user.settings"), "User's string value should be preserved")
assert.Equal(t, int64(123), k.Int64("newSection"), "User's number value should be preserved")
// Verify that type-mismatched values are overwritten with new structure (config evolution)
// This is important for software upgrades where config structure changes
assert.NotEqual(t, "simple_string", k.String("user.settings"), "User's string should be overwritten with new structure")
assert.NotEqual(t, int64(123), k.Int64("newSection"), "User's number should be overwritten with new structure")
// Verify new structure is properly applied
assert.True(t, k.Bool("user.settings.autoSave"), "New settings structure should be applied")
assert.Equal(t, "en", k.String("user.settings.language"), "New settings structure should be applied")
assert.True(t, k.Bool("user.settings.newSetting"), "New settings structure should be applied")
assert.Equal(t, "value", k.String("user.settings.newSetting2"), "New settings structure should be applied")
assert.True(t, k.Bool("newSection.enabled"), "New section structure should be applied")
assert.Equal(t, "new section", k.String("newSection.value"), "New section structure should be applied")
}
// TestConfigMigrator_ConfigEvolution tests configuration structure evolution scenarios
func TestConfigMigrator_ConfigEvolution(t *testing.T) {
tempDir, err := os.MkdirTemp("", "config_migrator_evolution_test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
// Simulate old config format where "features" was a simple string
// but new version expects it to be an object
oldConfig := map[string]interface{}{
"app": map[string]interface{}{
"name": "MyApp",
"version": "1.0.0",
"features": "plugin1,plugin2", // Old: simple comma-separated string
},
"database": "sqlite://data.db", // Old: simple string
}
configPath := filepath.Join(tempDir, "config.json")
jsonData, _ := json.MarshalIndent(oldConfig, "", " ")
os.WriteFile(configPath, jsonData, 0644)
logger := log.New()
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
k := koanf.New(".")
k.Load(file.Provider(configPath), jsonparser.Parser())
// New config format where "features" and "database" are objects
type NewConfig struct {
App struct {
Name string `json:"name"`
Version string `json:"version"`
Features struct {
Enabled []string `json:"enabled"`
Config string `json:"config"`
} `json:"features"`
} `json:"app"`
Database struct {
Type string `json:"type"`
URL string `json:"url"`
} `json:"database"`
}
defaultConfig := NewConfig{}
defaultConfig.App.Name = "DefaultApp" // Will be preserved from user
defaultConfig.App.Version = "2.0.0" // Will be preserved from user
defaultConfig.App.Features.Enabled = []string{"newFeature"}
defaultConfig.App.Features.Config = "default.conf"
defaultConfig.Database.Type = "postgresql"
defaultConfig.Database.URL = "postgres://localhost:5432/db"
// Run migration
result, err := migrator.AutoMigrate(defaultConfig, k)
assert.NoError(t, err)
assert.True(t, result.Migrated)
// User's non-conflicting values should be preserved
assert.Equal(t, "MyApp", k.String("app.name"), "User's app name should be preserved")
assert.Equal(t, "1.0.0", k.String("app.version"), "User's version should be preserved")
// Conflicting structure should be evolved (old string → new object)
assert.NotEqual(t, "plugin1,plugin2", k.String("app.features"), "Old features string should be replaced")
assert.Equal(t, []string{"newFeature"}, k.Strings("app.features.enabled"), "New features structure should be applied")
assert.Equal(t, "default.conf", k.String("app.features.config"), "New features structure should be applied")
assert.NotEqual(t, "sqlite://data.db", k.String("database"), "Old database string should be replaced")
assert.Equal(t, "postgresql", k.String("database.type"), "New database structure should be applied")
assert.Equal(t, "postgres://localhost:5432/db", k.String("database.url"), "New database structure should be applied")
t.Logf("Successfully evolved config structure, fields migrated: %d", len(result.MissingFields))
}
// TestConfigMigrator_ComplexNested tests complex nested structure migration

View File

@@ -109,9 +109,6 @@ func (cs *ConfigService) initConfig() error {
return cs.createDefaultConfig()
}
// 检查并自动迁移配置
cs.checkConfigMigration()
// 配置文件存在,直接加载现有配置
cs.fileProvider = file.Provider(cs.settingsPath)
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
@@ -121,8 +118,8 @@ func (cs *ConfigService) initConfig() error {
return nil
}
// checkConfigMigration 检查配置迁移
func (cs *ConfigService) checkConfigMigration() error {
// MigrateConfig 执行配置迁移
func (cs *ConfigService) MigrateConfig() error {
if cs.configMigrator == nil {
return nil
}

View File

@@ -251,7 +251,6 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
// 检查是否有可用更新
if !primaryRelease.GreaterThan(s.config.Updates.Version) {
s.logger.Info("Current version is up to date, no need to apply update")
result.LatestVersion = primaryRelease.Version()
return result, nil
}
@@ -310,6 +309,11 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
s.logger.Error("Failed to update config version", "error", err)
}
// 执行配置迁移
if err := s.configService.MigrateConfig(); err != nil {
s.logger.Error("Failed to migrate config after update", "error", err)
}
return result, nil
}
@@ -365,7 +369,6 @@ func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType mod
}
// 尝试下载并应用更新,不设置超时
s.logger.Info("Downloading update...", "source", sourceType)
err = updater.UpdateTo(ctx, release, exe)
if err != nil {