package repository import ( "fmt" "os" "path/filepath" ) // Dateirechte innerhalb eines Repositorys. // // Backup-Daten gehen niemandem außer dem Dienst etwas an; die Rechte sind // deshalb so eng wie möglich gewählt (PROMPT.md §2.1, Least Privilege). const ( // dataFilePermissions gelten für Chunks, Manifeste und den Katalog. dataFilePermissions os.FileMode = 0o600 // immutableFilePermissions gelten für Daten unter Retention Lock. // // **Sie verhindern kein Löschen.** Unter POSIX hängt das Entfernen einer // Datei am Schreibrecht ihres *Verzeichnisses*, nicht an ihren eigenen // Rechten; real gemessen in `MeasureEnforcement`. Was sie verhindern, ist // das versehentliche Überschreiben durch einen schreibenden Open. // // Den Löschschutz leistet das Unveränderlich-Kennzeichen des Dateisystems // (siehe immutability.go) — und auch das nur gegenüber dem Dienstbenutzer, // nicht gegenüber der Systemverwaltung. immutableFilePermissions os.FileMode = 0o400 // directoryPermissions gelten für alle Verzeichnisse des Repositorys. directoryPermissions os.FileMode = 0o700 ) // writeFileAtomically schreibt eine Datei so, dass sie niemals halb sichtbar wird. // // Der Ablauf ist für die Absturzsicherheit entscheidend (PROMPT.md §129: // Datenintegrität vor Geschwindigkeit): // // 1. In eine temporäre Datei im Zielverzeichnis schreiben. // 2. fsync auf die Datei — erst danach liegen die Daten wirklich auf dem Datenträger. // 3. rename auf den Zielnamen — unter POSIX ein atomarer Vorgang. // 4. fsync auf das Verzeichnis — erst danach überlebt auch der Namenseintrag // einen Stromausfall. // // Ohne Schritt 2 und 4 könnte nach einem Absturz eine Datei existieren, deren // Inhalt leer oder unvollständig ist — ein beschädigtes Backup, das sich als // vollständig ausgibt. func writeFileAtomically(targetPath string, fileContent []byte, filePermissions os.FileMode) error { targetDirectory := filepath.Dir(targetPath) // Die temporäre Datei liegt im Zielverzeichnis, damit rename nicht über eine // Dateisystemgrenze läuft — sonst wäre er nicht atomar. temporaryFile, createError := os.CreateTemp(targetDirectory, ".tmp-*") if createError != nil { return fmt.Errorf("die temporäre datei konnte nicht angelegt werden: %w", createError) } temporaryPath := temporaryFile.Name() // Bei jedem Fehler bleibt keine Bruchstückdatei zurück. removeTemporaryFile := true defer func() { if removeTemporaryFile { _ = temporaryFile.Close() _ = os.Remove(temporaryPath) } }() if _, writeError := temporaryFile.Write(fileContent); writeError != nil { return fmt.Errorf("die daten konnten nicht geschrieben werden: %w", writeError) } // Die Rechte werden vor dem Sichtbarwerden gesetzt. if chmodError := temporaryFile.Chmod(filePermissions); chmodError != nil { return fmt.Errorf("die dateirechte konnten nicht gesetzt werden: %w", chmodError) } // Schritt 2: Ohne fsync bestätigt das Betriebssystem den Schreibvorgang, // obwohl die Daten nur im Zwischenspeicher liegen. if syncError := temporaryFile.Sync(); syncError != nil { return fmt.Errorf("die daten konnten nicht dauerhaft gesichert werden: %w", syncError) } if closeError := temporaryFile.Close(); closeError != nil { return fmt.Errorf("die temporäre datei konnte nicht geschlossen werden: %w", closeError) } // Schritt 3: Der Umbenennungsvorgang macht die fertige Datei in einem Zug sichtbar. if renameError := os.Rename(temporaryPath, targetPath); renameError != nil { return fmt.Errorf("die datei konnte nicht an ihren platz gebracht werden: %w", renameError) } removeTemporaryFile = false // Schritt 4: Der Verzeichniseintrag selbst muss ebenfalls gesichert werden. return syncDirectory(targetDirectory) } // syncDirectory sichert die Verzeichniseinträge eines Verzeichnisses. // // Ohne diesen Schritt kann nach einem Stromausfall der Dateiinhalt vorhanden, // der Name aber verschwunden sein. func syncDirectory(directoryPath string) error { directoryHandle, openError := os.Open(directoryPath) if openError != nil { return fmt.Errorf("das verzeichnis konnte nicht geöffnet werden: %w", openError) } defer func() { _ = directoryHandle.Close() }() if syncError := directoryHandle.Sync(); syncError != nil { // Einige Dateisysteme lehnen fsync auf Verzeichnissen ab. Das ist kein // Fehler der Anwendung, wird aber weitergereicht, damit der Aufrufer // entscheiden kann. return fmt.Errorf("das verzeichnis konnte nicht dauerhaft gesichert werden: %w", syncError) } return nil } // ensureDirectory legt ein Verzeichnis samt Elternverzeichnissen an. func ensureDirectory(directoryPath string) error { if makeError := os.MkdirAll(directoryPath, directoryPermissions); makeError != nil { return fmt.Errorf("das verzeichnis %q konnte nicht angelegt werden: %w", directoryPath, makeError) } return nil }