package backup import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "path/filepath" ) // ErrCorrupted wird geliefert, wenn eine Sicherungsdatei nicht lesbar ist // (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 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 { 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) } defer func() { _ = f.Close() }() gz, err := gzip.NewReader(f) if err != nil { return fmt.Errorf("%w: gzip-header von %s ungueltig: %v", ErrCorrupted, path, err) } defer func() { _ = gz.Close() }() tr := tar.NewReader(gz) entries := 0 for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { 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 in %s nicht vollstaendig lesbar: %v", ErrCorrupted, hdr.Name, path, err) } entries++ } if entries == 0 { 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 }