package repository import ( "context" "errors" "io" "time" ) // Fehler der Repository-Schicht. var ( // ErrChunkNotFound meldet einen nicht vorhandenen Chunk. // // Beim Wiederherstellen bedeutet das einen Datenverlust und ist niemals // stillschweigend zu übergehen (PROMPT.md §14). ErrChunkNotFound = errors.New("der chunk ist im repository nicht vorhanden") // ErrChunkCorrupted meldet einen Chunk, dessen Inhalt nicht zu seiner Kennung passt. ErrChunkCorrupted = errors.New("der chunk ist beschädigt: sein inhalt passt nicht zu seiner prüfsumme") // ErrInvalidChunkIdentifier meldet eine unbrauchbare Chunk-Kennung. ErrInvalidChunkIdentifier = errors.New("die chunk-kennung ist ungültig") // ErrInvalidBackupIdentifier meldet eine unbrauchbare Backup-Kennung. ErrInvalidBackupIdentifier = errors.New("die backup-kennung ist ungültig") // ErrBackupNotFound meldet ein nicht vorhandenes Backup. ErrBackupNotFound = errors.New("das backup ist im repository nicht vorhanden") // ErrManifestCorrupted meldet ein beschädigtes Manifest. ErrManifestCorrupted = errors.New("das manifest ist beschädigt") // ErrBackupIncomplete meldet ein Backup ohne gültigen Abschlussvermerk. // // Ein solches Backup gilt als unvollständig und darf niemals als // erfolgreich dargestellt werden (SYNCOVA_ARCHITECTURE.md §10). ErrBackupIncomplete = errors.New("das backup besitzt keinen gültigen abschlussvermerk und ist unvollständig") // ErrRepositoryLocked meldet ein bereits von jemand anderem beschriebenes Repository. ErrRepositoryLocked = errors.New("das repository wird bereits von einem anderen vorgang beschrieben") // ErrRetentionLocked meldet den Versuch, geschützte Daten vorzeitig zu löschen. ErrRetentionLocked = errors.New("das backup steht unter aufbewahrungsschutz und kann noch nicht gelöscht werden") // ErrLegalHold meldet den Versuch, ein für Beweiszwecke gehaltenes Backup zu löschen. // // Getrennt von ErrRetentionLocked, weil die Abhilfe eine andere ist: Eine // Frist läuft ab, ein Legal Hold muss ausdrücklich aufgehoben werden. ErrLegalHold = errors.New("das backup wird für beweiszwecke gehalten und kann nicht gelöscht werden") // ErrRetentionCannotBeShortened meldet den Versuch, einen Schutz zu verkürzen. ErrRetentionCannotBeShortened = errors.New("eine aufbewahrungsfrist lässt sich verlängern, aber niemals verkürzen") // ErrSessionClosed meldet die Verwendung einer bereits beendeten Schreibsession. ErrSessionClosed = errors.New("die schreibsession ist bereits abgeschlossen oder abgebrochen") ) // ChunkReference beschreibt einen im Repository abgelegten Chunk. type ChunkReference struct { // Identifier ist der Inhaltshash und zugleich die Kennung des Chunks. Identifier string `json:"id"` // LogicalOffset ist die Position des Chunks im ursprünglichen Datenstrom. LogicalOffset int64 `json:"offset"` // LogicalLength ist die Länge der ursprünglichen Daten in Byte. LogicalLength int64 `json:"length"` // StoredLength ist die Länge der abgelegten Daten in Byte. // // Sie weicht von LogicalLength ab, sobald Kompression oder Verschlüsselung // im Spiel sind. StoredLength int64 `json:"stored_length"` // StoredDigest ist die Prüfsumme der abgelegten Form. // // Bei einem verschlüsselten Block lässt sich seine Unversehrtheit nicht // mehr an der Kennung ablesen — die beschreibt den Klartext. Diese // Prüfsumme erlaubt es einem Integritätslauf dennoch, ohne Schlüssel zu // prüfen. Sie bleibt leer, solange der Block untransformiert abliegt. StoredDigest string `json:"stored_digest,omitempty"` } // Writer nimmt die Daten eines Backups auf. // // Der Ablauf folgt dem Commit-Protokoll aus SYNCOVA_ARCHITECTURE.md §10: // Session anlegen, Chunks schreiben, Manifest schreiben und prüfen, Abschluss // atomar vermerken. Ohne Commit bleibt kein sichtbares Backup zurück. type Writer interface { // WriteChunk legt einen Datenblock ab und liefert dessen Kennung. // // Liegt derselbe Inhalt bereits vor, wird nichts erneut geschrieben — das // ist die Deduplizierung (PROMPT.md §10). Der zweite Rückgabewert meldet, // ob der Chunk neu war. WriteChunk(writeContext context.Context, chunkData []byte) (chunkReference ChunkReference, wasNew bool, writeError error) // WriteTransformedChunk legt einen bereits umgewandelten Block ab. // // Der Weg für die Backup Engine: Sie komprimiert und verschlüsselt selbst // und übergibt die fertige Form samt der Kennung des Klartexts. Die // Klartextlänge muss mitgegeben werden, weil sie sich aus der abgelegten // Form nicht mehr ermitteln lässt. // // **Diese Methode gehört an die Session, nicht ans Repository.** Sie zählt // mit — und ohne das Mitzählen trägt jedes Manifest eine Statistik von // null. Genau das war der Fall, bis es im Notfall-Nachweis der Phase 18 // auffiel: Ein wiederhergestelltes Repository meldete für jedes Backup die // Größe null, weil die Engine am Repository vorbei geschrieben hatte. WriteTransformedChunk(writeContext context.Context, plaintextIdentifier string, storedData []byte, plaintextLength int64) (storedDigest string, wasNew bool, writeError error) // NoteDeduplicatedChunk vermerkt einen Block, der bereits vorlag. // // Er wird nicht geschrieben — gelesen, gehasht und erkannt wurde er // trotzdem. Ohne diesen Vermerk traegt das Manifest eines zweiten Laufs // ueber unveraenderte Daten eine Statistik von null, und der Nutzen der // Deduplizierung liesse sich aus dem Repository allein nicht mehr belegen. NoteDeduplicatedChunk(plaintextLength int64) // Commit schließt das Backup ab und macht es sichtbar. Commit(commitContext context.Context, manifest *Manifest) error // Abort bricht die Session ab und räumt unfertige Daten weg. Abort(abortContext context.Context) error // Statistics liefert die bisher erfassten Kennzahlen der Session. Statistics() SessionStatistics } // SessionStatistics sind die Kennzahlen einer Schreibsession (PROMPT.md §10). type SessionStatistics struct { // ChunksWritten ist die Zahl tatsächlich geschriebener Chunks. ChunksWritten int64 `json:"chunks_written"` // ChunksDeduplicated ist die Zahl der Chunks, die bereits vorlagen. ChunksDeduplicated int64 `json:"chunks_deduplicated"` // LogicalBytes ist die Menge der verarbeiteten Ursprungsdaten. LogicalBytes int64 `json:"logical_bytes"` // StoredBytes ist die Menge der tatsächlich geschriebenen Daten. StoredBytes int64 `json:"stored_bytes"` // DeduplicatedBytes ist die durch Deduplizierung eingesparte Menge. DeduplicatedBytes int64 `json:"deduplicated_bytes"` } // DeduplicationRatio liefert das Verhältnis von Ursprungs- zu abgelegter Datenmenge. // // Ein Wert von 3.0 bedeutet: es wurde ein Drittel der Ursprungsmenge abgelegt. // // Der zweite Rückgabewert meldet, ob ein endliches Verhältnis überhaupt // existiert. Das ist nicht der Fall, wenn nichts verarbeitet wurde oder wenn // jeder Block bereits vorlag — dann wurde nichts abgelegt, und eine Division // wäre nicht definiert. Ein stillschweigend gelieferter Wert von 0 würde den // besten aller Fälle als den schlechtesten darstellen (PROMPT.md §138). func (statistics SessionStatistics) DeduplicationRatio() (ratio float64, isDefined bool) { if statistics.LogicalBytes == 0 || statistics.StoredBytes == 0 { return 0, false } return float64(statistics.LogicalBytes) / float64(statistics.StoredBytes), true } // SavingsPercentage liefert den Anteil eingesparter Daten in Prozent. // // Anders als das Verhältnis ist diese Kennzahl immer bestimmbar und damit die // verlässlichere Angabe für eine Anzeige: 100 % bedeutet, dass jeder Block // bereits im Repository vorlag. func (statistics SessionStatistics) SavingsPercentage() float64 { if statistics.LogicalBytes == 0 { return 0 } return float64(statistics.DeduplicatedBytes) / float64(statistics.LogicalBytes) * 100 } // Repository ist die Ablage der Backup-Nutzdaten. // // Die Schnittstelle ist bewusst schmal: eine spätere Umsetzung auf // S3-kompatiblem Objektspeicher soll ohne Änderung der Backup Engine möglich // sein (SYNCOVA_ARCHITECTURE.md §4.5). type Repository interface { // Descriptor beschreibt das Repository. Descriptor() Descriptor // BeginBackup öffnet eine Schreibsession für ein Backup. BeginBackup(beginContext context.Context, backupID string) (Writer, error) // ReadChunk liest einen Chunk und prüft dabei seine Unversehrtheit. ReadChunk(readContext context.Context, chunkIdentifier string) ([]byte, error) // OpenChunk öffnet einen Chunk als Datenstrom. // // Der Weg ist für große Chunks gedacht, die nicht vollständig in den // Arbeitsspeicher passen sollen (PROMPT.md §80). OpenChunk(readContext context.Context, chunkIdentifier string) (io.ReadCloser, error) // HasChunk meldet, ob ein Chunk bereits vorliegt. HasChunk(queryContext context.Context, chunkIdentifier string) (bool, error) // ReadManifest liest das Manifest eines Backups. ReadManifest(readContext context.Context, backupID string) (*Manifest, error) // ListBackups liefert alle abgeschlossenen Backups. ListBackups(listContext context.Context) ([]CatalogEntry, error) // Catalog liefert den Katalog des Repositorys. Catalog(catalogContext context.Context) (*Catalog, error) // RebuildCatalog baut den Katalog allein aus den Manifesten neu auf. RebuildCatalog(rebuildContext context.Context) (*Catalog, error) // Scan prüft die Unversehrtheit des gesamten Repositorys. Scan(scanContext context.Context, scanOptions ScanOptions) (*ScanReport, error) // Health liefert den Zustand des Repositorys. Health(healthContext context.Context) (HealthReport, error) // DeleteBackup entfernt ein Backup, sofern kein Aufbewahrungsschutz greift. DeleteBackup(deleteContext context.Context, backupID string) error // Close gibt belegte Betriebsmittel frei. Close() error } // ScanOptions steuern einen Integritätslauf. type ScanOptions struct { // VerifyChunkContents legt fest, ob jeder Chunk neu gehasht wird. // // Ohne diese Prüfung wird nur das Vorhandensein festgestellt. Die // vollständige Prüfung liest das gesamte Repository und dauert entsprechend. VerifyChunkContents bool // ProgressCallback wird während des Laufs mit dem Fortschritt aufgerufen. ProgressCallback func(scanProgress ScanProgress) } // ScanProgress beschreibt den Fortschritt eines Integritätslaufs. type ScanProgress struct { // BackupsChecked ist die Zahl der bislang geprüften Backups. BackupsChecked int // BackupsTotal ist die Gesamtzahl zu prüfender Backups. BackupsTotal int // ChunksChecked ist die Zahl der bislang geprüften Chunks. ChunksChecked int64 // BytesChecked ist die Menge der bislang gelesenen Daten. BytesChecked int64 } // HealthStatus ist der Zustand eines Repositorys (PROMPT.md §93). type HealthStatus string const ( // HealthStatusHealthy bedeutet: keine Auffälligkeiten. HealthStatusHealthy HealthStatus = "healthy" // HealthStatusWarning bedeutet: ein Problem bahnt sich an. HealthStatusWarning HealthStatus = "warning" // HealthStatusDegraded bedeutet: eingeschränkt nutzbar. HealthStatusDegraded HealthStatus = "degraded" // HealthStatusCritical bedeutet: Datenverlust oder Unbenutzbarkeit. HealthStatusCritical HealthStatus = "critical" ) // HealthReport beschreibt den Zustand eines Repositorys. type HealthReport struct { // Status ist der Gesamtzustand. Status HealthStatus `json:"status"` // Message erklärt den Zustand verständlich (PROMPT.md §48). Message string `json:"message"` // RecommendedAction nennt den nächsten sinnvollen Schritt. RecommendedAction string `json:"recommended_action,omitempty"` // CapacityBytes ist die Gesamtkapazität des Datenträgers. CapacityBytes int64 `json:"capacity_bytes"` // UsedBytes ist der belegte Speicherplatz. UsedBytes int64 `json:"used_bytes"` // FreeBytes ist der freie Speicherplatz. FreeBytes int64 `json:"free_bytes"` // BackupCount ist die Zahl abgeschlossener Backups. BackupCount int `json:"backup_count"` // LatencyMilliseconds ist die gemessene Antwortzeit eines Testzugriffs. LatencyMilliseconds float64 `json:"latency_ms"` // CheckedAt ist der Zeitpunkt der Prüfung in UTC. CheckedAt time.Time `json:"checked_at"` } // UsedPercentage liefert die Belegung in Prozent. func (report HealthReport) UsedPercentage() float64 { if report.CapacityBytes == 0 { return 0 } return float64(report.UsedBytes) / float64(report.CapacityBytes) * 100 }