diff --git a/INSTALL.md b/INSTALL.md index 54f26cf..b4dadba 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -23,9 +23,14 @@ an, nicht hier. ## 3. Starten (Web) ```sh -./mail-graveyard # lauscht auf 127.0.0.1:8087 +./mail-graveyard --init-db # nur beim ersten Start: DB aus db_path anlegen +./mail-graveyard # danach normal, lauscht auf 127.0.0.1:8087 ``` +Fehlt die DB-Datei spaeter, startet Mail-Graveyard bewusst nicht mehr still mit +einer leeren Datenbank. Dann `db_path` pruefen oder nur fuer eine echte +Neuinstallation erneut `--init-db` benutzen. + Browser auf `http://127.0.0.1:8087`, mit Admin-Login aus `config.json`. Oberfläche im Outlook-2013-Look: Reiter **Postfächer** (Konten anlegen: Quelle alt → Ziel neu, Sicherheits-Modus `tls`/`starttls`/`none`, „Test" prüft beide diff --git a/backend/02-database.go b/backend/02-database.go index e2f7662..04ad4ce 100644 --- a/backend/02-database.go +++ b/backend/02-database.go @@ -64,10 +64,13 @@ type AppUser struct { // Zweiter Lauf kopiert nur das Delta. // jobs(account_id, started, finished, -- Lauf-Historie fuer den Fortschritt // total, done, errors, state) im Browser. -func ConnectDB() error { +func ConnectDB(initDB bool) error { if Cfg.DBPath == "" { Cfg.DBPath = "mail-graveyard.db" } + if err := ensureDBFile(Cfg.DBPath, initDB); err != nil { + return err + } db, err := sql.Open("sqlite", Cfg.DBPath) if err != nil { return err @@ -167,6 +170,27 @@ func ConnectDB() error { return nil } +func ensureDBFile(path string, initDB bool) error { + info, err := os.Stat(path) + if err == nil { + if info.IsDir() { + return fmt.Errorf("DB %q ist ein Verzeichnis", path) + } + return nil + } + if !errors.Is(err, os.ErrNotExist) { + return err + } + if !initDB { + return fmt.Errorf("DB %q existiert nicht -- mit --init-db neu anlegen oder db_path pruefen", path) + } + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return err + } + return f.Close() +} + func ensureAppUserColumns() error { hasDisplayName := false rows, err := DB.Query(`PRAGMA table_info(app_users)`) diff --git a/db-rename-brief.md b/db-rename-brief.md new file mode 100644 index 0000000..b01d0b7 --- /dev/null +++ b/db-rename-brief.md @@ -0,0 +1,94 @@ +# Codex-Brief — DB-Namen-Altlast beseitigen (stiller Daten-GAU) + +**Priorität: vor der ersten echten `@dr-gold.de`-Migration.** Das ist kein +Kosmetikproblem. + +## Der Befund + +| Stelle | `db_path` | +|---|---| +| `config.json.example` | `mail-graveyard.db` | +| `INSTALL.md` (`cp config.json.example config.json`) | → `mail-graveyard.db` | +| Code-Default in `ConnectDB` (02-database.go) | `mail-graveyard.db` | +| **LIVE auf GoldPi** | **`/app/data/emailforwarder.db`** ← hier liegen die echten Daten | + +Im `data/`-Verzeichnis liegen aktuell: +``` +emailforwarder.db 380.928 <- die ECHTEN Daten +emailforwarder.db-wal 4.124.152 <- ACHTUNG: groesser als die DB selbst! +emailforwarder.db-shm 32.768 +mail-graveyard.db 0 <- Leiche +email-forwarder.db 0 <- Leiche +``` + +**Der Schaden, wenn der Default je greift** (jemand folgt der eigenen +INSTALL.md, oder `db_path` fehlt in einer Config): SQLite legt klaglos eine +**frische, leere DB** an. Konten weg, `copied`-Historie weg. Der Watcher sieht +eine leere `copied`-Tabelle und **kopiert jede Mail erneut in die +Ziel-Postfaecher** → **Dubletten quer ueber alle ~20 echten dr-gold-Postfaecher**, +lautlos, ohne einen einzigen Fehler im Log. + +## Teil A — Die Daten umziehen (einmalige Ops-Aktion) + +**Ist-Zustand als Baseline (muss danach identisch sein; Stand vor Codex-Umzug):** +`accounts=19`, `copied=215`, `app_users=3`, `jobs=294` + +Reihenfolge ist wichtig — **das WAL ist der gefaehrliche Teil**: + +```sh +APP=/mnt/DATA/Containers/Test-Container/Mail-Graveyard-Pod +D=$APP/data + +# 1. BEIDE Container stoppen -> kein Schreiber mehr auf der DB +podman stop mail-graveyard mail-graveyard-watch + +# 2. Backup, bevor irgendwas angefasst wird (inkl. WAL/SHM!) +cp -a $D/emailforwarder.db $D/BACKUP-emailforwarder.db +cp -a $D/emailforwarder.db-wal $D/BACKUP-emailforwarder.db-wal + +# 3. WAL in die Haupt-DB falten. OHNE DAS gehen 4 MB Daten verloren. +sqlite3 $D/emailforwarder.db "PRAGMA wal_checkpoint(TRUNCATE);" + +# 4. Gegenpruefen, dass alles noch da ist +sqlite3 $D/emailforwarder.db "SELECT + (SELECT COUNT(*) FROM accounts), (SELECT COUNT(*) FROM copied), + (SELECT COUNT(*) FROM app_users), (SELECT COUNT(*) FROM jobs);" +# -> MUSS 19|215|3|294 liefern. Wenn nicht: STOPP, nichts weiter tun. + +# 5. Erst jetzt umbenennen (ueberschreibt die 0-Byte-Leiche) +mv -f $D/emailforwarder.db $D/mail-graveyard.db +rm -f $D/emailforwarder.db-wal $D/emailforwarder.db-shm $D/email-forwarder.db + +# 6. Config umstellen +# $APP/config.json: "db_path": "/app/data/mail-graveyard.db" +``` + +## Teil B — Die Ursache beseitigen (Code + Repo) + +1. `deploy/goldpi-test-container/mail-graveyard-pod/config.json` **und** + `config.json.example`: `db_path` auf `mail-graveyard.db` — damit Doku, Deploy + und Live endlich dasselbe sagen. +2. **Kein stilles Anlegen mehr.** Das ist die eigentliche Wurzel: `ConnectDB` + erzeugt heute klaglos eine leere DB, wenn die Datei fehlt. Bau eine + Sicherung ein: + - Wenn die DB-Datei **nicht existiert**, nur dann anlegen, wenn explizit + `--init-db` uebergeben wurde. Sonst: **laut scheitern** mit + `DB %q existiert nicht — mit --init-db neu anlegen oder db_path pruefen` + und Exit ≠ 0. + - Damit wird aus einem stillen Daten-GAU ein sofort sichtbarer Startfehler. + Dasselbe Prinzip wie beim Watch-Fail-Fast: **permanenter Konfigfehler muss + schreien, nicht schweigen.** + +## Abnahme + +1. Container wieder hoch, dann in der DB pruefen: + `accounts=19`, `copied=215`, `app_users=3`, `jobs=289` — **identisch**. +2. **Der entscheidende Test:** Der erste Watcher-Lauf nach dem Umzug muss + `copied=0` melden (`total=215 copied=0 errors=0`). + → Meldet er stattdessen `copied=215`, ist die Dedup-Historie verloren + gegangen und er schreibt gerade Dubletten ins Ziel. **Sofort stoppen** und + das Backup aus Schritt 2 zurueckspielen. +3. Login in der Web-UI funktioniert weiter (die 3 Benutzer sind mitgezogen). +4. `go test ./...` gruen. + +Ich (Claude) verifiziere danach Punkt 1–3 unabhaengig auf GoldPi. diff --git a/main.go b/main.go index 4542e88..4c93db6 100644 --- a/main.go +++ b/main.go @@ -23,12 +23,13 @@ func main() { watchInterval := flag.Duration("watch-interval", 60*time.Second, "Pause zwischen Watch-Durchlaeufen") seedAccount := flag.String("seed-account", "", "Konto aus lokaler JSON-Datei in die DB schreiben (nur CLI/Test)") checkAccount := flag.String("check-account", "", "Konto-Logins pruefen und INBOX zaehlen, ohne zu kopieren") + initDB := flag.Bool("init-db", false, "SQLite-DB neu anlegen, falls db_path noch nicht existiert") flag.Parse() if err := backend.LoadConfig("config.json"); err != nil { log.Fatalf("config: %v", err) } - if err := backend.ConnectDB(); err != nil { + if err := backend.ConnectDB(*initDB); err != nil { log.Fatalf("database: %v", err) } if err := backend.InitAuth(); err != nil {