// Package restore implementiert BAK-03: dokumentiertes, wiederholbares // Restore-Verfahren für Datenbank (BAK-01) und Objekt-Storage (BAK-02). // Enthält NUR die gemeinsame Ablauflogik (Atomarität über Temp-Verzeichnis, // Protokollierung) — die eigentliche Wiederherstellung bleibt in den // jeweiligen Paketen (backup.Restore, objectbackup.Restore), damit // internal/restore nicht weiß, WIE eine Sicherung gelesen wird, nur WANN // und WIE SICHER sie an ihren Zielort darf. package restore import ( "encoding/json" "fmt" "os" "path/filepath" "time" ) // Kind unterscheidet die beiden Restore-Arten im Protokoll. type Kind string const ( KindDatabase Kind = "database" KindObjects Kind = "objects" ) // LogEntry ist EIN Protokolleintrag (Akzeptanzkriterium 3: Quelle, // Zeitpunkt, Ergebnis). type LogEntry struct { Timestamp time.Time `json:"timestamp"` Kind Kind `json:"kind"` Source string `json:"source"` // Generation-ID oder Snapshot-ID Target string `json:"target"` Result string `json:"result"` // "ok", "abgebrochen: ...", "fehlgeschlagen: ..." } // ErrTargetNotEmpty wird zurückgegeben, wenn targetDir bereits Inhalt hat // und force nicht gesetzt ist — Akzeptanzkriterium 2: bestehender Inhalt // bleibt unangetastet, solange der Vorgang nicht bestätigt wird. var ErrTargetNotEmpty = fmt.Errorf("restore: zielverzeichnis ist nicht leer, restore ohne bestätigung (force) abgebrochen") // AtomicRestore führt restoreFn gegen ein FRISCHES temporäres Verzeichnis // aus (niemals direkt gegen target) und übernimmt es erst bei Erfolg — // entweder wenn target leer ist, oder wenn force=true (bewusste // Bestätigung, bestehenden Inhalt zu überschreiben). Bei jedem Fehler // oder abgelehnter Bestätigung bleibt target garantiert unverändert // (Akzeptanzkriterium 2), das temporäre Verzeichnis wird aufgeräumt. // Jeder Aufruf – erfolgreich oder nicht – erzeugt genau einen // Protokolleintrag in logPath (Akzeptanzkriterium 3). func AtomicRestore(kind Kind, source, target string, force bool, logPath string, restoreFn func(tempDir string) error) (LogEntry, error) { entry := LogEntry{Timestamp: time.Now().UTC(), Kind: kind, Source: source, Target: target} empty, err := dirIsEmptyOrMissing(target) if err != nil { entry.Result = "fehlgeschlagen: " + err.Error() _ = appendLog(logPath, entry) return entry, err } if !empty && !force { entry.Result = "abgebrochen: " + ErrTargetNotEmpty.Error() _ = appendLog(logPath, entry) return entry, ErrTargetNotEmpty } parent := filepath.Dir(filepath.Clean(target)) tempDir, err := os.MkdirTemp(parent, ".restore-tmp-*") if err != nil { entry.Result = "fehlgeschlagen: temp-verzeichnis anlegen: " + err.Error() _ = appendLog(logPath, entry) return entry, fmt.Errorf("restore: temp-verzeichnis anlegen: %w", err) } defer func() { _ = os.RemoveAll(tempDir) }() // no-op nach erfolgreichem Rename (Verzeichnis existiert dann nicht mehr) if err := restoreFn(tempDir); err != nil { entry.Result = "fehlgeschlagen: " + err.Error() _ = appendLog(logPath, entry) return entry, fmt.Errorf("restore: wiederherstellung: %w", err) } if !empty { // force=true, bewusste Bestätigung: alter Inhalt wird ERST JETZT // entfernt, nachdem restoreFn bereits erfolgreich in tempDir // abgeschlossen hat — ein Fehlschlag oben hätte target nie berührt. if err := os.RemoveAll(target); err != nil { entry.Result = "fehlgeschlagen: altes ziel entfernen: " + err.Error() _ = appendLog(logPath, entry) return entry, fmt.Errorf("restore: altes ziel entfernen: %w", err) } } else if err := os.MkdirAll(parent, 0o750); err != nil { entry.Result = "fehlgeschlagen: übergeordnetes verzeichnis anlegen: " + err.Error() _ = appendLog(logPath, entry) return entry, fmt.Errorf("restore: übergeordnetes verzeichnis anlegen: %w", err) } if err := os.Rename(tempDir, target); err != nil { entry.Result = "fehlgeschlagen: umbenennen: " + err.Error() _ = appendLog(logPath, entry) return entry, fmt.Errorf("restore: temp-verzeichnis nach ziel umbenennen: %w", err) } entry.Result = "ok" if err := appendLog(logPath, entry); err != nil { return entry, fmt.Errorf("restore: protokoll schreiben: %w", err) } return entry, nil } func dirIsEmptyOrMissing(dir string) (bool, error) { entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return true, nil } return false, fmt.Errorf("zielverzeichnis lesen: %w", err) } return len(entries) == 0, nil } func appendLog(logPath string, entry LogEntry) error { f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return fmt.Errorf("protokolldatei öffnen: %w", err) } defer func() { _ = f.Close() }() line, err := json.Marshal(entry) if err != nil { return fmt.Errorf("protokolleintrag kodieren: %w", err) } if _, err := f.Write(append(line, '\n')); err != nil { return fmt.Errorf("protokolleintrag schreiben: %w", err) } return nil }