Files
nexarch/STORAGE-KONZEPT.md
patrickandClaude Sonnet 5 78ab9a1300
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
docs/shl: Tote Kanban-Verweise entfernt, Modul-Href-Dopplung aufgeloest
Fable-Review auf Code-Dopplung/interne Links (siehe Analyse). P0-Funde:

- Dangling Prosa-Verweise auf die per PR #367 geloeschten *-kanban/-Ordner
  in SKALIERUNGSKONZEPT.md, STORAGE-KONZEPT.md, MIGRATIONSKONZEPT.md,
  SICHERHEITSKONZEPT.md (7x "siehe X-kanban/README.md"), PRODUKTARCHITEKTUR.md
  um Hinweis ergaenzt, dass Boards jetzt in main-Historie/Gitea-PRs liegen.
- dms/internal/searchsync/searchsync_test.go:19: falscher Pfad im Kommentar
  ("internal/bulkedit"/"internal/jobqueue" statt "dms/internal/...").
- web/shl/components/ModuleNavShell.tsx: resolveHrefClientOnly war inhaltlich
  identisch zu resolveModuleHref in web/portal/app/ModuleGrid.tsx (zwei
  unabhaengig gepflegte Kopien derselben Hostname-Ersetzung). Als
  resolveModuleHref aus shl exportiert, ModuleGrid.tsx nutzt jetzt die
  gemeinsame Funktion, Test-Import entsprechend umgestellt.
