Files
nexarch/archive/internal/backup/rotate.go
T
sysops 823a14ae12 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.
2026-08-30 00:49:23 +02:00

77 lines
2.3 KiB
Go

package backup
import (
"fmt"
"os"
"path/filepath"
"sort"
)
// ListGenerations liefert alle Generation-IDs in backupDir, aufsteigend
// sortiert (die GenerationID selbst ist chronologisch sortierbar, siehe
// NewGenerationID — kein Blick auf Dateisystem-Zeitstempel nötig, die bei
// einem Restore/Kopiervorgang verändert werden könnten).
func ListGenerations(backupDir string) ([]string, error) {
entries, err := os.ReadDir(backupDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("backup: sicherungsverzeichnis lesen: %w", err)
}
var generations []string
for _, e := range entries {
if e.IsDir() {
generations = append(generations, e.Name())
}
}
sort.Strings(generations)
return generations, nil
}
// Rotate entfernt alle bis auf die `keep` NEUESTEN Generationen
// (Akzeptanzkriterium 3) — jede Generation umfasst ihre Vollsicherung UND
// alle davon abhängigen Inkremente, ein Löschen der gesamten
// Generationsverzeichnisses entfernt beides konsistent zusammen.
func Rotate(backupDir string, keep int) (removed []string, err error) {
if keep < 0 {
keep = 0
}
generations, err := ListGenerations(backupDir)
if err != nil {
return nil, err
}
if len(generations) <= keep {
return nil, nil
}
toRemove := generations[:len(generations)-keep]
for _, gen := range toRemove {
if err := os.RemoveAll(filepath.Join(backupDir, gen)); err != nil {
return removed, fmt.Errorf("backup: generation %q entfernen: %w", gen, err)
}
removed = append(removed, gen)
}
return removed, nil
}
// PruneRotate wendet BAK-07s gestaffelte RetentionPolicy (PruneTiered,
// siehe retention.go) tatsächlich an — entfernt alle nicht mehr zu
// behaltenden Generationsverzeichnisse. Automatisierte Löschung nach
// Ablauf der Frist (Akzeptanzkriterium 2), unabhängig von RET-02s
// Dokumenten-Retention.
func PruneRotate(backupDir string, policy RetentionPolicy) (removed []string, err error) {
generations, err := ListGenerations(backupDir)
if err != nil {
return nil, err
}
_, toRemove := PruneTiered(generations, policy)
for _, gen := range toRemove {
if err := os.RemoveAll(filepath.Join(backupDir, gen)); err != nil {
return removed, fmt.Errorf("backup: generation %q entfernen: %w", gen, err)
}
removed = append(removed, gen)
}
return removed, nil
}