syncova-backup/packages/repository/format.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

195 lines
7.7 KiB
Go

// Package repository implementiert die Ablage der Backup-Nutzdaten.
//
// Kerngedanke (PROMPT.md §2.4, SYNCOVA_ARCHITECTURE.md §11): Ein Repository ist
// selbstbeschreibend. Sämtliche Angaben, die zur Wiederherstellung nötig sind,
// liegen im Repository selbst — niemals ausschließlich in PostgreSQL. Geht der
// Control Server verloren, muss ein Repository allein durch Scannen wieder
// nutzbar werden.
package repository
import (
"encoding/json"
"errors"
"fmt"
"time"
)
// FormatVersion ist die Version des Repository-Formats.
//
// Sie wird bei jedem Öffnen geprüft. Ein Repository neueren Formats wird nicht
// angetastet: eine ältere Programmversion könnte es sonst beschädigen
// (PROMPT.md §13).
const FormatVersion = 1
// FormatIdentifier kennzeichnet ein Syncova-Repository eindeutig.
//
// Er verhindert, dass ein beliebiges Verzeichnis versehentlich als Repository
// angesprochen und dabei überschrieben wird.
const FormatIdentifier = "syncova-repository"
// HashAlgorithm benennt das Verfahren zur Bildung der Chunk-Kennung.
type HashAlgorithm string
const (
// HashAlgorithmSHA256 ist das Verfahren der Formatversion 1.
//
// SHA-256 ist kryptografisch sicher (PROMPT.md §9), Bestandteil der
// Standardbibliothek und wird auf allen Zielplattformen hardwarebeschleunigt.
// Eine Fremdbibliothek brächte hier keinen Vorteil, aber eine zusätzliche
// Abhängigkeit im sicherheitskritischsten Pfad des Produkts.
HashAlgorithmSHA256 HashAlgorithm = "sha256"
)
// RepositoryKind beschreibt die Betriebsart eines Repositorys.
type RepositoryKind string
const (
// KindLocal ist ein gewöhnliches Repository im Dateisystem.
KindLocal RepositoryKind = "local"
// KindHardenedLinux ist ein gehärtetes Repository mit Retention Lock.
//
// Gelöscht werden darf hier erst nach Ablauf der Aufbewahrungsfrist
// (PROMPT.md §15).
KindHardenedLinux RepositoryKind = "hardened_linux"
)
// Descriptor beschreibt ein Repository und liegt in seinem Wurzelverzeichnis.
//
// Der Descriptor ist der Einstiegspunkt jedes Wiederaufbaus: er benennt Format,
// Verfahren und Betriebsart, ohne die die abgelegten Daten nicht deutbar wären.
type Descriptor struct {
// Identifier kennzeichnet die Datei als Syncova-Repository.
Identifier string `json:"identifier"`
// FormatVersion ist die Version des Repository-Formats.
FormatVersion int `json:"format_version"`
// RepositoryID ist der dauerhafte Bezeichner dieses Repositorys.
//
// Er bleibt auch dann gültig, wenn das Repository an einen anderen Pfad
// oder an einen neu aufgesetzten Control Server angehängt wird.
RepositoryID string `json:"repository_id"`
// Name ist die sprechende Bezeichnung des Repositorys.
Name string `json:"name"`
// Kind ist die Betriebsart.
Kind RepositoryKind `json:"kind"`
// HashAlgorithm ist das Verfahren der Chunk-Kennungen.
HashAlgorithm HashAlgorithm `json:"hash_algorithm"`
// ChunkFanoutDepth ist die Zahl der Unterverzeichnisebenen der Chunk-Ablage.
ChunkFanoutDepth int `json:"chunk_fanout_depth"`
// Immutable meldet, ob ein Retention Lock gilt.
Immutable bool `json:"immutable"`
// RetentionSeconds ist die Aufbewahrungsfrist neuer Backups in Sekunden.
//
// Sie steht im Descriptor und nicht in der Datenbank, weil das Repository
// ohne Control Server deutbar bleiben muss: Wer es an einen fremden Server
// anhaengt, soll die geltende Frist vorfinden und nicht die des neuen
// Servers untergeschoben bekommen.
//
// Null bedeutet: die Standardfrist von 30 Tagen (PROMPT.md §119).
RetentionSeconds int64 `json:"retention_seconds,omitempty"`
// MinimumRetentionSeconds ist die kuerzeste je zulaessige Frist.
//
// Sie ist die eigentliche Sperre gegen den Angriff „Frist auf null setzen,
// dann alles loeschen". Einmal gesetzt, laesst sie sich nicht mehr
// verringern — auch nicht von einem Administrator.
MinimumRetentionSeconds int64 `json:"minimum_retention_seconds,omitempty"`
// EncryptionRequired meldet, ob Backups verschlüsselt abgelegt werden müssen.
EncryptionRequired bool `json:"encryption_required"`
// CreatedAt ist der Anlagezeitpunkt in UTC.
CreatedAt time.Time `json:"created_at"`
// CreatedByVersion ist die Programmversion, die das Repository angelegt hat.
//
// Bei einem Wiederaufbau lässt sich damit feststellen, welche Software das
// Format geschrieben hat.
CreatedByVersion string `json:"created_by_version"`
}
// Fehler des Repository-Formats.
var (
// ErrNotARepository meldet ein Verzeichnis ohne gültigen Descriptor.
ErrNotARepository = errors.New("das verzeichnis enthält kein syncova-repository")
// ErrUnsupportedFormat meldet eine nicht unterstützte Formatversion.
ErrUnsupportedFormat = errors.New("die formatversion des repositorys wird von dieser programmversion nicht unterstützt")
// ErrRepositoryExists meldet ein bereits vorhandenes Repository.
ErrRepositoryExists = errors.New("in diesem verzeichnis existiert bereits ein repository")
)
// Validate prüft einen gelesenen Descriptor auf Verwendbarkeit.
func (descriptor Descriptor) Validate() error {
if descriptor.Identifier != FormatIdentifier {
return ErrNotARepository
}
// Ein neueres Format darf nicht beschrieben werden: diese Programmversion
// kennt seine Regeln nicht und könnte Daten unbrauchbar machen.
if descriptor.FormatVersion > FormatVersion {
return fmt.Errorf("%w: gefunden wurde Version %d, unterstützt wird bis Version %d",
ErrUnsupportedFormat, descriptor.FormatVersion, FormatVersion)
}
if descriptor.FormatVersion < 1 {
return fmt.Errorf("%w: die Formatversion %d ist ungültig", ErrUnsupportedFormat, descriptor.FormatVersion)
}
if descriptor.HashAlgorithm != HashAlgorithmSHA256 {
return fmt.Errorf("%w: das Hashverfahren %q ist unbekannt", ErrUnsupportedFormat, descriptor.HashAlgorithm)
}
if descriptor.RepositoryID == "" {
return fmt.Errorf("%w: dem Repository fehlt seine Kennung", ErrNotARepository)
}
return nil
}
// encodeDescriptor serialisiert einen Descriptor.
//
// Die Ausgabe ist bewusst eingerückt: bei einem Wiederaufbau von Hand muss ein
// Administrator die Datei lesen können.
func encodeDescriptor(descriptor Descriptor) ([]byte, error) {
encodedDescriptor, marshalError := json.MarshalIndent(descriptor, "", " ")
if marshalError != nil {
return nil, fmt.Errorf("der repository-descriptor konnte nicht erzeugt werden: %w", marshalError)
}
return append(encodedDescriptor, '\n'), nil
}
// decodeDescriptor liest einen Descriptor.
func decodeDescriptor(rawDescriptor []byte) (Descriptor, error) {
var descriptor Descriptor
if unmarshalError := json.Unmarshal(rawDescriptor, &descriptor); unmarshalError != nil {
// Eine unlesbare Datei bedeutet nicht zwingend ein defektes Repository,
// aber sie darf keinesfalls als gültig durchgehen.
return Descriptor{}, fmt.Errorf("%w: der descriptor ist unlesbar", ErrNotARepository)
}
return descriptor, nil
}
// RetentionPeriod liefert die Aufbewahrungsfrist neuer Backups.
//
// Ohne ausdrückliche Angabe gilt die Standardfrist. Eine Frist von null wäre
// die gefährlichste Vorgabe: Der Betreiber hielte sein Repository für gehärtet,
// während jedes Backup sofort löschbar wäre.
func (descriptor Descriptor) RetentionPeriod() time.Duration {
if descriptor.RetentionSeconds <= 0 {
return DefaultRetentionPeriod
}
return time.Duration(descriptor.RetentionSeconds) * time.Second
}
// MinimumRetentionPeriod liefert die kürzeste zulässige Aufbewahrungsfrist.
func (descriptor Descriptor) MinimumRetentionPeriod() time.Duration {
if descriptor.MinimumRetentionSeconds <= 0 {
return 0
}
return time.Duration(descriptor.MinimumRetentionSeconds) * time.Second
}
// DefaultRetentionPeriod ist die Aufbewahrungsfrist ohne ausdrückliche Angabe.
//
// 30 Tage entsprechen der Standardvorgabe aus PROMPT.md §119.
const DefaultRetentionPeriod = 30 * 24 * time.Hour