// Package reconcile implementiert BAK-05: periodischer Abgleich, ob jeder // in der Datenbank referenzierte Objekt-Storage-Eintrag tatsächlich // existiert und umgekehrt. Prüft AUSSCHLIESSLICH Existenz — niemals // Inhalt (das ist Archive BAK-08, eine eigene Fehlerklasse, bewusst nicht // hier mit hineingezogen, siehe reconcile_test.go // TestReconcile_ExistingButCorruptedObjectProducesNoFinding). package reconcile import ( "sort" "time" ) // Finding ist EIN Abweichungsfund — entweder ein Datenbankeintrag ohne // Storage-Objekt oder umgekehrt. type Finding struct { StorageKey string `json:"storage_key"` DocumentID string `json:"document_id,omitempty"` RevisionID string `json:"revision_id,omitempty"` } // Report ist das Ergebnis EINES Abgleichslaufs (Akzeptanzkriterium 3: // Abweichungen werden BERICHTET, nicht automatisch behoben — Report ist // reine Information, keine Reparaturfunktion existiert in diesem Paket). // // Beide Listen sind nach StorageKey aufsteigend sortiert — bei gleicher // Eingabe liefert Reconcile IMMER dieselbe Reihenfolge (deterministisch), // damit ein nachgelagerter Verbraucher (Archive BAK-08: zieht seine // Stichprobe aus der Liste der EXISTIERENDEN Objekte) sich auf eine // stabile Sortierung verlassen kann, statt bei jedem Lauf neu zu // filtern/sortieren. type Report struct { GeneratedAt time.Time `json:"generated_at"` // MissingInStorage: Datenbankeintrag vorhanden, Objekt im Storage fehlt // (Akzeptanzkriterium 1). MissingInStorage []Finding `json:"missing_in_storage"` // OrphanedInStorage: Objekt im Storage vorhanden, kein Datenbankeintrag // (Akzeptanzkriterium 2). OrphanedInStorage []Finding `json:"orphaned_in_storage"` // ExistingInStorage: Datenbankeintrag UND Storage-Objekt beide // vorhanden — reine Existenzbestätigung, KEINE Inhaltsprüfung. Dient // Archive BAK-08 als stabile, deterministisch sortierte // Stichprobengrundlage (nach StorageKey aufsteigend, siehe Report- // Dokumentation oben) — BAK-08 muss dafür selbst nicht mehr // sortieren/filtern. ExistingInStorage []Finding `json:"existing_in_storage"` } // IsClean liefert true, wenn der Lauf keine Abweichungen fand (Pflicht- // prüfung 3: "Lauf ohne Abweichungen liefert einen leeren, eindeutig als // sauber erkennbaren Bericht" — IsClean ist genau dieses eindeutige // Erkennungsmerkmal, statt dass ein Aufrufer beide Listen selbst auf // Leere prüfen muss). func (r Report) IsClean() bool { return len(r.MissingInStorage) == 0 && len(r.OrphanedInStorage) == 0 } // DBEntry ist ein Datenbankeintrag, wie ihn ListDBStorageKeys liefert. type DBEntry struct { StorageKey string DocumentID string RevisionID string } // Reconcile vergleicht dbEntries (aus file_revisions.storage_key, DMS // FDN-02) gegen storageKeys (tatsächlich im Objekt-Storage vorhandene // Schlüssel, z.B. per Verzeichnis-Walk des FDN-03-LocalDriver- // Basisverzeichnisses) und liefert die Abweichungen in beide Richtungen. // Reine Funktion — kein Datenbank-/Storage-Zugriff hier, dadurch ohne // echte Infrastruktur testbar (siehe reconcile_test.go). func Reconcile(dbEntries []DBEntry, storageKeys []string) Report { storageSet := make(map[string]bool, len(storageKeys)) for _, k := range storageKeys { storageSet[k] = true } dbSet := make(map[string]DBEntry, len(dbEntries)) for _, e := range dbEntries { dbSet[e.StorageKey] = e } var missing, existing []Finding for _, e := range dbEntries { if !storageSet[e.StorageKey] { missing = append(missing, Finding(e)) } else { existing = append(existing, Finding(e)) } } var orphaned []Finding for _, k := range storageKeys { if _, ok := dbSet[k]; !ok { orphaned = append(orphaned, Finding{StorageKey: k}) } } sort.Slice(missing, func(i, j int) bool { return missing[i].StorageKey < missing[j].StorageKey }) sort.Slice(orphaned, func(i, j int) bool { return orphaned[i].StorageKey < orphaned[j].StorageKey }) sort.Slice(existing, func(i, j int) bool { return existing[i].StorageKey < existing[j].StorageKey }) return Report{ GeneratedAt: time.Now().UTC(), MissingInStorage: missing, OrphanedInStorage: orphaned, ExistingInStorage: existing, } }