feat(backup): "keep every backup" option — timestamped files per exit

Sub-option of "back up on every exit": when on, backup.Run/RunADIF stamp the
filename with the time (opslog-YYYY-MM-DD-HHMMSS.db) so each run is a distinct
file instead of overwriting the day's snapshot. Rotation still trims to the
newest N (raise Rotation for a longer history). Threaded as a `unique` flag
through runConfiguredBackup/backupLogADIF from BackupSettings.KeepAll
(keyBackupKeepAll); UI sub-checkbox shown only when EveryExit is on.
This commit is contained in:
2026-08-07 12:28:52 +02:00
parent e9feeffdc3
commit 949cc17ed1
5 changed files with 50 additions and 22 deletions
+16 -7
View File
@@ -23,10 +23,10 @@ import (
// every time we add a field).
type Settings struct {
Enabled bool `json:"enabled"`
Folder string `json:"folder"` // empty → DefaultFolder
Rotation int `json:"rotation"` // how many backups to keep; 0/neg = 5
Zip bool `json:"zip"` // compress with deflate
LastBackupAt string `json:"last_backup_at"`// RFC3339; empty if never
Folder string `json:"folder"` // empty → DefaultFolder
Rotation int `json:"rotation"` // how many backups to keep; 0/neg = 5
Zip bool `json:"zip"` // compress with deflate
LastBackupAt string `json:"last_backup_at"` // RFC3339; empty if never
}
// DefaultFolder returns the folder used when Settings.Folder is empty.
@@ -44,7 +44,7 @@ func DefaultFolder(dataDir string) string {
// statement (no torn-copy window while the app keeps writing), and compacts
// the destination as a bonus. It replaces the old "checkpoint + raw io.Copy",
// which could capture a half-written page during a concurrent write.
func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation int, doZip bool, prefix string) (string, error) {
func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation int, doZip bool, prefix string, unique bool) (string, error) {
if dbConn == nil {
return "", fmt.Errorf("nil db connection")
}
@@ -61,7 +61,12 @@ func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation in
return "", fmt.Errorf("create backup folder: %w", err)
}
// One file per DAY by default (same-day runs overwrite). With `unique`, add the
// time so every run is a distinct file — rotation still keeps the newest N.
stamp := time.Now().Format("2006-01-02")
if unique {
stamp = time.Now().Format("2006-01-02-150405")
}
base := fmt.Sprintf("%s-%s", prefix, stamp)
// VACUUM INTO requires a non-existent target → use a temp file, then
@@ -106,7 +111,7 @@ func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation in
// log lives on a shared MySQL server, where VACUUM INTO can't snapshot it — an
// ADIF export is a portable, re-importable backup of the actual contacts.
// writeADIF must write the full ADIF to the path it's handed.
func RunADIF(folder string, rotation int, doZip bool, writeADIF func(path string) error) (string, error) {
func RunADIF(folder string, rotation int, doZip bool, unique bool, writeADIF func(path string) error) (string, error) {
if rotation <= 0 {
rotation = 5
}
@@ -116,7 +121,11 @@ func RunADIF(folder string, rotation int, doZip bool, writeADIF func(path string
if err := os.MkdirAll(folder, 0o755); err != nil {
return "", fmt.Errorf("create backup folder: %w", err)
}
base := "opslog-log-" + time.Now().Format("2006-01-02")
stamp := time.Now().Format("2006-01-02")
if unique {
stamp = time.Now().Format("2006-01-02-150405")
}
base := "opslog-log-" + stamp
tmp := filepath.Join(folder, base+".adi.tmp")
_ = os.Remove(tmp)
if err := writeADIF(tmp); err != nil {