79 lines
2.7 KiB
Go
79 lines
2.7 KiB
Go
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()
|
||
}
|