Smail/backend/config/setup_writer.go
smail 0a7167eac0
Some checks are pending
Security Checks / security (push) Waiting to run
修复初始化分区配置代次同步
同步初始化候选配置的分区 generation,避免数据库设置提交被分区代次校验拒绝,并补充回归测试。
2026-09-29 22:54:17 +08:00

180 lines
5.7 KiB
Go

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()
setCandidateGeneration(&candidate, candidate.Generation)
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
}