返回 DeepSeek-Reasonix
backup.go
根目录 / internal / topicstate / backup.go
1 package topicstate
2
3 import (
4 "context"
5 "database/sql"
6 "fmt"
7 "net/url"
8 "os"
9 "path/filepath"
10
11 "reasonix/internal/sqliteuri"
12 )
13
14 // BackupExisting reads an existing database without running schema migrations.
15 // VACUUM INTO captures a consistent SQLite snapshot including committed WAL
16 // pages and unknown tables/columns. The destination must not already exist.
17 func BackupExisting(ctx context.Context, path, destination string) error {
18 info, err := os.Lstat(path)
19 if err != nil {
20 return err
21 }
22 if !info.Mode().IsRegular() {
23 return fmt.Errorf("topic state is not a regular file")
24 }
25 if _, err := os.Lstat(destination); err == nil {
26 return &os.PathError{Op: "backup", Path: destination, Err: os.ErrExist}
27 } else if !os.IsNotExist(err) {
28 return err
29 }
30 q := url.Values{}
31 q.Set("mode", "ro")
32 q.Add("_pragma", "busy_timeout(5000)")
33 dsn, err := sqliteuri.Disk(path, q)
34 if err != nil {
35 return err
36 }
37 db, err := sql.Open("sqlite", dsn)
38 if err != nil {
39 return err
40 }
41 defer db.Close()
42 // Build the snapshot privately and publish it without replacement. VACUUM
43 // itself accepts existing empty files, and a preflight existence check alone
44 // cannot protect a destination created while the snapshot is being built.
45 parent, err := filepath.Abs(filepath.Dir(destination))
46 if err != nil {
47 return err
48 }
49 tmp, err := os.MkdirTemp(parent, ".topic-backup-")
50 if err != nil {
51 return err
52 }
53 defer os.RemoveAll(tmp)
54 snapshot := filepath.Join(tmp, "snapshot.sqlite")
55 if _, err := db.ExecContext(ctx, "VACUUM INTO ?", snapshot); err != nil {
56 return err
57 }
58 if err := os.Chmod(snapshot, 0o600); err != nil {
59 return err
60 }
61 return os.Link(snapshot, destination)
62 }
63
63 lines GO