🎨 Modify configuration migration policy
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user