feat: add DB backup with consistent BoltDB snapshots
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
40
store/backup.go
Normal file
40
store/backup.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// Backup creates a consistent snapshot of the database in destDir.
|
||||
// Returns the path to the backup file.
|
||||
func (d *DB) Backup(destDir string) (string, error) {
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("create backup dir: %w", err)
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
filename := fmt.Sprintf("catacombs-%s.db", timestamp)
|
||||
destPath := filepath.Join(destDir, filename)
|
||||
|
||||
f, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create backup file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// BoltDB View transaction provides a consistent snapshot
|
||||
err = d.db.View(func(tx *bolt.Tx) error {
|
||||
_, err := tx.WriteTo(f)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
os.Remove(destPath)
|
||||
return "", fmt.Errorf("backup write: %w", err)
|
||||
}
|
||||
|
||||
return destPath, nil
|
||||
}
|
||||
68
store/backup_test.go
Normal file
68
store/backup_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
db, err := Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := db.SaveProfile("fp1", "player1"); err != nil {
|
||||
t.Fatalf("failed to save profile: %v", err)
|
||||
}
|
||||
|
||||
backupDir := filepath.Join(tmpDir, "backups")
|
||||
|
||||
backupPath, err := db.Backup(backupDir)
|
||||
if err != nil {
|
||||
t.Fatalf("backup failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
||||
t.Fatal("backup file does not exist")
|
||||
}
|
||||
|
||||
base := filepath.Base(backupPath)
|
||||
if !strings.HasPrefix(base, "catacombs-") || !strings.HasSuffix(base, ".db") {
|
||||
t.Fatalf("unexpected backup filename: %s", base)
|
||||
}
|
||||
|
||||
backupDB, err := Open(backupPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open backup: %v", err)
|
||||
}
|
||||
defer backupDB.Close()
|
||||
|
||||
name, err := backupDB.GetProfile("fp1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read from backup: %v", err)
|
||||
}
|
||||
if name != "player1" {
|
||||
t.Fatalf("expected player1, got %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupCreatesDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
db, err := Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
backupDir := filepath.Join(tmpDir, "nested", "backups")
|
||||
_, err = db.Backup(backupDir)
|
||||
if err != nil {
|
||||
t.Fatalf("backup with nested dir failed: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user