backup.go
⎇
Raw
1package database
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "log/slog"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "time"
13)
14
15// Backup schreibt eine konsistente Kopie der laufenden Datenbank (VACUUM INTO).
16// Ein einfaches cp der .db-Datei reicht im WAL-Modus NICHT – Daten liegen ggf. noch in -wal.
17func Backup(ctx context.Context, db *sql.DB, dir string) (string, error) {
18 if err := os.MkdirAll(dir, 0o700); err != nil {
19 return "", err
20 }
21 name := filepath.Join(dir, "physiobase-"+time.Now().Format("2006-01-02_150405")+".db")
22 if _, err := db.ExecContext(ctx, `VACUUM INTO ?`, name); err != nil {
23 os.Remove(name)
24 return "", fmt.Errorf("backup: %w", err)
25 }
26 if err := os.Chmod(name, 0o600); err != nil {
27 return name, fmt.Errorf("backup rechte: %w", err)
28 }
29 return name, nil
30}
31
32// Prune behält die neuesten keep Backups.
33func Prune(dir string, keep int) error {
34 files, err := filepath.Glob(filepath.Join(dir, "physiobase-*.db"))
35 if err != nil {
36 return err
37 }
38 sort.Strings(files) // Zeitstempel im Namen → chronologisch
39 for i := 0; i < len(files)-keep; i++ {
40 if strings.HasPrefix(filepath.Base(files[i]), "physiobase-") {
41 os.Remove(files[i])
42 }
43 }
44 return nil
45}
46
47// RunBackups: sofort beim Start, danach alle 24 h, bis ctx endet.
48func RunBackups(ctx context.Context, db *sql.DB, dir string, keep int, log *slog.Logger) {
49 if keep <= 0 {
50 log.Info("automatische backups deaktiviert")
51 return
52 }
53 run := func() {
54 name, err := Backup(ctx, db, dir)
55 if err != nil {
56 log.Error("backup fehlgeschlagen", "err", err)
57 return
58 }
59 if err := Prune(dir, keep); err != nil {
60 log.Warn("alte backups nicht gelöscht", "err", err)
61 }
62 log.Info("backup erstellt", "datei", name)
63 }
64 run()
65 t := time.NewTicker(24 * time.Hour)
66 defer t.Stop()
67 for {
68 select {
69 case <-ctx.Done():
70 return
71 case <-t.C:
72 run()
73 }
74 }
75}
76