uid-delta-brief.md: UIDVALIDITY+UID waehlt nur KANDIDATEN aus. Body wird weiterhin geladen, Identitaet weiterhin aus canonicalMessageBytes berechnet. body_sha256 bleibt die Wahrheit; die UID spart nur den Download alter Kandidaten. backup-brief.md (Pos-81): DB und mbox sind NICHT symmetrisch abhaengig - mbox ohne DB ist per Reindex heilbar, DB ohne mbox ist wertlos. Gefaehrlich ist allein neuere DB + aeltere mbox. Daraus die Reihenfolge-Regel: DB zuerst. Gemessen: /mnt/DATA ist ext4 ohne LVM -> atomarer Snapshot unmoeglich. Aber mbox ist strikt append-only (06-mbox.go:61, kein O_TRUNC/Truncate/ Rename), und MAX(file_offset+frame_len) aus mbox_index ist die exakte Schreib-Wasserlinie - ueber alle 228 mbox-Dateien 0 Abweichungen. Daraus: VACUUM INTO + Praefix-Kopie = konsistenter Stand OHNE Stillstand. Falle notiert: mbox_index_state.indexed_bytes ist NICHT die Wasserlinie. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.2 KiB
Codex-Brief — Sicherung des Archivs (mbox + DB als EIN Stand)
Betrifft Pos-81 des Ledger-Vorgangs
netcup-svr-mail-graveyard-eklarner-sicherung-2026-07-16.
Die Regel
mbox ist der Inhaltsbeleg, die DB ist Index und Zustandsbuchhaltung. Beide gehoeren in denselben Sicherungszeitpunkt — aber nur die DB ist aus der mbox rekonstruierbar.
Die Abhaengigkeit ist nicht symmetrisch. Das ist der Kern:
| Kombination | Bewertung |
|---|---|
| mbox ohne DB | heilbar. --reindex baut den Index vollstaendig neu (main.go:26, ReindexArchives, plain und zstd). Wir haben das bei der Index-Drift real benutzt. |
| DB ohne mbox | wertlos. Die DB enthaelt nur Offsets, Hashes und Zustaende — keinen einzigen Mailinhalt. |
| neuere mbox + aeltere DB | grundsaetzlich heilbar per Reindex. Die ungefaehrliche Richtung. |
| neuere DB + aeltere mbox | GEFAEHRLICH. copied markiert eine Mail als gesichert, obwohl ihr Archivdatensatz im Backup fehlt. Ein stiller Verlust, der sich als gruener Haken tarnt. |
Daraus folgt eine Reihenfolge-Regel, falls kein gemeinsamer Stand erzeugt werden kann: erst die DB sichern, dann die mbox. Dann ist die mbox im Backup immer die neuere — die heilbare Richtung. Die umgekehrte Reihenfolge produziert exakt den gefaehrlichen Fall.
Was der Host hergibt
Gemessen auf netcup-svr am 2026-07-16:
/mnt/DATA ext4 auf /dev/vda4, noatime lazytime
LVM nicht vorhanden
btrfs/ZFS nicht vorhanden
SQLite journal_mode=wal, WAL 34 MB unmerged, integrity_check=ok
data/ + backup/ liegen unter EINEM Elternbestand
(/mnt/DATA/Containers/Test-Container/Mail-Graveyard-Pod/)
Der atomare Dateisystem-Snapshot — die eigentlich beste Loesung — steht auf diesem Host nicht zur Verfuegung. Ein Wechsel auf LVM/btrfs/ZFS waere die saubere Antwort, ist aber ein Eingriff am laufenden Rettungsbetrieb. Nicht jetzt.
Nebenbefund: Ein blosses cp der .db ohne -wal wuerde die neuesten 34 MB
Zustand verlieren. Das ist zufaellig die heilbare Richtung (aeltere DB), aber
verlassen darf man sich darauf nicht.
Der Weg, den das Append-Only-Design erlaubt
Zwei gemessene Eigenschaften machen einen dritten Weg moeglich, der keinen Stillstand braucht:
1. Die mbox-Dateien sind strikt append-only.
// 06-mbox.go:61 — der einzige Schreibpfad
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
Kein O_TRUNC, kein Truncate, kein Rename, keine Kompaktierung in der
gesamten Datei. Einmal geschriebene Bytes aendern sich nie mehr. Ein
Praefix [0, N) ist damit zu jedem spaeteren Zeitpunkt byte-identisch mit dem
Stand von damals.
2. Die DB kennt die exakte Schreib-Wasserlinie.
MAX(file_offset + frame_len) aus mbox_index ist per Konstruktion eine
Grenze auf vollstaendigen Datensaetzen. Geprueft ueber alle 228
mbox-Dateien: 0 Abweichungen zur echten Dateigroesse.
Nicht verwenden:
mbox_index_state.indexed_bytes. Das ist eine Lese-/ Viewer-Marke (02-database.go:281setzt sie beim Reindex auf-1) und laeuft dem Schreibstand um Groessenordnungen hinterher — bei e.klarner/INBOX 4,5 MB gegenueber 9,4 GB Datei. Diese Falle hat mich beim Entwurf einmal erwischt.
Das Verfahren
1. VACUUM INTO '<snap>/mail-graveyard.db'
-> konsistenter DB-Stand einer LAUFENDEN WAL-Datenbank, ohne Stopp.
SQLite garantiert das; WAL und -shm werden mit aufgeloest.
2. Aus DIESEM Schnappschuss je Konto/Ordner:
N = SELECT MAX(file_offset+frame_len) FROM mbox_index WHERE ...
3. Je mbox die ersten N Bytes kopieren (in Ruhe, waehrend das Tool weiterlaeuft).
4. Fertig. Kein Stillstand, kein LVM, kein Stoppen der Container.
Warum das ein Stand ist und kein Widerspruch: N stammt aus dem
DB-Schnappschuss selbst. Jeder copied- und jeder mbox_index-Eintrag darin
liegt per Konstruktion vollstaendig innerhalb des Praefix. Mails, die waehrend
des Kopierens ankommen, liegen jenseits von N und sind in beiden Haelften
abwesend. Die gefaehrliche Kombination „neuere DB + aeltere mbox" kann gar
nicht entstehen — nicht weil wir schnell sind, sondern weil das Paar
konstruktiv einen Zeitpunkt beschreibt.
Sollte doch etwas driften, zeigt es in die heilbare Richtung (Praefix enthaelt mindestens, was die DB kennt → Reindex).
Der Fallback, falls Schritt 1-3 nicht gebaut wird
Deine urspruengliche Variante bleibt gueltig und ist als Handgriff sofort
verfuegbar: schreibende Container kurz stoppen, WAL sauber checkpointen
(PRAGMA wal_checkpoint(TRUNCATE)), gemeinsamen Kopierstand erzeugen, starten.
Nur: ohne Snapshot-Dateisystem muesste hier fuer die vollen 42 GB gestoppt
werden — genau das vermeidet der Praefix-Weg.
Was zu bauen ist
--snapshot <zielverzeichnis> erzeugt DB-Schnappschuss + mbox-Praefixe
--verify-snapshot <verzeichnis> prueft einen Stand isoliert durch
Fallstricke
- N NIEMALS per
statvon der Datei nehmen. Dann kann die Grenze mitten in eine gerade geschriebene Mail fallen. N kommt aus dem DB-Schnappschuss, sonst ist die ganze Garantie hin. - Reihenfolge ist Teil der Korrektheit: erst
VACUUM INTO, dann N daraus lesen. Nicht umgekehrt, nicht aus der Live-DB. indexed_bytesist nicht die Wasserlinie (s. o.).- Der Snapshot ist kein Backup. Er liegt auf derselben Platte. Er ist die Vorstufe: von ihm wird raeumlich getrennt und moeglichst verschluesselt weggeschrieben.
- Kein stiller Default. Ohne Flag aendert sich nichts.
Abnahme
go test ./...gruen.--snapshotwaehrend eines laufenden Migrationslaufs: Der Stand muss danach in sich stimmen. Das ist der eigentliche Test — ein Snapshot im Leerlauf beweist nichts.- Restore isoliert testen (nicht auf der Produktions-DB):
PRAGMA integrity_check= ok--reindex all --rebuildauf dem Restore laeuft durch- Indexanzahl stimmt mit
SELECT COUNT(*) FROM mbox_indexdes Schnappschusses ueberein - Hashabdeckung: jede
copied-Zeile mitbody_sha256 <> ''hat einenmbox_index-Eintrag - Pending-Stufen:
mbox_done/target_doneplausibel, keine Zeile mitmbox_done=1ohne Archivdatensatz
- Der Restore-Test gehoert wiederholt gefahren, nicht einmal. Ein Backup, das nie zurueckgespielt wurde, ist eine Behauptung.