Smail/backend/services/dbmigration/target.go
smail 33b4258fbb
Some checks are pending
Security Checks / security (push) Waiting to run
修复SQLite嵌套路径迁移校验
2026-09-06 09:01:14 +08:00

86 lines
3.0 KiB
Go

package dbmigration
import (
"context"
"strings"
"time"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/utils/credential"
)
// TargetInput 只接受结构化连接字段,不接受任意 DSN、SQL 或密码密文。
type TargetInput struct {
Type config.DatabaseType `json:"type"`
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
Username string `json:"username,omitempty"`
Name string `json:"name,omitempty"`
LocalRef string `json:"localRef,omitempty"`
Password *config.SecretInput `json:"password,omitempty"`
}
func (input TargetInput) Prepare(root string) (config.DatabaseConfig, error) {
target := config.NormalizeDatabaseConfig(config.DatabaseConfig{
Type: input.Type, Host: strings.TrimSpace(input.Host), Port: input.Port,
Username: strings.TrimSpace(input.Username), Name: strings.TrimSpace(input.Name),
LocalRef: strings.TrimSpace(input.LocalRef),
})
if target.Type == config.DatabaseTypeSQLite {
if input.Password != nil {
return config.DatabaseConfig{}, ErrInvalidRequest
}
} else if target.Type.Valid() {
vault, err := credential.NewVault(root)
if err != nil {
return config.DatabaseConfig{}, config.ErrSecretUnavailable
}
secret, err := config.ApplySecretChange(vault, config.StoredSecret{}, config.SecretChange{
Operation: config.SecretReplace, Value: input.Password,
}, config.SecretPolicy{Required: true, MaxLength: 4096}, time.Now().UTC())
if err != nil {
return config.DatabaseConfig{}, ErrInvalidRequest
}
target.Password = secret
}
if err := config.ValidateDatabaseConfig(target); err != nil {
return config.DatabaseConfig{}, ErrInvalidRequest
}
return target, nil
}
func databaseIdentity(database config.DatabaseConfig) DatabaseIdentity {
return DatabaseIdentity{Type: database.Type, Host: database.Host, Port: database.Port,
Username: database.Username, Name: database.Name, LocalRef: database.LocalRef}
}
func buildTargetPlan(source, target config.DatabaseConfig) (Plan, error) {
source = config.NormalizeDatabaseConfig(source)
target = config.NormalizeDatabaseConfig(target)
if err := config.ValidateDatabaseConfig(source); err != nil {
return Plan{}, ErrSourceDatabase
}
if err := config.ValidateDatabaseConfig(target); err != nil {
return Plan{}, ErrInvalidRequest
}
if target.Type == config.DatabaseTypeSQLite {
if !validSQLiteReference(target.LocalRef) {
return Plan{}, ErrInvalidTargetName
}
} else if !targetNamePattern.MatchString(target.Name) {
return Plan{}, ErrInvalidTargetName
}
if config.SameDatabaseLocation(source, target) {
return Plan{}, ErrInvalidTargetName
}
return Plan{Source: databaseIdentity(source), Target: databaseIdentity(target), sourceConfig: source, targetConfig: target}, nil
}
// Preview 不创建目标库,只验证源库可读、目标服务可连接且名称未占用。
func (e *DatabaseExecutor) Preview(ctx context.Context, plan Plan) error {
if e.portable != nil {
return e.portable.Preview(ctx, plan)
}
return nil
}