Deterministischer Abgleich zwischen DMS file_revisions und Objekt-Storage-Verzeichnis, existenz-only (keine Inhaltspruefung, saubere Abgrenzung zu BAK-08). Report sortiert nach storage_key fuer stabile Weiterverarbeitung durch BAK-08. Systemd-Timer taeglich, real auf 192.168.1.131 verdrahtet und ausgeloest.
96 lines
3.5 KiB
Go
96 lines
3.5 KiB
Go
// 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"`
|
|
}
|
|
|
|
// 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 []Finding
|
|
for _, e := range dbEntries {
|
|
if !storageSet[e.StorageKey] {
|
|
missing = append(missing, 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 })
|
|
|
|
return Report{
|
|
GeneratedAt: time.Now().UTC(),
|
|
MissingInStorage: missing,
|
|
OrphanedInStorage: orphaned,
|
|
}
|
|
}
|