From 84c267472b385520a764c6550c35a892c6bc1542 Mon Sep 17 00:00:00 2001 From: smail Date: Tue, 29 Sep 2026 21:36:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=86=E5=8C=BA=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=88=9D=E5=A7=8B=E5=8C=96=E5=86=99=E5=85=A5=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将安装向导写入、域名设置和安装完成流程统一接入分区配置事务,保留旧版配置覆盖保护。 --- backend/config/partitioned_compatibility.go | 8 +- backend/config/setup_finalize.go | 18 ++ backend/config/setup_writer.go | 178 ++++++++++++++++++++ backend/services/setup/db.go | 4 +- backend/services/setup/domain.go | 2 +- backend/services/setup/finish.go | 2 +- 6 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 backend/config/setup_writer.go diff --git a/backend/config/partitioned_compatibility.go b/backend/config/partitioned_compatibility.go index c8f62d6..220e87c 100644 --- a/backend/config/partitioned_compatibility.go +++ b/backend/config/partitioned_compatibility.go @@ -26,9 +26,11 @@ func isPartitionedConfig(data []byte) (bool, error) { // loadPartitionedCompatibility 仅构造旧业务所需的只读快照,不重写分区配置。 func loadPartitionedCompatibility() (*Config, error) { - runtime, err := NewConfigLoader(ROOT_PATH, ConfigLoaderOptions{ - Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: !DevelopmentMode}, - }).Load() + return loadPartitionedCompatibilityWithOptions(RuntimeValidationOptions{HasSuperAdmin: true, Production: !DevelopmentMode}) +} + +func loadPartitionedCompatibilityWithOptions(validation RuntimeValidationOptions) (*Config, error) { + runtime, err := NewConfigLoader(ROOT_PATH, ConfigLoaderOptions{Validation: validation}).Load() if err != nil { return nil, err } diff --git a/backend/config/setup_finalize.go b/backend/config/setup_finalize.go index 72bd73e..f6244f4 100644 --- a/backend/config/setup_finalize.go +++ b/backend/config/setup_finalize.go @@ -8,6 +8,17 @@ func FinalizeSetupConfig(cfg *Config) error { if cfg == nil { return ErrInvalidRuntimeConfig } + data, err := readConfigFile("config.json") + if err != nil { + return err + } + partitioned, err := isPartitionedConfig(data) + if err != nil { + return err + } + if partitioned { + return finalizePartitionedSetupConfig(cfg) + } pending := *cfg pending.SetupState = SetupStateBootstrap @@ -28,3 +39,10 @@ func FinalizeSetupConfig(cfg *Config) error { PublishConfigSnapshot(loaded) return nil } + +func finalizePartitionedSetupConfig(cfg *Config) error { + data := *cfg + data.SetupState = SetupStateReady + data.IsInit = true + return writeSetupConfigUnlocked(&data) +} diff --git a/backend/config/setup_writer.go b/backend/config/setup_writer.go new file mode 100644 index 0000000..afedecb --- /dev/null +++ b/backend/config/setup_writer.go @@ -0,0 +1,178 @@ +package config + +import ( + "fmt" + "strings" + "time" + + "git.pcux.cn/xuans/Smail/utils/credential" +) + +// WriteSetupConfig 将安装向导使用的兼容配置安全提交到分区配置。 +// 运行配置已经分区化时,安装流程不得直接覆盖 config.json。 +func WriteSetupConfig(cfg *Config) error { + legacyConfigMu.Lock() + defer legacyConfigMu.Unlock() + return writeSetupConfigUnlocked(cfg) +} + +func writeSetupConfigUnlocked(cfg *Config) error { + if cfg == nil { + return ErrInvalidRuntimeConfig + } + + data, err := readConfigFile("config.json") + if err != nil { + return err + } + partitioned, err := isPartitionedConfig(data) + if err != nil { + return err + } + if !partitioned { + return writeConfig(cfg) + } + + current, err := loadSetupRuntime() + if err != nil { + return err + } + candidate, err := setupRuntimeFromConfig(current, cfg) + if err != nil { + return err + } + validation := RuntimeValidationOptions{ + HasSuperAdmin: candidate.SetupState == SetupStateReady, + Production: !DevelopmentMode, + } + if err := NewTransactionStore(ROOT_PATH, validation).Commit(current, candidate); err != nil { + return err + } + loaded, err := loadPartitionedCompatibilityWithOptions(validation) + if err != nil { + return err + } + PublishConfigSnapshot(loaded) + return nil +} + +func loadSetupRuntime() (RuntimeConfig, error) { + production := !DevelopmentMode + var lastErr error + for _, hasSuperAdmin := range []bool{false, true} { + runtime, err := NewConfigLoader(ROOT_PATH, ConfigLoaderOptions{ + Validation: RuntimeValidationOptions{HasSuperAdmin: hasSuperAdmin, Production: production}, + }).Load() + if err == nil { + return runtime, nil + } + lastErr = err + } + return RuntimeConfig{}, lastErr +} + +func setupRuntimeFromConfig(current RuntimeConfig, cfg *Config) (RuntimeConfig, error) { + vault, err := credential.NewVault(ROOT_PATH) + if err != nil { + return RuntimeConfig{}, err + } + legacy := legacyMigrationConfigFromConfig(cfg) + database, manualFields, err := legacyDatabaseConfig(ROOT_PATH, vault, legacy) + if err != nil { + return RuntimeConfig{}, err + } + if len(manualFields) != 0 { + return RuntimeConfig{}, fmt.Errorf("安装配置无法安全转换:%s", strings.Join(manualFields, ", ")) + } + relay, manualFields, err := legacySMTPConfig(ROOT_PATH, vault, legacy) + if err != nil { + return RuntimeConfig{}, err + } + if len(manualFields) != 0 { + return RuntimeConfig{}, fmt.Errorf("安装配置无法安全转换:%s", strings.Join(manualFields, ", ")) + } + ssl, manualFields := legacySSLConfig(legacy) + if len(manualFields) != 0 { + return RuntimeConfig{}, fmt.Errorf("安装配置无法安全转换:%s", strings.Join(manualFields, ", ")) + } + + candidate := cloneRuntimeConfig(current) + candidate.Generation = current.Generation + 1 + candidate.SetupState = cfg.SetupState + if !candidate.SetupState.Valid() { + candidate.SetupState = SetupStateBootstrap + } + candidate.Root.Generation = candidate.Generation + candidate.Root.SetupState = candidate.SetupState + candidate.Root.LogLevel = cfg.LogLevel + candidate.Root.Database = database + candidate.Root.Domains = DomainConfig{ + Primary: cfg.Domain, + Accepted: append([]string(nil), cfg.Domains...), + Web: cfg.WebDomain, + AdminRecovery: cfg.AdminRecoveryDomain, + AccessHosts: append([]string(nil), cfg.AccessHosts...), + } + httpsEnabled := cfg.HttpsEnabled != 2 + httpsPort := cfg.HttpsPort + tlsMode := strings.ToLower(strings.TrimSpace(cfg.TLSMode)) + if tlsMode == HTTPSTLSModeProxy { + httpsEnabled = false + httpsPort = 0 + } + candidate.Root.HTTP = HTTPConfig{ + Port: cfg.HttpPort, + HTTPSPort: httpsPort, + PublicHTTPSPort: cfg.PublicHTTPSPort, + HTTPSEnabled: httpsEnabled, + TLSMode: tlsMode, + TrustedProxyCIDRs: append([]string(nil), cfg.TrustedProxyCIDRs...), + } + candidate.SMTP.Relay = relay + candidate.SMTP.Relay.VerificationEnabled = cfg.VerificationEnabled + candidate.SMTP.Relay.LoginPolicy = cfg.LoginPolicy + candidate.Email.Policy.SpamFilterLevel = cfg.SpamFilterLevel + candidate.Email.Policy.ExternalRecoveryEnabled = cfg.ExternalRecoveryEnabled + candidate.SSL.TLS = ssl + candidate.Root.UpdatedAt = time.Now().UTC() + return candidate, nil +} + +func legacyMigrationConfigFromConfig(cfg *Config) legacyMigrationConfig { + return legacyMigrationConfig{ + LogLevel: cfg.LogLevel, + Domain: cfg.Domain, + Domains: append([]string(nil), cfg.Domains...), + WebDomain: cfg.WebDomain, + AdminRecoveryDomain: cfg.AdminRecoveryDomain, + AccessHosts: append([]string(nil), cfg.AccessHosts...), + SSLType: cfg.SSLType, + SSLPrivateKeyPath: cfg.SSLPrivateKeyPath, + SSLPublicKeyPath: cfg.SSLPublicKeyPath, + SSL: cfg.SSL, + DbDSN: cfg.DbDSN, + DbType: cfg.DbType, + HttpsEnabled: cfg.HttpsEnabled, + HttpPort: cfg.HttpPort, + HttpsPort: cfg.HttpsPort, + PublicHTTPSPort: cfg.PublicHTTPSPort, + TLSMode: cfg.TLSMode, + TrustedProxyCIDRs: append([]string(nil), cfg.TrustedProxyCIDRs...), + SMTPHost: cfg.SMTPHost, + SMTPPort: cfg.SMTPPort, + SMTPUsername: cfg.SMTPUsername, + SMTPPassword: cfg.SMTPPassword, + SMTPSecurity: cfg.SMTPSecurity, + SMTPFrom: cfg.SMTPFrom, + SMTPFromDomain: cfg.SMTPFromDomain, + VerificationEnabled: boolPointer(cfg.VerificationEnabled), + ExternalRecoveryEnabled: boolPointer(cfg.ExternalRecoveryEnabled), + LoginPolicy: cfg.LoginPolicy, + SetupState: cfg.SetupState, + IsInit: cfg.IsInit, + } +} + +func boolPointer(value bool) *bool { + return &value +} diff --git a/backend/services/setup/db.go b/backend/services/setup/db.go index fedf1d6..93cce1d 100644 --- a/backend/services/setup/db.go +++ b/backend/services/setup/db.go @@ -286,7 +286,7 @@ func SetDatabaseSettingsWithOptions(ctx *context.Context, settings DatabaseSetti markSetupInProgress(&candidateConfig) candidateConfig.DbType = dbType candidateConfig.DbDSN = dbDSN - if err = config.WriteConfig(&candidateConfig); err != nil { + if err = config.WriteSetupConfig(&candidateConfig); err != nil { return errors.Wrap(err) } if err = config.Init(); err != nil { @@ -685,7 +685,7 @@ func restoreDatabaseConfig(previous *config.Config) { return } closeDatabaseInstance() - if err := config.WriteConfig(previous); err != nil { + if err := config.WriteSetupConfig(previous); err != nil { return } _ = config.Init() diff --git a/backend/services/setup/domain.go b/backend/services/setup/domain.go index 173d01b..abf68bb 100644 --- a/backend/services/setup/domain.go +++ b/backend/services/setup/domain.go @@ -63,7 +63,7 @@ func setDomainSettings(smtpDomain, webDomain, multiDomains string, publicHTTPSPo // 检查域名是否指向本机 todo - err = config.WriteConfig(configData) + err = config.WriteSetupConfig(configData) if err != nil { return errors.Wrap(err) } diff --git a/backend/services/setup/finish.go b/backend/services/setup/finish.go index a7c40a9..dc322cb 100644 --- a/backend/services/setup/finish.go +++ b/backend/services/setup/finish.go @@ -25,7 +25,7 @@ func Finish() error { portsChanged := applyServicePorts(cfg) if cfg.IsSetupReady() { if portsChanged { - if err = config.WriteConfig(cfg); err != nil { + if err = config.WriteSetupConfig(cfg); err != nil { return errors.Wrap(err) } }