Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.8 KiB
Go
125 lines
4.8 KiB
Go
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
|
|
}
|