Smail/backend/config/database_migration.go
smail 5faadcd6eb
Some checks are pending
Security Checks / security (push) Waiting to run
保存三数据库日志与三十天清理实现,继续补充验收
2026-09-08 22:30:50 +08:00

152 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package config
import (
"context"
"errors"
"strings"
"time"
)
var ErrDatabaseMigrationTarget = errors.New("database migration target is invalid")
type migrationAuditKey struct{}
// WithMigrationAuditWriter 仅供持有迁移目标连接的执行器设置迁移审计去向。
// 普通配置更新不读取该上下文值,HTTP 请求不能指定审计数据库。
func WithMigrationAuditWriter(ctx context.Context, writer ConfigAuditWriter) context.Context {
return context.WithValue(ctx, migrationAuditKey{}, writer)
}
// SwitchDatabase 提交已完成复制与校验的目标连接,保留旧库并标记重启。
// 普通配置编辑不能绕过本迁移入口切换数据库类型。
func (m *ConfigManager) SwitchDatabase(ctx context.Context, expectedGeneration uint64, target DatabaseConfig, actor AuditActor) error {
if err := contextError(ctx); err != nil {
return err
}
if m == nil || expectedGeneration == 0 {
return ErrManagerStorage
}
if actor.UserID == "" || actor.SessionID == "" {
return ErrAuthorizationRequired
}
if err := m.authorize(ctx, actor, ConfigOperationMigration, SectionRoot); err != nil {
return err
}
if err := contextError(ctx); err != nil {
return err
}
m.writeMu.Lock()
defer m.writeMu.Unlock()
m.mu.RLock()
current := cloneRuntimeConfig(m.current)
pending := m.pending != nil
m.mu.RUnlock()
if pending || expectedGeneration != current.Generation {
return ErrGenerationConflict
}
target = NormalizeDatabaseConfig(target)
if err := ValidateDatabaseConfig(target); err != nil {
return ErrDatabaseMigrationTarget
}
if SameDatabaseLocation(current.Root.Database, target) {
return ErrDatabaseMigrationTarget
}
candidate := cloneRuntimeConfig(current)
candidate.Root.Database = target
setCandidateGeneration(&candidate, current.Generation+1)
if err := ValidateRuntimeConfig(candidate, m.options); err != nil {
return err
}
if err := ValidateRuntimeSecrets(m.vault, candidate); err != nil {
return err
}
if err := m.snapshots.Validate(candidate); err != nil {
return err
}
if err := contextError(ctx); err != nil {
return err
}
if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil {
return err
}
if err := contextError(ctx); err != nil {
return err
}
if err := m.transactions.Commit(current, candidate); err != nil {
return err
}
if err := m.publishSnapshot(candidate); err != nil {
return err
}
now := time.Now().UTC()
m.mu.Lock()
m.current = cloneRuntimeConfig(candidate)
m.pending = nil
m.status = ConfigStatus{
SetupState: candidate.SetupState,
ActiveGeneration: candidate.Generation,
LastValidAt: &now,
PendingRestart: []ServiceName{ServiceDatabase},
}
m.mu.Unlock()
changedField := "root.database.name"
if target.Type == DatabaseTypeSQLite {
changedField = "root.database.localRef"
}
if !sameDatabaseConnection(current.Root.Database, target) {
changedField = "root.database"
}
event := ConfigAuditEvent{
Operation: ConfigOperationMigration,
Section: SectionRoot,
ActorID: actor.UserID,
ActorRole: actor.Role,
ActorName: actor.Name,
Changes: AuditChanges(current, candidate),
FromGeneration: current.Generation,
ToGeneration: candidate.Generation,
TargetGeneration: candidate.Generation,
ChangedFields: []string{changedField},
Result: "success",
CreatedAt: now,
}
if writer, ok := ctx.Value(migrationAuditKey{}).(ConfigAuditWriter); ok && writer != nil {
if err := writer.WriteConfigAudit(ctx, event); err != nil {
return errors.Join(ErrAuditWrite, err)
}
return nil
}
return m.writeAudit(ctx, event)
}
// SameDatabaseLocation 不把用户名或密码不同误认为不同数据库。
func SameDatabaseLocation(current, target DatabaseConfig) bool {
current = NormalizeDatabaseConfig(current)
target = NormalizeDatabaseConfig(target)
if current.Type != target.Type {
return false
}
if current.Type == DatabaseTypeSQLite {
return strings.EqualFold(current.LocalRef, target.LocalRef)
}
return strings.EqualFold(current.Host, target.Host) && current.Port == target.Port &&
strings.EqualFold(current.Name, target.Name)
}
func sameDatabaseConnection(current, target DatabaseConfig) bool {
if current.Type != target.Type ||
current.Host != target.Host ||
current.Port != target.Port ||
current.Username != target.Username ||
current.Password != target.Password {
return false
}
if current.Type == DatabaseTypeSQLite {
return true
}
return current.LocalRef == target.LocalRef
}