Align with engineering standards (steps 1-5)
- Rename dist/ -> deploy/ with subdirs examples/, scripts/, systemd/ per standard repository layout - Update .gitignore: gitignore all of dist/ (build output only) - Makefile: all target is now vet->lint->test->build; add vet, proto-lint, devserver targets; CGO_ENABLED=0 for builds (modernc.org/sqlite is pure-Go, no C toolchain needed); CGO_ENABLED=1 retained for tests (race detector) - Dockerfile: builder -> golang:1.26-alpine, runtime -> alpine:3.21; drop libc6 dep; add /srv/mcias/certs and /srv/mcias/backups to image - deploy/systemd/mcias.service: add RestrictSUIDSGID=true - deploy/systemd/mcias-backup.service: new oneshot backup unit - deploy/systemd/mcias-backup.timer: daily 02:00 UTC, 5m jitter - deploy/scripts/install.sh: install backup units and enable timer; create certs/ and backups/ subdirs in /srv/mcias - buf.yaml: add proto linting config for proto-lint target - internal/db: add Snapshot and SnapshotDir methods (VACUUM INTO) - cmd/mciasdb: add snapshot subcommand; no master key required
This commit is contained in:
68
internal/db/snapshot.go
Normal file
68
internal/db/snapshot.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Snapshot creates a consistent backup of the database at destPath using
|
||||
// SQLite's VACUUM INTO statement. VACUUM INTO acquires a read lock for the
|
||||
// duration of the copy, which is safe while the server is running in WAL mode.
|
||||
// The destination file is created by SQLite; the caller must ensure the parent
|
||||
// directory exists.
|
||||
func (db *DB) Snapshot(destPath string) error {
|
||||
// VACUUM INTO is not supported on in-memory databases.
|
||||
if strings.Contains(db.path, "mode=memory") {
|
||||
return fmt.Errorf("db: snapshot not supported on in-memory databases")
|
||||
}
|
||||
if _, err := db.sql.Exec("VACUUM INTO ?", destPath); err != nil {
|
||||
return fmt.Errorf("db: snapshot VACUUM INTO %q: %w", destPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SnapshotDir creates a timestamped backup in dir and prunes backups older
|
||||
// than retainDays days. dir is created with mode 0750 if it does not exist.
|
||||
// The backup filename format is mcias-20060102-150405.db.
|
||||
func (db *DB) SnapshotDir(dir string, retainDays int) (string, error) {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return "", fmt.Errorf("db: create backup dir %q: %w", dir, err)
|
||||
}
|
||||
|
||||
ts := time.Now().UTC().Format("20060102-150405")
|
||||
dest := filepath.Join(dir, fmt.Sprintf("mcias-%s.db", ts))
|
||||
if err := db.Snapshot(dest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Prune backups older than retainDays.
|
||||
if retainDays > 0 {
|
||||
cutoff := time.Now().UTC().AddDate(0, 0, -retainDays)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
// Non-fatal: the backup was written; log pruning failure separately.
|
||||
return dest, fmt.Errorf("db: list backup dir for pruning: %w", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".db") {
|
||||
continue
|
||||
}
|
||||
// Skip the file we just wrote.
|
||||
if e.Name() == filepath.Base(dest) {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
_ = os.Remove(filepath.Join(dir, e.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
Reference in New Issue
Block a user