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.
77 lines
2.3 KiB
Go
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
|
|
}
|