修复分区配置初始化写入流程
Some checks are pending
Security Checks / security (push) Waiting to run

将安装向导写入、域名设置和安装完成流程统一接入分区配置事务,保留旧版配置覆盖保护。
This commit is contained in:
smail 2026-09-29 21:36:53 +08:00
parent eb50d062b1
commit 84c267472b
6 changed files with 205 additions and 7 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}
}