package repository import ( "encoding/json" "fmt" "time" ) // CatalogVersion ist die Version des Katalogformats. const CatalogVersion = 1 // CatalogEntry beschreibt ein Backup im Katalog. // // Der Eintrag enthält bewusst nur, was für eine Übersicht nötig ist. Alle // Einzelheiten stehen im Manifest — der Katalog bleibt dadurch klein und // schnell ladbar (PROMPT.md §47). type CatalogEntry struct { // BackupID ist die Kennung des Backups. BackupID string `json:"backup_id"` // ChainID verbindet das Backup mit seiner Kette. ChainID string `json:"chain_id"` // ParentBackupID benennt das Elternbackup einer Zusatzsicherung. ParentBackupID string `json:"parent_backup_id,omitempty"` // BackupType ist die Art des Backups. BackupType BackupType `json:"backup_type"` // ConsistencyLevel beschreibt die Konsistenz der Daten. ConsistencyLevel ConsistencyLevel `json:"consistency_level"` // SourceType benennt die Art der Quelle. SourceType string `json:"source_type"` // SourceID ist die Kennung der Quelle. SourceID string `json:"source_id"` // SourceName ist der sprechende Name der Quelle. SourceName string `json:"source_name"` // StartedAt ist der Beginn des Backups in UTC. StartedAt time.Time `json:"started_at"` // CompletedAt ist der Abschluss des Backups in UTC. CompletedAt time.Time `json:"completed_at"` // LogicalBytes ist die Menge der gesicherten Ursprungsdaten. LogicalBytes int64 `json:"logical_bytes"` // StoredBytes ist die tatsächlich abgelegte Datenmenge. StoredBytes int64 `json:"stored_bytes"` // ChunkCount ist die Zahl der Chunk-Verweise des Backups. ChunkCount int64 `json:"chunk_count"` // EntryCount ist die Zahl der gesicherten Objekte. EntryCount int `json:"entry_count"` // ImmutableUntil ist das Ende der Aufbewahrungspflicht in UTC. ImmutableUntil *time.Time `json:"immutable_until,omitempty"` // SelfContainedRestore meldet ein Manifest, das ohne seine Kette ausreicht. SelfContainedRestore bool `json:"self_contained_restore,omitempty"` // ManifestHash ist die Prüfsumme des zugehörigen Manifests. // // Sie erlaubt es festzustellen, ob ein Manifest nach der Aufnahme in den // Katalog verändert wurde. ManifestHash string `json:"manifest_hash"` } // Catalog ist die Übersicht aller Backups eines Repositorys. // // Der Katalog ist ausdrücklich nur ein Beschleuniger. Verbindlich sind die // Manifeste; geht der Katalog verloren oder ist er beschädigt, wird er allein // aus ihnen wiederhergestellt (PROMPT.md §46). type Catalog struct { // CatalogVersion ist die Version des Katalogformats. CatalogVersion int `json:"catalog_version"` // RepositoryID benennt das zugehörige Repository. // // Sie verhindert, dass ein versehentlich kopierter Katalog zu einem fremden // Repository gehört und dort falsche Angaben macht. RepositoryID string `json:"repository_id"` // Entries sind die Backups, nach Abschlusszeitpunkt absteigend sortiert. Entries []CatalogEntry `json:"entries"` // GeneratedAt ist der Zeitpunkt der letzten Aktualisierung in UTC. GeneratedAt time.Time `json:"generated_at"` // RebuiltFromManifests meldet, dass der Katalog durch einen Scan entstand. RebuiltFromManifests bool `json:"rebuilt_from_manifests"` } // FindByBackupID sucht einen Eintrag anhand seiner Backup-Kennung. func (catalog *Catalog) FindByBackupID(backupID string) (CatalogEntry, bool) { for _, catalogEntry := range catalog.Entries { if catalogEntry.BackupID == backupID { return catalogEntry, true } } return CatalogEntry{}, false } // FindByChainID liefert alle Backups einer Kette, älteste zuerst. // // Die Reihenfolge ist für die Wiederherstellung wesentlich: eine Zusatzsicherung // lässt sich nur zusammen mit ihren Vorgängern zurückspielen. func (catalog *Catalog) FindByChainID(chainID string) []CatalogEntry { chainEntries := make([]CatalogEntry, 0) for _, catalogEntry := range catalog.Entries { if catalogEntry.ChainID == chainID { chainEntries = append(chainEntries, catalogEntry) } } // Der Katalog ist absteigend sortiert; für eine Kette wird aufsteigend benötigt. for leftIndex, rightIndex := 0, len(chainEntries)-1; leftIndex < rightIndex; leftIndex, rightIndex = leftIndex+1, rightIndex-1 { chainEntries[leftIndex], chainEntries[rightIndex] = chainEntries[rightIndex], chainEntries[leftIndex] } return chainEntries } // ValidateChain prüft, ob eine Backup-Kette lückenlos ist (PROMPT.md §84). // // Fehlt ein Glied, sind alle darauf folgenden Wiederherstellungspunkte // unbrauchbar. Das muss eindeutig benannt werden, statt eine scheinbar // vollständige Liste anzuzeigen. func (catalog *Catalog) ValidateChain(chainID string) ChainValidationResult { chainEntries := catalog.FindByChainID(chainID) if len(chainEntries) == 0 { return ChainValidationResult{ ChainID: chainID, IsValid: false, Message: "Zu dieser Kette existiert kein Backup.", } } // Die vorhandenen Kennungen erlauben es, fehlende Eltern zu erkennen. availableBackupIDs := make(map[string]struct{}, len(chainEntries)) for _, chainEntry := range chainEntries { availableBackupIDs[chainEntry.BackupID] = struct{}{} } var brokenEntries []string var affectedEntries []string for _, chainEntry := range chainEntries { // Eine Vollsicherung braucht kein Elternbackup. if chainEntry.ParentBackupID == "" { continue } if _, parentExists := availableBackupIDs[chainEntry.ParentBackupID]; !parentExists { brokenEntries = append(brokenEntries, chainEntry.BackupID) } } if len(brokenEntries) == 0 { return ChainValidationResult{ ChainID: chainID, IsValid: true, BackupCount: len(chainEntries), Message: "Die Kette ist vollständig.", } } // Von einem fehlenden Glied an sind alle nachfolgenden Punkte betroffen. for _, chainEntry := range chainEntries { for _, brokenBackupID := range brokenEntries { if chainEntry.BackupID == brokenBackupID { affectedEntries = append(affectedEntries, chainEntry.BackupID) } } } return ChainValidationResult{ ChainID: chainID, IsValid: false, BackupCount: len(chainEntries), BrokenBackupIDs: brokenEntries, AffectedBackupIDs: affectedEntries, Message: fmt.Sprintf("Die Kette ist unterbrochen: %d Wiederherstellungspunkte sind nicht verwendbar.", len(affectedEntries)), } } // ChainValidationResult ist das Ergebnis einer Kettenprüfung. type ChainValidationResult struct { // ChainID benennt die geprüfte Kette. ChainID string `json:"chain_id"` // IsValid meldet, ob die Kette lückenlos ist. IsValid bool `json:"is_valid"` // BackupCount ist die Zahl der Backups in der Kette. BackupCount int `json:"backup_count"` // BrokenBackupIDs sind Backups, deren Elternbackup fehlt. BrokenBackupIDs []string `json:"broken_backup_ids,omitempty"` // AffectedBackupIDs sind alle dadurch unbrauchbaren Wiederherstellungspunkte. AffectedBackupIDs []string `json:"affected_backup_ids,omitempty"` // Message erklärt das Ergebnis verständlich. Message string `json:"message"` } // catalogEntryFromManifest bildet einen Katalogeintrag aus einem Manifest. func catalogEntryFromManifest(manifest *Manifest) CatalogEntry { return CatalogEntry{ BackupID: manifest.BackupID, ChainID: manifest.ChainID, ParentBackupID: manifest.ParentBackupID, BackupType: manifest.BackupType, ConsistencyLevel: manifest.ConsistencyLevel, SourceType: manifest.Source.SourceType, SourceID: manifest.Source.SourceID, SourceName: manifest.Source.SourceName, StartedAt: manifest.StartedAt, CompletedAt: manifest.CompletedAt, LogicalBytes: manifest.Statistics.LogicalBytes, StoredBytes: manifest.Statistics.StoredBytes, ChunkCount: manifest.TotalChunkCount(), EntryCount: len(manifest.Entries), ImmutableUntil: manifest.ImmutableUntil, SelfContainedRestore: manifest.SelfContainedRestore, ManifestHash: manifest.ContentHash, } } // encodeCatalog serialisiert einen Katalog. func encodeCatalog(catalog *Catalog) ([]byte, error) { encodedCatalog, marshalError := json.MarshalIndent(catalog, "", " ") if marshalError != nil { return nil, fmt.Errorf("der katalog konnte nicht erzeugt werden: %w", marshalError) } return append(encodedCatalog, '\n'), nil } // decodeCatalog liest einen Katalog. func decodeCatalog(rawCatalog []byte) (*Catalog, error) { var catalog Catalog if unmarshalError := json.Unmarshal(rawCatalog, &catalog); unmarshalError != nil { // Ein unlesbarer Katalog ist kein Datenverlust: er lässt sich aus den // Manifesten neu aufbauen. return nil, fmt.Errorf("der katalog ist unlesbar: %w", unmarshalError) } return &catalog, nil }