feat(archive): BAK-07 gestaffelte Aufbewahrungsfrist fuer Backup-Snapshots
internal/backup.PruneTiered: reine GFS-Funktion (Tag/Woche/Monat) fuer Datenbank-Generationen, behaelt strukturell immer die neueste Generation (Sicherheitsnetz gegen Legal-Hold-Kollision). internal/objectbackup. PruneTiered: duenner Wrapper um restics native --keep-daily/-weekly/ -monthly-Staffelung. Beide CLIs nutzen die Staffelung, wenn konfiguriert, bleiben sonst abwaertskompatibel zur flachen "letzte N"-Regel. Real gegen zeitversetzt erzeugte restic-Snapshots getestet (Fund: restics --time-Flag erwartet eigenes Format, nicht RFC3339), Pruning-Sicherheit nach dem Loeschen alter Snapshots ueber vollstaendigen Restore + restic check --read-data bewiesen. Beide Rotationswege real ueber systemd auf 131 ausgeloest.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RetentionPolicy ist BAK-07s gestaffelte Aufbewahrungsfrist für
|
||||
// Datenbank-Sicherungsgenerationen — unabhängig von RET-02s
|
||||
// Dokumenten-Retention (Akzeptanzkriterium 1). Ein Feldwert 0 bedeutet
|
||||
// "diese Stufe nicht anwenden", dieselbe Konvention wie
|
||||
// objectbackup.RetentionPolicy (BAK-02), damit beide Backup-Arten
|
||||
// gleich konfiguriert werden.
|
||||
type RetentionPolicy struct {
|
||||
KeepDaily int
|
||||
KeepWeekly int
|
||||
KeepMonthly int
|
||||
}
|
||||
|
||||
// PruneTiered wählt aus generations (IDs im NewGenerationID-Format) die
|
||||
// zu BEHALTENDEN nach gestaffelter GFS-Regel: je Granularität
|
||||
// (Tag/Woche/Monat) wird pro Zeitfenster die NEUESTE Generation
|
||||
// behalten, bis die konfigurierte Anzahl Fenster erreicht ist — reine
|
||||
// Funktion, keine Dateisystem-/Netzwerkzugriffe, dadurch ohne echte
|
||||
// Infrastruktur testbar (Rotate in rotate.go führt die tatsächliche
|
||||
// Löschung anhand des Ergebnisses aus).
|
||||
//
|
||||
// Sicherheitsnetz für Akzeptanzkriterium 3 ("mindestens ein aktueller
|
||||
// Snapshot bleibt erhalten"): die NEUESTE Generation wird IMMER
|
||||
// behalten, unabhängig von der konfigurierten Staffelung — selbst bei
|
||||
// versehentlich auf 0 gesetzter Policy geht nie die letzte Sicherung
|
||||
// verloren.
|
||||
func PruneTiered(generations []string, policy RetentionPolicy) (keep, remove []string) {
|
||||
type parsed struct {
|
||||
id string
|
||||
t time.Time
|
||||
}
|
||||
var items []parsed
|
||||
for _, id := range generations {
|
||||
t, err := time.Parse("20060102T150405Z", id)
|
||||
if err != nil {
|
||||
continue // unbekanntes Format wird ignoriert, nicht geloescht
|
||||
}
|
||||
items = append(items, parsed{id: id, t: t})
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].t.After(items[j].t) }) // neueste zuerst
|
||||
|
||||
keepSet := make(map[string]bool)
|
||||
if len(items) > 0 {
|
||||
keepSet[items[0].id] = true // Sicherheitsnetz: immer die neueste
|
||||
}
|
||||
|
||||
applyTier := func(bucketKey func(time.Time) string, n int) {
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
count := 0
|
||||
for _, it := range items {
|
||||
if count >= n {
|
||||
break
|
||||
}
|
||||
key := bucketKey(it.t)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
count++
|
||||
keepSet[it.id] = true
|
||||
}
|
||||
}
|
||||
|
||||
applyTier(func(t time.Time) string { return t.Format("2006-01-02") }, policy.KeepDaily)
|
||||
applyTier(func(t time.Time) string {
|
||||
y, w := t.ISOWeek()
|
||||
return fmt.Sprintf("%04d-W%02d", y, w)
|
||||
}, policy.KeepWeekly)
|
||||
applyTier(func(t time.Time) string { return t.Format("2006-01") }, policy.KeepMonthly)
|
||||
|
||||
for _, it := range items {
|
||||
if keepSet[it.id] {
|
||||
keep = append(keep, it.id)
|
||||
} else {
|
||||
remove = append(remove, it.id)
|
||||
}
|
||||
}
|
||||
return keep, remove
|
||||
}
|
||||
Reference in New Issue
Block a user