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.
90 lines
2.6 KiB
Go
90 lines
2.6 KiB
Go
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
|
|
}
|