Smail/backend/controllers/database_migration.go
smail f68c367042
Some checks are pending
Security Checks / security (push) Waiting to run
实现三种数据库迁移并完善认证与迁移写入保护
2026-09-06 01:31:29 +08:00

123 lines
4.1 KiB
Go

package controllers
import (
"net/http"
"strings"
"time"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/dto/response"
"git.pcux.cn/xuans/Smail/services/access"
"git.pcux.cn/xuans/Smail/services/dbmigration"
appcontext "git.pcux.cn/xuans/Smail/utils/context"
)
type databaseMigrationPreviewRequest struct {
TargetName string `json:"targetName"`
Target *dbmigration.TargetInput `json:"target,omitempty"`
ExpectedGeneration uint64 `json:"expectedGeneration"`
}
type databaseMigrationExecuteRequest struct {
ID string `json:"id"`
Token string `json:"token"`
}
// PreviewDatabaseMigration 验证三种数据库的结构化目标连接,不创建或修改目标库。
func PreviewDatabaseMigration(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
if err := authorizeCapability(ctx, access.CapabilityDatabaseMigrate, "", false); err != nil {
writeConfigAccessError(w, err)
return
}
manager, ok := configManagerOrError(w)
if !ok {
return
}
service := dbmigration.Current()
if service == nil {
writeConfigError(w, dbmigration.ErrExecutorUnavailable)
return
}
var input databaseMigrationPreviewRequest
if err := decodeJSONRequest(w, req, &input); err != nil || input.ExpectedGeneration == 0 {
writeConfigError(w, dbmigration.ErrInvalidRequest)
return
}
status := manager.Status()
if input.ExpectedGeneration != status.ActiveGeneration || len(status.PendingRestart) > 0 {
writeConfigError(w, config.ErrGenerationConflict)
return
}
var record dbmigration.Record
var token string
var err error
if input.Target != nil {
if input.TargetName != "" {
writeConfigError(w, dbmigration.ErrInvalidRequest)
return
}
target, prepareErr := input.Target.Prepare(config.ROOT_PATH)
if prepareErr != nil {
writeConfigError(w, prepareErr)
return
}
record, token, err = service.PreviewTarget(req.Context(), manager.Snapshot().Root.Database, status.ActiveGeneration, target)
} else {
record, token, err = service.Preview(req.Context(), manager.Snapshot().Root.Database, status.ActiveGeneration, input.TargetName)
}
if err != nil {
writeConfigError(w, err)
return
}
response.NewSuccessResponse(map[string]any{"migration": record, "token": token}).FPrint(w)
}
// ExecuteDatabaseMigration 执行预览票据对应的固定迁移计划,不接受 DSN、SQL 或连接参数。
func ExecuteDatabaseMigration(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
if err := authorizeCapability(ctx, access.CapabilityDatabaseMigrate, "", false); err != nil {
writeConfigAccessError(w, err)
return
}
if dbmigration.Current() == nil {
writeConfigError(w, dbmigration.ErrExecutorUnavailable)
return
}
var input databaseMigrationExecuteRequest
if err := decodeJSONRequest(w, req, &input); err != nil {
writeConfigError(w, dbmigration.ErrInvalidRequest)
return
}
// 迁移最长十分钟,不能沿用普通接口的九十秒响应写入期限。
_ = http.NewResponseController(w).SetWriteDeadline(time.Now().Add(11 * time.Minute))
record, err := dbmigration.Current().ExecuteAuthorized(req.Context(), input.ID, input.Token, configActor(ctx, req))
if err != nil {
writeConfigError(w, err)
return
}
response.NewSuccessResponse(record).FPrint(w)
}
// GetDatabaseMigration 返回迁移状态,不返回密码、密钥或底层数据库错误。
func GetDatabaseMigration(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
if err := authorizeCapability(ctx, access.CapabilityDatabaseMigrate, "", false); err != nil {
writeConfigAccessError(w, err)
return
}
service := dbmigration.Current()
if service == nil {
writeConfigError(w, dbmigration.ErrExecutorUnavailable)
return
}
parts := strings.Split(strings.Trim(req.URL.Path, "/"), "/")
if len(parts) != 5 || parts[0] != "api" || parts[1] != "v1" || parts[2] != "database" || parts[3] != "migrations" || strings.TrimSpace(parts[4]) == "" {
writeConfigError(w, dbmigration.ErrInvalidRequest)
return
}
record, err := service.Get(parts[4])
if err != nil {
writeConfigError(w, err)
return
}
response.NewSuccessResponse(record).FPrint(w)
}