🎨 Modify configuration migration policy
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +53,6 @@ func NewConfigMigrator(
|
|||||||
|
|
||||||
// AutoMigrate automatically detects and migrates missing configuration fields
|
// AutoMigrate automatically detects and migrates missing configuration fields
|
||||||
func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
|
func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
|
||||||
// Load default config into temporary koanf instance
|
|
||||||
defaultKoanf := koanf.New(".")
|
defaultKoanf := koanf.New(".")
|
||||||
if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||||
return nil, fmt.Errorf("failed to load default config: %w", err)
|
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
|
actuallyMerged := 0
|
||||||
|
|
||||||
for _, field := range missingFields {
|
for _, field := range missingFields {
|
||||||
// Use Exists() for better semantic checking
|
if defaultConfig.Exists(field) {
|
||||||
if !current.Exists(field) && defaultConfig.Exists(field) {
|
if defaultValue := defaultConfig.Get(field); defaultValue != nil {
|
||||||
// Check if setting this field would conflict with existing user values
|
// Always set the field, even if it causes type conflicts
|
||||||
if !cm.wouldCreateTypeConflict(current, field) {
|
// This allows configuration structure evolution during upgrades
|
||||||
if defaultValue := defaultConfig.Get(field); defaultValue != nil {
|
current.Set(field, defaultValue)
|
||||||
current.Set(field, defaultValue)
|
actuallyMerged++
|
||||||
actuallyMerged++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,28 +153,6 @@ func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf
|
|||||||
return nil
|
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
|
// createBackup creates a backup of the configuration file
|
||||||
func (cm *ConfigMigrator) createBackup() (string, error) {
|
func (cm *ConfigMigrator) createBackup() (string, error) {
|
||||||
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
|
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")
|
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) {
|
func TestConfigMigrator_TypeMismatch(t *testing.T) {
|
||||||
tempDir, err := os.MkdirTemp("", "config_migrator_type_test")
|
tempDir, err := os.MkdirTemp("", "config_migrator_type_test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -290,13 +290,95 @@ func TestConfigMigrator_TypeMismatch(t *testing.T) {
|
|||||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Should detect missing fields but refuse to merge them due to type conflicts
|
// Should detect missing fields and merge them, overriding type conflicts for config evolution
|
||||||
assert.True(t, result.Migrated, "Migration should be attempted")
|
assert.True(t, result.Migrated, "Migration should be performed")
|
||||||
assert.Greater(t, len(result.MissingFields), 0, "Should detect missing fields even with type mismatch")
|
assert.Greater(t, len(result.MissingFields), 0, "Should detect missing fields with type mismatch")
|
||||||
|
|
||||||
// Verify user's type-mismatched values are preserved (not overwritten)
|
// Verify that type-mismatched values are overwritten with new structure (config evolution)
|
||||||
assert.Equal(t, "simple_string", k.String("user.settings"), "User's string value should be preserved")
|
// This is important for software upgrades where config structure changes
|
||||||
assert.Equal(t, int64(123), k.Int64("newSection"), "User's number value should be preserved")
|
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
|
// TestConfigMigrator_ComplexNested tests complex nested structure migration
|
||||||
|
|||||||
@@ -109,9 +109,6 @@ func (cs *ConfigService) initConfig() error {
|
|||||||
return cs.createDefaultConfig()
|
return cs.createDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查并自动迁移配置
|
|
||||||
cs.checkConfigMigration()
|
|
||||||
|
|
||||||
// 配置文件存在,直接加载现有配置
|
// 配置文件存在,直接加载现有配置
|
||||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||||
@@ -121,8 +118,8 @@ func (cs *ConfigService) initConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkConfigMigration 检查配置迁移
|
// MigrateConfig 执行配置迁移
|
||||||
func (cs *ConfigService) checkConfigMigration() error {
|
func (cs *ConfigService) MigrateConfig() error {
|
||||||
if cs.configMigrator == nil {
|
if cs.configMigrator == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,7 +251,6 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
|
|||||||
|
|
||||||
// 检查是否有可用更新
|
// 检查是否有可用更新
|
||||||
if !primaryRelease.GreaterThan(s.config.Updates.Version) {
|
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()
|
result.LatestVersion = primaryRelease.Version()
|
||||||
return result, nil
|
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)
|
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
|
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)
|
err = updater.UpdateTo(ctx, release, exe)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user