feat(archive): BAK-03 Restore-Verfahren fuer Datenbank und Objekt-Storage
internal/restore: Atomarer Restore ueber Temp-Verzeichnis + Rename, nicht-leeres Ziel ohne -force bricht VOR jeder Beruehrung ab, JSONL- Protokoll jedes Laufs. Drei reale Defekte beim Bau gefunden und behoben: pg_combinebackup braucht Plain- statt Tar-Format (Extraktionsschritt ergaenzt), pg_wal.tar.gz wurde nie verifiziert/wiederhergestellt (BAK-01s Verify jetzt erweitert), Go-exec haengt bei pg_ctl start wegen vererbter Pipes (Testfix: echte Logdatei statt CombinedOutput). Beide Restore-Pfade real auf 131 ueber restore-cli nachgewiesen, inkl. echtem Postgres-Start aus wiederhergestelltem Verzeichnis.
This commit is contained in:
@@ -13,13 +13,23 @@ import (
|
||||
// (Akzeptanzkriterium 2: Verifikation, nicht nur Erstellungs-Prüfung).
|
||||
var ErrCorrupted = fmt.Errorf("backup: sicherungsdatei ist beschaedigt oder unvollstaendig")
|
||||
|
||||
// Verify prüft, dass base.tar.gz im gegebenen Sicherungsverzeichnis
|
||||
// vollständig lesbar ist — öffnet gzip- UND tar-Stream und liest JEDEN
|
||||
// Eintrag bis zum Ende durch (nicht nur die Kopfdaten), damit ein
|
||||
// abgeschnittener oder mit kaputten Bytes überschriebener Inhalt
|
||||
// zuverlässig auffällt, nicht nur ein defekter Tar-Header.
|
||||
// Verify prüft, dass base.tar.gz UND pg_wal.tar.gz im gegebenen
|
||||
// Sicherungsverzeichnis vollständig lesbar sind — öffnet gzip- UND
|
||||
// tar-Stream und liest JEDEN Eintrag bis zum Ende durch (nicht nur die
|
||||
// Kopfdaten), damit ein abgeschnittener oder mit kaputten Bytes
|
||||
// überschriebener Inhalt zuverlässig auffällt, nicht nur ein defekter
|
||||
// Tar-Header. pg_wal.tar.gz wird geprüft, weil ohne intaktes WAL kein
|
||||
// crash-konsistenter Restore möglich ist (siehe Restore/WalTarGzFile) —
|
||||
// eine Sicherung mit beschädigtem WAL-Archiv wäre sonst unbemerkt
|
||||
// unbrauchbar.
|
||||
func Verify(backupDir string) error {
|
||||
path := filepath.Join(backupDir, BaseTarGzFile)
|
||||
if err := verifyTarGz(filepath.Join(backupDir, BaseTarGzFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
return verifyTarGz(filepath.Join(backupDir, WalTarGzFile))
|
||||
}
|
||||
|
||||
func verifyTarGz(path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s nicht lesbar: %v", ErrCorrupted, path, err)
|
||||
@@ -28,7 +38,7 @@ func Verify(backupDir string) error {
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: gzip-header ungueltig: %v", ErrCorrupted, err)
|
||||
return fmt.Errorf("%w: gzip-header von %s ungueltig: %v", ErrCorrupted, path, err)
|
||||
}
|
||||
defer func() { _ = gz.Close() }()
|
||||
|
||||
@@ -40,15 +50,69 @@ func Verify(backupDir string) error {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: tar-eintrag ungueltig: %v", ErrCorrupted, err)
|
||||
return fmt.Errorf("%w: tar-eintrag in %s ungueltig: %v", ErrCorrupted, path, err)
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, tr); err != nil {
|
||||
return fmt.Errorf("%w: inhalt von %q nicht vollstaendig lesbar: %v", ErrCorrupted, hdr.Name, err)
|
||||
return fmt.Errorf("%w: inhalt von %q in %s nicht vollstaendig lesbar: %v", ErrCorrupted, hdr.Name, path, err)
|
||||
}
|
||||
entries++
|
||||
}
|
||||
if entries == 0 {
|
||||
return fmt.Errorf("%w: archiv enthaelt keine eintraege", ErrCorrupted)
|
||||
return fmt.Errorf("%w: %s enthaelt keine eintraege", ErrCorrupted, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractTarGz entpackt tarGzPath vollständig nach destDir — genutzt von
|
||||
// Restore, um die TAR+GZIP-Sicherungsstufen (siehe Verify) in das
|
||||
// PLAIN-Format zu überführen, das pg_combinebackup als Eingabe erwartet.
|
||||
func extractTarGz(tarGzPath, destDir string) error {
|
||||
f, err := os.Open(tarGzPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s öffnen: %w", tarGzPath, err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gzip-header ungueltig: %w", err)
|
||||
}
|
||||
defer func() { _ = gz.Close() }()
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("tar-eintrag lesen: %w", err)
|
||||
}
|
||||
target := filepath.Join(destDir, filepath.Clean(hdr.Name))
|
||||
switch hdr.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, 0o750); err != nil {
|
||||
return fmt.Errorf("verzeichnis %q anlegen: %w", target, err)
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
|
||||
return fmt.Errorf("übergeordnetes verzeichnis von %q anlegen: %w", target, err)
|
||||
}
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode))
|
||||
if err != nil {
|
||||
return fmt.Errorf("datei %q anlegen: %w", target, err)
|
||||
}
|
||||
if _, err := io.Copy(out, tr); err != nil {
|
||||
_ = out.Close()
|
||||
return fmt.Errorf("datei %q schreiben: %w", target, err)
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
return fmt.Errorf("datei %q schliessen: %w", target, err)
|
||||
}
|
||||
default:
|
||||
// Symlinks/Sonderdateien: pg_basebackup-Archive enthalten
|
||||
// praktisch keine, übersprungen statt Restore abzubrechen.
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user