From 3d5c94e55210bdfa6beb1ac383d6dd835309f355 Mon Sep 17 00:00:00 2001 From: smail Date: Wed, 16 Sep 2026 20:29:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A6=96=E6=AC=A1=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=AE=8C=E6=88=90=E5=90=8E=E6=9C=AA=E5=90=AF=E7=94=A8?= =?UTF-8?q?=E5=88=86=E5=8C=BA=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/migrate.go | 8 +++++ backend/config/setup_finalize.go | 30 ++++++++++++++++ backend/services/setup/finish.go | 2 +- backend/services/setup/finish_test.go | 49 +++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 backend/config/setup_finalize.go diff --git a/backend/config/migrate.go b/backend/config/migrate.go index 5ecaa9d..658ef79 100644 --- a/backend/config/migrate.go +++ b/backend/config/migrate.go @@ -149,6 +149,10 @@ func PlanLegacyMigration(root string, _ RuntimeValidationOptions) (LegacyMigrati return plan, nil } func MigrateLegacyConfig(root string, options RuntimeValidationOptions) (LegacyMigrationResult, error) { + return migrateLegacyConfig(root, options, false) +} + +func migrateLegacyConfig(root string, options RuntimeValidationOptions, completeSetup bool) (LegacyMigrationResult, error) { plan, err := PlanLegacyMigration(root, options) if err != nil { return LegacyMigrationResult{}, err @@ -168,6 +172,10 @@ func MigrateLegacyConfig(root string, options RuntimeValidationOptions) (LegacyM if err := json.Unmarshal(legacyData, &legacy); err != nil { return LegacyMigrationResult{}, err } + if completeSetup { + legacy.SetupState = SetupStateReady + legacy.IsInit = true + } runtime, manualFields, err := legacyRuntimeConfig(root, legacy) if err != nil { return LegacyMigrationResult{}, err diff --git a/backend/config/setup_finalize.go b/backend/config/setup_finalize.go new file mode 100644 index 0000000..72bd73e --- /dev/null +++ b/backend/config/setup_finalize.go @@ -0,0 +1,30 @@ +package config + +// FinalizeSetupConfig 先保存可重试的安装状态,再以分区根文件发布就绪状态。 +// 只由完成安装的流程调用,不自动迁移已有的运行实例。 +func FinalizeSetupConfig(cfg *Config) error { + legacyConfigMu.Lock() + defer legacyConfigMu.Unlock() + if cfg == nil { + return ErrInvalidRuntimeConfig + } + + pending := *cfg + pending.SetupState = SetupStateBootstrap + pending.IsInit = false + if err := writeConfig(&pending); err != nil { + return err + } + if _, err := migrateLegacyConfig(ROOT_PATH, RuntimeValidationOptions{ + HasSuperAdmin: true, + Production: !DevelopmentMode, + }, true); err != nil { + return err + } + loaded, err := loadPartitionedCompatibility() + if err != nil { + return err + } + PublishConfigSnapshot(loaded) + return nil +} diff --git a/backend/services/setup/finish.go b/backend/services/setup/finish.go index 021b891..a7c40a9 100644 --- a/backend/services/setup/finish.go +++ b/backend/services/setup/finish.go @@ -40,7 +40,7 @@ func Finish() error { cfg.LoginPolicy = "new_device" } - err = config.WriteConfig(cfg) + err = config.FinalizeSetupConfig(cfg) if err != nil { return errors.Wrap(err) } diff --git a/backend/services/setup/finish_test.go b/backend/services/setup/finish_test.go index 10c31c0..b858245 100644 --- a/backend/services/setup/finish_test.go +++ b/backend/services/setup/finish_test.go @@ -20,6 +20,10 @@ func TestFinishPersistsServicePorts(t *testing.T) { if err := config.WriteConfig(&config.Config{ SetupState: config.SetupStateBootstrap, HttpsEnabled: 1, + Domain: "example.com", + WebDomain: "mail.example.com", + DbType: config.DBTypeSQLite, + DbDSN: filepath.Join(config.ROOT_PATH, "data", "smail.db"), }); err != nil { t.Fatal(err) } @@ -40,6 +44,14 @@ func TestFinishPersistsServicePorts(t *testing.T) { if !loaded.VerificationEnabled || !loaded.ExternalRecoveryEnabled || loaded.LoginPolicy != "new_device" { t.Fatalf("security defaults = verification %t, recovery %t, policy %q", loaded.VerificationEnabled, loaded.ExternalRecoveryEnabled, loaded.LoginPolicy) } + for _, name := range []string{"config.json", "email.json", "smtp.json", "ssl.json", "plugins.json"} { + if _, err := os.Stat(filepath.Join(config.ROOT_PATH, "config", name)); err != nil { + t.Fatalf("partition %s: %v", name, err) + } + } + if err := Finish(); err != nil { + t.Fatalf("repeated completion failed: %v", err) + } } func TestFinishPreservesExplicitlyDisabledSecurityOptions(t *testing.T) { @@ -75,6 +87,10 @@ func TestFinishRepairsReadyConfigWithMissingPorts(t *testing.T) { SetupState: config.SetupStateReady, IsInit: true, HttpsEnabled: 1, + Domain: "example.com", + WebDomain: "mail.example.com", + DbType: config.DBTypeSQLite, + DbDSN: filepath.Join(config.ROOT_PATH, "data", "smail.db"), }); err != nil { t.Fatal(err) } @@ -101,6 +117,10 @@ func TestFinishKeepsInternalHTTPSDisabledInProxyMode(t *testing.T) { TLSMode: config.HTTPSTLSModeProxy, HttpsPort: 9443, HttpsEnabled: 1, + Domain: "example.com", + WebDomain: "mail.example.com", + DbType: config.DBTypeSQLite, + DbDSN: filepath.Join(config.ROOT_PATH, "data", "smail.db"), }); err != nil { t.Fatal(err) } @@ -117,6 +137,35 @@ func TestFinishKeepsInternalHTTPSDisabledInProxyMode(t *testing.T) { } } +func TestFinishRejectsInvalidConfigBeforeReady(t *testing.T) { + prepareFinishConfigTest(t) + if err := config.WriteConfig(&config.Config{ + SetupState: config.SetupStateBootstrap, + DbType: config.DBTypeSQLite, + DbDSN: filepath.Join(config.ROOT_PATH, "data", "smail.db"), + }); err != nil { + t.Fatal(err) + } + if err := Finish(); err == nil { + t.Fatal("missing domains must prevent setup completion") + } + loaded, err := config.ReadConfig() + if err != nil { + t.Fatal(err) + } + if loaded.IsSetupReady() || loaded.IsInit || config.IsSetupReady() { + t.Fatal("failed completion published ready state") + } + loaded.Domain = "example.com" + loaded.WebDomain = "mail.example.com" + if err := config.WriteConfig(loaded); err != nil { + t.Fatal(err) + } + if err := Finish(); err != nil { + t.Fatalf("corrected setup cannot retry: %v", err) + } +} + func prepareFinishConfigTest(t *testing.T) { t.Helper() previousRoot := config.ROOT_PATH