- web/portal/lib/modules.test.ts: KNOWN_REAL_PERMISSIONS-Whitelist hatte
  kennzahlen.read (migrations/0019_kennzahlen_policy_rule.up.sql, real
  existierend seit ANA-02/PR #362) nie nachgetragen, Test lief seitdem
  unbemerkt im Fehler. Ergaenzt.

go test/vitest: web/portal (15/15) und web/shl (61/61) gruen auf Testhost 131.
Verbleibender P0-Punkt (14 Web-Apps ohne ModuleNavShell) braucht
Produktentscheidung -- mehrere Registry-Keys (ai/mail/workflow/connect)
sind bereits durch eine jeweils ANDERE App belegt als die ohne Navigation
(z.B. mail-search hat "mail", mail-postfach nicht), daher keine mechanische
Zuordnung moeglich ohne zu raten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-19 13:09:53 +02:00

15 KiB
Raw Permalink Blame History

NEXARCH 2.0 – Storage-Konzept

Stand: 2026-08-26, Abschnitt 9 aktualisiert + Abschnitt 10 neu (Arbeitsverzeichnisse) 2026-08-28. Grundlage: dms-kanban/tickets/FDN-03.md, mail-kanban/tickets/ARC-01.md, archive-kanban/tickets/RET-*.md, archive-kanban/tickets/BAK-*.md, SKALIERUNGSKONZEPT.md (Kanban-Boards inzwischen aus dem Repo entfernt, Ticket-Historie liegt in der Gitea-PR-Historie).

1. Grundprinzip

Strikte Trennung, bereits in FDN-03 (DMS) und ARC-01 (Mail) unabhängig, aber konsistent so entschieden:

  • PostgreSQL: ausschließlich Metadaten (wer, was, wann, Tenant, Beziehungen, Status, Fristen).
  • Objekt-Storage: ausschließlich Binärinhalte (Dokumente, E-Mails, Anhänge, Thumbnails, Backups).

Kein Binärinhalt landet je in einer Datenbankspalte (kein bytea für Dateien) — das ist Bedingung für alles Weitere in diesem Dokument (Skalierung, Backup-Strategie, S3-Austauschbarkeit).

2. Backend-Optionen je Betriebsstufe

Deckt sich mit SKALIERUNGSKONZEPT.md Abschnitt 3:

Betriebsstufe Backend Treiber
Stufe 1 (Einzel-LXC) lokales Dateisystem oder ZFS lokaler Treiber (FDN-03/ARC-01 "ein Interface, zwei austauschbare Treiber")
Stufe 2 (verteilt) S3-kompatibel: MinIO (selbst betrieben) oder Ceph (RADOS Gateway) S3-Treiber, identische API wie Stufe 3
Stufe 3 (Private Cloud / SaaS) S3-kompatibel: MinIO/Ceph selbst betrieben oder Managed-Object-Storage beim Provider S3-Treiber, kein Unterschied im Code

Der Übergang Stufe 1 → 2/3 ist ein reiner Infrastruktur-Schritt (Treiber-Konfiguration ändern, nicht Code), das war der explizite Zweck der Abstraktion. Datenübernahme beim Wechsel: einmaliger Kopiervorgang (z.B. mc mirror bei MinIO) mit anschließendem Prüfsummen-Abgleich (Abschnitt 6) — kein Big-Bang-Risiko wie bei der Systemmigration aus MIGRATIONSKONZEPT.md, da es dieselbe NEXARCH-Instanz ist, nur das Backend wechselt.

Bewusst nicht vorgesehen: NFS als produktives Ziel für Stufe 2/3 — das S3-Protokoll ist die gemeinsame Klammer, NFS wäre ein dritter, inkompatibler Zugriffspfad. NFS bleibt als Option für den lokalen Stufe-1-Treiber denkbar (Mount-Punkt statt echtes lokales Dateisystem), ändert aber nichts am Treiber-Interface.

3. Pfad-/Namenskonvention (modulübergreifend vereinheitlicht)

FDN-03 und ARC-01 legen je ihre eigene Pfadkonvention fest (Akzeptanzkriterium in beiden Tickets), bisher ohne geteilten Rahmen. Damit Backup (Archive BAK-02), Export (Archive DMS2-01 im Connect-Board, DMS DOC-08) und Betrieb nicht drei verschiedene Konventionen lernen müssen, gilt projektweit ein gemeinsames Schema — und, analog zur Datenbank (Modell C, eigene Datenbank pro Mandant), jetzt auch eine physische Trennung im Objekt-Storage: jeder Mandant erhält einen eigenen Bucket bzw. Pfad-Root, statt eines gemeinsamen Buckets mit tenant_id-Pfadpräfix.

Begründung: physische Trennung schließt bei Objekt-Storage dieselbe Fehlerklasse aus wie bei der Datenbank — ein Codefehler ohne Tenant-Kontext kann kein fremdes Bucket erreichen, nicht nur einen falschen Pfad innerhalb eines gemeinsamen Buckets. Ein vergessener Pfadfilter/Präfix-Check in einem gemeinsamen Bucket führt zum Cross-Tenant-Leck; ein fehlendes Tenant-Bucket-Mapping führt dagegen nur dazu, dass gar kein Bucket referenzierbar ist.

Die Provisionierung des Tenant-Buckets ist Teil desselben Provisioning-Schritts wie die Anlage der Tenant-Datenbank in Core TEN-01 — ein neuer Mandant erhält Datenbank und Bucket in einem automatisierten Schritt.

Innerhalb des Tenant-Buckets entfällt das führende tenant_id-Segment, das Schema wird zu:

{modul}/{objekttyp}/{objekt_id}/{revision-oder-teil}

Beispiele (innerhalb des Buckets von Mandant t-4471):

  • dms/documents/doc-9021/rev-3.bin
  • mail/messages/msg-88213/part-2-attachment.bin
  • archive/backups/2026-08-26/snapshot.tar.zst

Innerhalb des Pfads bleibt jedes Modul für sein {objekttyp}-Vokabular selbst zuständig (DMS: documents, thumbnails; Mail: messages, attachments) — nur die äußeren zwei Ebenen (modul/Rest) sind projektweit fix, damit ein Werkzeug (Backup, Export, Kapazitätsauswertung) modulübergreifend funktioniert, ohne pro Modul eine Sonderbehandlung zu brauchen. Die Mandantentrennung selbst (Bucket-Auswahl) ist damit kein Bestandteil des Pfadschemas mehr, sondern eine reine Provisioning-/Zugriffsschicht-Frage (siehe Ticket ARC-06, Mandantentrennung im Objekt-Storage; Kanban-Board inzwischen aus dem Repo entfernt, Historie in Gitea).

4. Verarbeitungsreihenfolge (Lebenszyklus einer Datei)

Zusammengeführt aus den bereits getroffenen Einzelentscheidungen (FDN-01/DOC-01/DOC-02/FDN-09 im DMS-Board, ARC-01/ARC-02/ARC-03 im Mail-Board):

1. Empfang (Klartext, im Speicher/temporär)
2. SHA-256-Hash auf Klartext
3. Duplikatsprüfung (DOC-02 / ARC-03) — Gate: nur neue oder bewusste Duplikate gehen weiter
4. Verschlüsselung at rest (FDN-09 / ARC-02, AES-256-GCM, Envelope Encryption)
5. Ablage im Objekt-Storage nach Pfadkonvention (Abschnitt 3)
6. Prüfsummenvergleich nach dem Schreiben (Akzeptanzkriterium in FDN-03/ARC-01)
7. Registrierung bei Archive-Retention-Engine (RET-05 Modul-Adapter) — Aufbewahrungsfrist beginnt zu laufen
8. OCR (SRC-05 / SRC-07) und Indexierung (SRC-02 / SRC-01+SRC-02) — läuft NACH Schritt 3, nicht parallel dazu

Löschung läuft ausschließlich über die Archive-Engine (Retention/Legal-Hold-geprüft), niemals direkt durch DMS oder Mail — das ist bereits Konsequenz der Archive-Konsolidierung, hier nur noch einmal aus Storage-Sicht festgehalten: kein Modul löscht Objekte eigenständig aus dem Storage.

5. Zugriffsmodell

  • Kein direkter Client-Zugriff auf den Objekt-Storage. Up-/Downloads laufen über zeitlich begrenzte signierte URLs (FDN-03 Akzeptanzkriterium 2, Standard-Pattern für S3-kompatible Backends), die das Backend ausstellt — Storage-Zugangsdaten selbst erreichen nie den Browser.
  • Presigned-URL-Gültigkeit kurz und konfigurierbar halten (Minuten, nicht Stunden) — lange gültige URLs sind ein unnötig großes Zeitfenster für Linkweitergabe.
  • Storage-Zugangsdaten (S3-Credentials) sind Secrets wie jedes andere — ausschließlich über Umgebungsvariablen, nie im Code (bereits projektweite Regel aus jedem Ticket-Abschluss-Abschnitt).

6. Konsistenzprüfung

Jeder Schreibvorgang wird durch Prüfsummenvergleich verifiziert (bestehende Akzeptanzkriterien in FDN-03 und ARC-01). Für den Bestand insgesamt zusätzlich:

  • Reconciliation (Archive BAK-05): periodischer Abgleich zwischen den in Postgres referenzierten Objekten und dem tatsächlichen Storage-Inhalt — erkennt verwaiste Metadaten-Einträge (Objekt fehlt im Storage) und verwaiste Storage-Objekte (kein Metadaten-Eintrag mehr, z.B. nach einem Fehler bei Schritt 5/6 aus Abschnitt 4).
  • Reale Lehre aus archivmail: Reconciliation-Reports existierten dort bereits als Soll/Ist-Abgleich (siehe datenmodell-altsysteme.md) — Muster wird über Archive BAK-05 fortgeführt, nicht neu erfunden.
  • Integritätsprüfung per Hash (Archive BAK-08): periodischer Scrub-Job, der Stichproben liest, SHA-256 neu berechnet und gegen file_revisions.checksum_sha256 vergleicht — erkennt stille Inhaltsveränderung/Bit-Rot bei Objekten, die noch existieren.

6.1 Grenzen der Prüfung bei extern eingebundenem Kunden-Storage

Bindet ein Kunde einen eigenen S3-Bucket ein, überwachen wir weder dessen Hardware noch dessen Betreiber. Was NEXARCH technisch leisten kann, hängt davon ab, was wir dürfen:

  • Kein Lesezugriff, nur Schreiben: keine Verifikation möglich. Wir können höchstens den Upload-Beleg und den Hash zum Upload-Zeitpunkt festhalten — das ist kein Monitoring, sondern ein Nachweis für später.
  • Lese-/API-Zugriff vorhanden: Existenz- (BAK-05) und Inhaltsprüfung (BAK-08) sind möglich, aber periodisch/stichprobenhaft, nicht in Echtzeit, und nur für das, was wir selbst wieder lesen können.

Was diese beiden Prüfungen NICHT leisten:

  • Sie erkennen nicht lückenlos, ob ein Objekt unbefugt gelöscht oder durch ein neues ersetzt wurde, wenn uns keine Inventar-/Versionsdaten vom Storage vorliegen (ETag/LastModified/VersionId — sofern verfügbar, nicht überall ein einfacher Hash, besonders bei Multipart-Uploads oder providerseitiger Verschlüsselung, daher nicht als alleinige Wahrheitsquelle nehmen).
  • Sie ersetzen keine storage-seitige WORM-/Versionierungsstrategie und keine Zugriffs-/Audit-Logs des Storage-Providers — nur Object Lock, Versioning, Retention Policies und providerseitige Audit-Logs verhindern Veränderungen oder machen sie lückenlos nachweisbar, ein nachträglicher Scrub kann sie nur bemerken, nicht verhindern.

6.2 Drei Betriebsmodi für Objekt-Storage

  1. NEXARCH-managed Storage — wir kontrollieren Policies, Versioning, Object Lock, Backup vollständig.
  2. Customer-managed, compliant Storage — Kunde stellt den S3-Bucket, aber vertraglich/beim Onboarding verbindlich verlangt: Versioning aktiviert, Object Lock/Retention bei revisionssicherer Nutzung, Audit-Logs aktiviert, eingeschränkte Löschrechte, idealerweise eigener Backup-/Replikationspfad.
  3. Customer-managed, non-compliant Storage — nur für nicht revisionssichere Nutzung zulässig; muss im System klar markiert sein ("keine belastbare Unveränderbarkeitsgarantie"), damit keine falsche GoBD-Erwartung entsteht.

7. Backup vs. Storage-Redundanz — zwei verschiedene Schichten

Häufige Verwechslung, deshalb hier explizit getrennt:

  • Storage-Redundanz (diese Datei, Abschnitt 2): wie das Backend selbst Datenverlust durch Hardware-Ausfall verhindert (S3-Provider-Redundanz, MinIO-Distributed-Mode, Ceph-Replikation). Schützt vor Festplatten-/Knotenausfall, NICHT vor Anwendungsfehlern oder böswilligem/versehentlichem Löschen.
  • Backup (Archive BAK-01/BAK-02/BAK-03): eigenständige, deduplizierende Sicherung nach restic/Borg-Konzept, zeitversetzt, wiederherstellbar auch wenn die Primärkopie durch einen Anwendungsfehler beschädigt wurde. Das ist die Antwort auf "ein Bug löscht fälschlich Dateien" — Storage-Redundanz allein hilft dabei nicht, weil sie den fehlerhaften Löschvorgang originalgetreu mit-repliziert.

Beide Schichten sind nötig, keine ersetzt die andere.

8. Export & Portabilität

Aus der DMS-Produkt-DNA fortgeführt (ecoDMS-Lehre: "was die Oberfläche kann, kann auch die offene Schnittstelle", keine Herstellerbindung):

  • Vollständiger Export von Dateiinhalt + Metadaten ohne NEXARCH-eigenes Werkzeug muss jederzeit möglich sein (DMS DOC-08 Massenexport, Mail ARC-07 Export/Portabilität, Connect DMS2-01 offenes Austauschformat für Fremd-DMS-Interoperabilität).
  • Objekt-Storage-Ablage selbst nutzt offene Formate (Original-Dateiformat + PDF/A für OCR-Ergebnisse, kein proprietäres Container-Format) — keine Vendor-Lock-Kodierung, die einen Export nachträglich erschweren würde.

9. Speicherverbrauch-Messung für Quota/Lizenzierung

Offene Lücke — geklärt (2026-08-28): Core LIC-05 "Speicherverbrauch-Metrik je Tenant" setzt genau den hier vorgeschlagenen Mechanismus um — Objekt-Storage-Treiber der Module (FDN-03/ARC-01) melden bei jedem Schreib-/Löschvorgang die Objektgröße, ein leichtgewichtiger Postgres-Zähler pro Tenant wird geführt, LIC-03 fragt ihn zur Quota-Durchsetzung ab. LIC-05 hängt im Core-Abschluss-Gate QA-05.

10. Arbeitsverzeichnisse (Consume, Quarantäne, Export-Staging, Modell-Cache)

Ergänzung 2026-08-28, angestoßen durch den Vergleich mit paperless-ngx (consume/, data/, media/, export/, trash/) — mehrere Tickets definieren Funktionalität, die einen lokalen Arbeitsordner braucht, legen aber bisher nicht fest, WO dieser liegt. Papierkorb (DMS DOC-11/DOC-12, Mail ARC-05) braucht dagegen keinen eigenen Pfad — das ist ein reines DB-Status-Flag, das Objekt bleibt physisch am gewohnten Platz im Objekt-Storage, bis Archive es nach Fristablauf endgültig entfernt.

10.1 Consume-/Hot-Folder (DMS INT-06/INT-08, Mail IMP-05)

Anders als der reguläre Objekt-Storage-Zugriff (Abschnitt 2, ausschließlich über signierte URLs, Abschnitt 5) braucht ein Datei-Watch zwingend ein echtes lokales Dateisystemverzeichnis — S3-kompatible Backends unterstützen kein Datei-Watch. Konsequenz: Consume-Ordner sind kein Teil des Tenant-Objekt-Storage-Buckets, sondern ein separater, lokaler Zwischenspeicher je Tenant auf dem Modul-Host:

{consume-root}/{tenant_id}/dms/inbox/
{consume-root}/{tenant_id}/mail/inbox/

Nach erfolgreicher Verarbeitung (Hash, Duplikatsprüfung, Verschlüsselung, Ablage im Objekt-Storage — Abschnitt 4) wird die Datei aus dem Consume-Ordner entfernt, nicht dauerhaft dort belassen. Consume-Root ist damit strukturell näher an einem Cache als an Aufbewahrung und braucht keine Backup-Einbindung (Abschnitt 7) — im Fehlerfall wird die Quelle erneut abgelegt/gescannt.

10.2 Quarantäne (Mail IMP-06 Virenscan-Fund)

Bei Virenscan-Fund wird die Anhang-Datei nicht gelöscht, sondern in einen isolierten Quarantäne-Bereich verschoben (Nachvollziehbarkeit, siehe Compliance-Anforderung) — ebenfalls lokal, ebenfalls je Tenant:

{consume-root}/{tenant_id}/mail/quarantine/

Quarantäne-Objekte werden nicht automatisch gelöscht; Aufbewahrungsdauer und Zugriff (nur Admin/Security-Rolle) sind Akzeptanzkriterium von IMP-06, nicht Gegenstand dieses Dokuments.

10.3 Export-Staging (DMS DOC-08, Mail ARC-07)

Massenexport (ZIP) erzeugt eine temporäre Datei zwischen "Export angestoßen" und "Download abgeschlossen". Diese Staging-Fläche ist rein ephemer (Lebensdauer im Bereich von Minuten bis wenigen Stunden), braucht keine Tenant-Bucket-Struktur und keine Backup-Einbindung:

{export-staging-root}/{tenant_id}/{export_job_id}.zip

Aufräumung nach Ablauf einer kurzen Frist (Vorschlag: 24 Stunden) oder nach bestätigtem Download ist Akzeptanzkriterium der jeweiligen Export-Kachel.

10.4 Ollama-Modell-Cache (AI ENG-02)

Bisher nicht adressierte Lücke: ENG-02 (Ollama-Adapter) lädt und hält LLM-Modellgewichte lokal vor — je nach Modellklasse mehrere GB bis über 40 GB (siehe PROXMOX-DEPLOYMENT.md Abschnitt 6a, dort nur RAM-Sizing, keine Disk-Planung). Anders als Consume/Quarantäne/Export ist das kein Zwischenspeicher, sondern persistenter, potenziell großer Datenbestand, der eine eigene Kapazitätsplanung braucht, getrennt vom restlichen Objekt-Storage:

{ollama-root}/models/

Nicht tenant-partitioniert (Modelle werden über Tenants hinweg geteilt, sofern keine Tenant-spezifische Modellwahl vorliegt — siehe AI ENG-04). Bei Stufe-1-Einzel-LXC (siehe PROXMOX-DEPLOYMENT.md Abschnitt 2) sollte dieser Pfad auf ein eigenes Volume/Dataset gelegt werden, damit ein wachsender Modellbestand nicht den restlichen Systemspeicher verdrängt — gleiches Isolationsprinzip wie bei Tenant-Buckets (Abschnitt 3), nur nicht tenant-, sondern funktionsbezogen.

10.5 Warum eigene Datasets/Mountpoints sinnvoll sind

Alle vier Arbeitsverzeichnisse (10.1–10.4) profitieren aus demselben Grund von eigenen ZFS-Datasets/Mountpoints statt Unterordnern im Root-Dateisystem: unabhängige Quotas (ein vollaufender Consume-Ordner darf nicht das Systemvolume fluten), unabhängige Snapshots/Löschbarkeit (siehe Erfahrung mit archivmail-Alt-Deinstallation, nexarch-state.json Entscheidungsverlauf 2026-08-28), und die Möglichkeit, unterschiedliche Backup-/Retention-Policies zuzuweisen (Consume/Export brauchen keine, Modell-Cache profitiert von keiner inkrementellen Sicherung großer Binärdateien, sondern von einfacher Neubeschaffung bei Bedarf).