Scert/backend/internal/cli/sqlite_restore.go

79 lines
2.7 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 cli
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"strings"
)
// 调用方必须持有服务维护锁,并停止所有外部数据库访问。
// 不能直接替换主文件后再处理旧 WAL,否则旧事务会覆盖恢复的数据。
func prepareSQLiteRestoreTarget(ctx context.Context, path string) (err error) {
if err := ctx.Err(); err != nil {
return err
}
info, statErr := os.Lstat(path)
exists := statErr == nil
if statErr != nil && !errors.Is(statErr, os.ErrNotExist) {
return fmt.Errorf("检查 SQLite 恢复目标失败:%w", statErr)
}
if exists && !info.Mode().IsRegular() {
return errors.New("SQLite 恢复目标必须是普通文件")
}
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
sidecar, sidecarErr := os.Lstat(path + suffix)
if errors.Is(sidecarErr, os.ErrNotExist) {
continue
}
if sidecarErr != nil {
return fmt.Errorf("检查 SQLite 日志文件失败:%w", sidecarErr)
}
if !exists || !sidecar.Mode().IsRegular() {
return fmt.Errorf("SQLite 日志状态不安全,拒绝恢复:%s", suffix)
}
}
if !exists {
return nil
}
db, err := sql.Open("sqlite", path)
if err != nil {
return fmt.Errorf("打开 SQLite 恢复目标失败:%w", err)
}
db.SetMaxOpenConns(1)
defer func() { err = errors.Join(err, db.Close()) }()
// 维护窗口仍有外部事务时立即失败,不等待后继续替换文件。
if _, err := db.ExecContext(ctx, "PRAGMA busy_timeout = 0"); err != nil {
return fmt.Errorf("配置 SQLite 恢复检查失败:%w", err)
}
var busy, logged, checkpointed int
if err := db.QueryRowContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)").Scan(&busy, &logged, &checkpointed); err != nil {
return fmt.Errorf("合并 SQLite 恢复前日志失败:%w", err)
}
if busy != 0 || logged != checkpointed {
return errors.New("SQLite 仍被其他连接占用,拒绝恢复;请停止所有数据库访问")
}
// 由 SQLite 自身完成恢复及日志模式切换,不手动删除日志文件。
var mode string
if err := db.QueryRowContext(ctx, "PRAGMA journal_mode = DELETE").Scan(&mode); err != nil {
return fmt.Errorf("关闭 SQLite 恢复前日志失败,请停止所有数据库访问:%w", err)
}
if !strings.EqualFold(mode, "delete") {
return fmt.Errorf("SQLite 恢复前日志模式未切换完成:%s", mode)
}
if err := db.Close(); err != nil {
return fmt.Errorf("关闭 SQLite 恢复目标失败:%w", err)
}
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
if _, err := os.Lstat(path + suffix); !errors.Is(err, os.ErrNotExist) {
if err != nil {
return fmt.Errorf("复核 SQLite 日志文件失败:%w", err)
}
return fmt.Errorf("SQLite 日志仍存在,拒绝替换数据库:%s", suffix)
}
}
return ctx.Err()
}