Add optional zstd mbox archives

This commit is contained in:
DonVoo 2026-07-14 20:20:30 +02:00
parent 21844688ac
commit e612896e06
14 changed files with 620 additions and 39 deletions

114
zstd-brief.md Normal file
View file

@ -0,0 +1,114 @@
# Codex-Brief — mbox-Kompression mit zstd-3
Ziel: Das Datengrab schrumpfen, **ohne** den verifizierten Append-only-Pfad und
die Vorschau kaputtzumachen. Danach vergleichen wir gemessen: Größe und
Vorschau-Latenz, komprimiert vs. plain.
## Gemessene Ausgangslage (echte mbox auf GoldPi)
| mbox | roh | zstd-3 | zstd-19 |
|---|---|---|---|
| INBOX.mbox | 4.372.002 | 113.564 (**38,5×**) | 111.017 (39,4×) |
| Test-Haupt-Ordner | 51.699 | 11.316 (**4,6×**) | 10.677 (4,8×) |
**zstd-19 bringt nur ~2 % mehr als zstd-3, kostet aber ein Vielfaches an Zeit →
Level 3.** (Die 38× sind ein Artefakt: die INBOX besteht großteils aus fast
identischen Benachrichtigungsmails. Realistisch bei echten Postfächern: 24×.)
## Format
`<ordner>.mbox.zst` = **Folge von zstd-Frames**. Konkatenierte Frames sind eine
gültige `.zst``zstd -d datei.mbox.zst` liefert **exakt** die mbox, die wir
sonst plain geschrieben hätten. Damit bleibt der Append-only-Charakter erhalten:
neue Mail = neuer Frame hinten dran.
**Start mit: ein Frame pro Mail.** Das macht die Vorschau trivial schnell (genau
ein Frame entpacken). Nachteil: keine Redundanz *zwischen* Mails → schlechtere
Rate. Falls die gemessene Rate enttäuscht, schalten wir auf **Batch-Frames mit
8-MB-Deckel** um (bessere Rate, Vorschau entpackt dann ≤ 8 MB ≈ 30 ms). **Der
Index unten ist so gebaut, dass dieser Wechsel nichts kostet** — bitte die
Felder auch dann schon so anlegen.
Bibliothek: **`github.com/klauspost/compress/zstd`** — pure Go, kein cgo,
bleibt single-binary.
## Offset-Index (der Kern)
Neue Tabelle, geschrieben im selben Schritt wie der mbox-Append:
```sql
CREATE TABLE IF NOT EXISTS mbox_index(
account_id INTEGER NOT NULL,
folder TEXT NOT NULL,
seq INTEGER NOT NULL, -- laufende Nummer im Ordner
message_id TEXT NOT NULL,
subject TEXT NOT NULL DEFAULT '',
from_addr TEXT NOT NULL DEFAULT '',
date TEXT NOT NULL DEFAULT '',
file_offset INTEGER NOT NULL, -- Byte-Offset des FRAMES in der Datei
frame_len INTEGER NOT NULL, -- Laenge des Frames (komprimiert)
inner_offset INTEGER NOT NULL DEFAULT 0, -- Offset der Mail IM entpackten Frame
inner_len INTEGER NOT NULL, -- Laenge der Mail entpackt
UNIQUE(account_id, folder, message_id)
);
```
(`inner_offset` ist bei „ein Frame pro Mail" immer 0 — er ist die Vorbereitung
für Batch-Frames.)
**Das schenkt uns nebenbei einen Bug-Fix:** Die Liste im Viewer parst heute die
*ganze* mbox-Datei (`ReadMboxList`). Künftig kommt sie aus dem Index → **kein
Dateizugriff mehr, schneller als heute**, auch bei einem 10-GB-Archiv.
## Schreibpfad — die Reihenfolge bleibt heilig
In `07-migrate.go` **nicht umsortieren**. Neu ist nur, dass der mbox-Append
Offsets zurückgibt und danach der Index geschrieben wird:
```
AlreadyCopied → dst.Append → mbox.Append (liefert offset/len) → Index-Zeile → MarkCopied
```
`MarkCopied` bleibt **das Letzte**. Ein Absturz davor kopiert die Mail beim
nächsten Lauf erneut — dieselbe bewusste Entscheidung wie bisher.
## Lesepfad
- **Liste** → nur SQLite (`mbox_index`), keine Datei anfassen.
- **Eine Mail öffnen**`file_offset` seeken, **genau diesen Frame** entpacken,
`inner_offset`/`inner_len` herausschneiden.
- **Abwärtskompatibel:** Bestehende **plain `.mbox`** müssen weiter funktionieren.
Erkennung über Endung bzw. zstd-Magic (`0xFD2FB528`). Wenn für eine Datei keine
Index-Zeilen existieren → alter Parse-Pfad.
## Umschaltbar (wichtig für den Vergleich)
`config.json`: `"mbox_compression": "none" | "zstd"` (Default zunächst `none`,
damit nichts überrascht). So können wir **dasselbe Postfach zweimal sichern**
einmal plain, einmal zstd — und sauber vergleichen.
## Export (die harte Anforderung bleibt)
Eine Funktion/Route **„Archiv als plain mbox exportieren"**: entpackt die
`.mbox.zst` zu einer normalen `.mbox`. Damit bleibt die Zusage erhalten, die der
Grund für mbox war: **man kann sie in eM Client ziehen** bzw. einem Anwalt oder
Prüfer in die Hand geben. Ein Handgriff statt null — akzeptabel.
## Abnahme
1. `go test ./...` grün.
2. **Round-trip byte-identisch** — das ist die wichtigste Prüfung für ein
Beweis-Archiv: Denselben Quell-Ordner einmal mit `none` und einmal mit `zstd`
sichern. Dann muss gelten:
`zstd -d ordner.mbox.zst` == `ordner.mbox` **Byte für Byte** (`cmp`).
Wenn nicht: **Stopp.** Ein Archiv, das nicht exakt zurückkommt, ist wertlos.
3. **Rate messen** an einer echten mbox (nicht an meinem Testkorpus — der ist
durch die vielen identischen Benachrichtigungsmails unrealistisch gut).
4. Idempotenz unverändert: zweiter Lauf `copied=0`.
5. Anhänge (PDF/PNG/ZIP) kommen aus dem komprimierten Archiv byte-identisch
wieder heraus.
6. **Vorschau-Latenz**: Mail öffnen aus plain vs. aus zstd — Zeit messen.
(Diesen Vergleich fahre ich, Claude.)
## NICHT in diesem Schritt
- Bestehende plain-mbox nachträglich komprimieren (Migration alter Archive) —
eigener, späterer Schritt.
- Batch-Frames mit 8-MB-Deckel — nur falls die gemessene Rate enttäuscht.