feat: stille Datenverlust-Pfade geschlossen, gemeinsame Shell-Lib (v0.7.0)
Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde. Datenverlust: - veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" — Ergebnis nach error/, Original geloescht (Default delete). run_verapdf() trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das Original wird nicht entsorgt. - Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel daneben, mit Warnung; ProcessResult.output traegt den echten Pfad. Robustheit: - Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback. - RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen. - Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr. - Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt still unter /opt zu landen. - Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail. - Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet. - Logging explizit nach stdout (die Doku versprach das schon). Struktur: - Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung — die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte venv als gesund durchgewunken (nachgewiesen). - install.sh warnt in Containern, wenn systemd-journald nicht laeuft. Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify), Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS, AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte, Exit-Code-Tabelle. 254 Tests gruen (vorher 152). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+183
@@ -1,5 +1,188 @@
|
||||
# Changelog
|
||||
|
||||
## [0.7.0] - 2026-09-23
|
||||
|
||||
Schliesst die stillen Datenverlust-Pfade: gleichnamige Dateien werden nirgends
|
||||
mehr ueberschrieben, und ein nicht aufrufbares veraPDF wird nicht mehr als
|
||||
"PDF ist ungueltig" missverstanden. Dazu Robustheit (Exit 2 statt Traceback,
|
||||
toter Verzeichnis-Watch faellt auf, relative Pfade sind ein Config-Fehler) und
|
||||
eine gemeinsame Shell-Bibliothek fuer install.sh und update.sh.
|
||||
Test-Suite: 254 pytest-Tests gruen.
|
||||
|
||||
### Added
|
||||
- **`lib/common.sh` — gemeinsame Shell-Bibliothek.** `install.sh` und
|
||||
`update.sh` sourcen sie und teilen sich darueber Log-Funktionen
|
||||
(`log_info`/`log_warn`/`log_error`/`log_step`), `require_root`, die
|
||||
Layout-Konstanten (`INSTALL_DIR`, `CONFIG_DIR`, `DATA_ROOT`, `SYSTEMD_DIR`,
|
||||
`DEFAULT_USER`, `SERVICE_TEMPLATE`, `LXC_DROPIN*`), die apt-Paketliste
|
||||
(`pdf_ocr_apt_packages()` zwischen den BEGIN/END-Marken) und die
|
||||
venv-Pruefung (`py_mm`, `pyvenv_cfg_mm`, `venv_is_healthy`,
|
||||
`report_venv_issues`).
|
||||
- Die **Paketliste** steht damit nicht mehr in `install.sh`, sondern in
|
||||
`lib/common.sh`; `update.sh` schneidet sie weiterhin per `sed` aus der
|
||||
**Repo**-Fassung heraus, damit beim Update die neue Liste gilt und nicht
|
||||
die vielleicht aeltere, bereits gesourcte aus der Installation.
|
||||
- `venv_is_healthy()` gab es bisher **zweimal** — die schlanke Variante in
|
||||
`install.sh` haette den Distro-Upgrade-Fall (`pyvenv.cfg` gegen
|
||||
System-Python) nicht erkannt. Jetzt existiert nur noch die gruendliche
|
||||
Fassung, und `install.sh` nennt die Befunde ueber `report_venv_issues`.
|
||||
- `lib/` wird von `install.sh` **und** `update.sh` nach
|
||||
`/opt/pdf-ocr-hotfolder/lib/` mitkopiert und liegt damit im
|
||||
Update-Backup. `update.sh` sourct bevorzugt die Repo-Fassung neben sich
|
||||
und faellt auf die installierte zurueck; fehlt sie ueberall, bricht es
|
||||
sofort ab statt mitten im Lauf mit "command not found".
|
||||
- **veraPDF wird im Preflight geprueft** (`check_verapdf_binary()`,
|
||||
`resolve_verapdf_binary()`). Mit `[verapdf].enabled = true` muss
|
||||
`[verapdf].binary` auf ein vorhandenes, ausfuehrbares Programm zeigen —
|
||||
sonst startet der Dienst gar nicht erst (Exit 2), und `--check-config`
|
||||
meldet den Fehler. Ein Pfad mit `/` wird direkt geprueft, ein nackter Name
|
||||
im `PATH` gesucht. `--check-config` zeigt jetzt ausserdem Binary und Flavour
|
||||
bzw. `(aus)` an.
|
||||
Hintergrund: ein Tippfehler im Pfad war der **gefaehrlichste Fehler des
|
||||
ganzen Dienstes**. `run_verapdf()` fand das Programm fuer JEDE Datei nicht,
|
||||
wertete das als FAIL, schob das OCR-Ergebnis nach `error/` — und
|
||||
`_dispose_original()` entsorgte das Original laut
|
||||
`[output].original_on_success`, bei dessen Default `delete` also Scan fuer
|
||||
Scan die Vorlage. Die Unit stand dabei als `active (running)` da.
|
||||
- **Exit 3: toter Verzeichnis-Watch** (`EXIT_OBSERVER_DEAD` in `service.py`).
|
||||
Stirbt der watchdog-Observer im Betrieb (erschoepftes
|
||||
`fs.inotify.max_user_watches`, ersetztes oder neu gemountetes Verzeichnis),
|
||||
blieb die Unit bisher `active (running)` und verarbeitete nichts mehr — kein
|
||||
Log, keine Mail, niemand merkt es. Die Hauptschleife prueft den Observer
|
||||
jetzt sekuendlich mit, loggt im Ernstfall die moeglichen Ursachen und
|
||||
beendet sich mit Exit 3; `Restart=on-failure` startet den Dienst neu und der
|
||||
Watch wird neu aufgesetzt. Bewusst **nicht** Exit 2 — den unterdrueckt die
|
||||
Unit jetzt beim Neustart (s.u.).
|
||||
- **`ProcessResult.warning`** — erfolgreicher Durchlauf mit Nebenbefund. Bisher
|
||||
gab es nur Erfolg oder Fehler; ein liegengebliebenes Original passte in
|
||||
keine der beiden Schubladen.
|
||||
- **Sammelmeldung fuer Nicht-PDF-Dateien in `incoming/`**
|
||||
(`_report_non_pdf()`). Alles ohne `.pdf`-Endung wurde ignoriert und
|
||||
sammelte sich stumm an (Scanner-Fehlablagen, abgebrochene Uploads,
|
||||
Thumbnails). Beim Start-Scan gibt es jetzt **eine** Warnung mit Anzahl und
|
||||
bis zu drei Beispielnamen — keine Zeile pro Datei und nichts im laufenden
|
||||
Betrieb.
|
||||
- **`install.sh` warnt beim Erstinstall in Containern, wenn
|
||||
`systemd-journald` nicht laeuft.** Der Dienst loggt ausschliesslich nach
|
||||
journald; ist journald kaputt, gibt es gar keine Logs. Die Warnung nennt den
|
||||
Drop-in-Befehl fuer den Debian-13-Fall und fragt, ob fortgefahren werden
|
||||
soll.
|
||||
- **Dokumentation** (siehe unten unter *Docs*): Dateisystem-Festlegung,
|
||||
Debian-13-journald-Befund, konkrete Speicher-Messwerte, Exit-Code-Tabelle.
|
||||
|
||||
### Fixed
|
||||
- **`outgoing/`: gleichnamige Datei wird nicht mehr ueberschrieben.** Liefert
|
||||
der Scanner denselben Dateinamen ein zweites Mal (oder wurde das
|
||||
Vorgaengerergebnis noch nicht abgeholt), legte der `shutil.move` die
|
||||
aeltere Datei kommentarlos um. Neu: `_collision_free_path()` haengt einen
|
||||
Zeitstempel an (`scan.pdf` -> `scan_20260923-081500.pdf`), bei Kollision
|
||||
innerhalb derselben Sekunde zusaetzlich einen Zaehler. Es gibt eine
|
||||
`log.warning`, und `ProcessResult.output` traegt den **tatsaechlich**
|
||||
geschriebenen Pfad — die Upload-Ziele und die Mail nennen damit die richtige
|
||||
Datei.
|
||||
- **Dieselbe Klasse in `error/` und beim Ordner-Upload.** `_move_to_error()`
|
||||
(und damit auch `_rescue_to_error()`) sowie `upload_folder()` mit
|
||||
abweichendem `[upload.folder].target` ersetzten bisher still eine
|
||||
gleichnamige Datei im Ziel. Beide nutzen jetzt denselben
|
||||
Zeitstempel-Ausweg. Scheitert dieselbe `scan.pdf` zweimal, liegen jetzt
|
||||
beide Fassungen in `error/`.
|
||||
- **veraPDF: Stoerung wird nicht mehr als FAIL gewertet.** `run_verapdf()`
|
||||
unterscheidet jetzt zwischen einem echten Urteil (PASS/FAIL) und
|
||||
"Programm nicht aufrufbar, nicht startbar, Timeout oder kein PASS/FAIL in
|
||||
der Ausgabe" — Letzteres wirft `VeraPdfUnavailable`. Im Stoerungsfall
|
||||
wandern **Original UND OCR-Ergebnis** nach `error/`, und das Original wird
|
||||
weder geloescht noch archiviert, unabhaengig von
|
||||
`[output].original_on_success`. Vorher lieferte jeder dieser Faelle
|
||||
schlicht `False` und damit ein Fehlurteil ueber die Datei.
|
||||
- **Kaputtes TOML und nicht lesbare Config beenden den Dienststart mit
|
||||
Exit 2** statt mit einem nackten Traceback. `main()` faengt jetzt
|
||||
`tomllib.TOMLDecodeError` und `OSError` genauso ab wie `--check-config`; die
|
||||
Meldung nennt Zeile und Spalte, sofern der Interpreter sie liefert
|
||||
(`TOMLDecodeError.lineno`/`.colno` gibt es erst ab Python 3.14 — auf
|
||||
Debian 12 steht die Position nur im Meldungstext, deshalb `getattr`).
|
||||
- **Relative Pfade in der Config sind jetzt ein Fehler.** `[paths].incoming`,
|
||||
`outgoing`, `working`, `error` sowie `[output].archive_dir` und
|
||||
`[upload.folder].target` muessen absolut sein (`_require_absolute()`,
|
||||
`ConfigError` + Exit 2). Ein relativer Pfad wurde gegen das
|
||||
`WorkingDirectory` der Unit aufgeloest und landete still unter
|
||||
`/opt/pdf-ocr-hotfolder/` — der Scanner schrieb dann woanders hin als der
|
||||
Dienst schaute, ohne dass irgendwo ein Fehler auftauchte. Leere Werte
|
||||
bleiben erlaubt (`archive_dir`/`target` sind optional).
|
||||
- **Ein Fehler beim Entsorgen des Originals entwertet den Durchlauf nicht
|
||||
mehr.** `_dispose_original()` wirft nicht mehr, sondern liefert eine
|
||||
Meldung zurueck: zum Aufrufzeitpunkt liegt das fertige PDF schon in
|
||||
`outgoing/`, eine Exception von dort haette den gelungenen Durchlauf im
|
||||
Catch-all des Service in einen Fehler verwandelt — mitsamt ausgefallenem
|
||||
Upload. Jetzt gilt der Lauf als Erfolg, Upload und Benachrichtigung laufen,
|
||||
es gibt aber eine `log.error` (Original liegt noch in `working/` und wird
|
||||
beim naechsten Start erneut durch das OCR geschickt), und die Mail geht als
|
||||
**"OK mit Warnung"** auch bei `[notify.email].on = "errors"` raus — sonst
|
||||
waere genau das wieder ein stiller Fehlerpfad.
|
||||
- **Log geht explizit nach stdout.** `logging.basicConfig()` schreibt per
|
||||
Default nach **stderr**; README und `docs/INSTALLATION.md` versprachen aber
|
||||
stdout. Fuer journald egal, fuer den dort beschriebenen
|
||||
Vordergrund-Notbehelf und fuer jede Weiterleitung nicht.
|
||||
|
||||
### Changed
|
||||
- **`systemd/pdf-ocr-hotfolder@.service`: `RestartPreventExitStatus=2`.**
|
||||
Exit 2 = Config- oder Preflight-Fehler, den behebt kein Neustart. Bisher
|
||||
startete `Restart=on-failure` die Instanz endlos im 5-Sekunden-Takt neu
|
||||
(das Start-Rate-Limit greift bei `RestartSec=5` nie). Jetzt bleibt die
|
||||
Instanz sichtbar `failed` stehen.
|
||||
- `HotfolderService.run()` gibt einen **Exit-Code** zurueck (0 oder
|
||||
`EXIT_OBSERVER_DEAD`) statt `None`; `main()` reicht ihn durch. Preflight und
|
||||
`check_output_config()` stehen jetzt gebuendelt in `_preflight()`, das
|
||||
`run()` und `run_once()` gemeinsam nutzen.
|
||||
- `check_preflight()` nimmt zwei weitere Parameter (`verapdf_enabled`,
|
||||
`verapdf_binary`) — beide mit Default, bestehende Aufrufe bleiben gueltig.
|
||||
- `config.example.toml`: Warnhinweise zu absoluten Pfaden (`[paths]`,
|
||||
`[output].archive_dir`, `[upload.folder].target`), zur veraPDF-Preflight-
|
||||
Pruefung samt Begruendung und zum Kollisionsverhalten im Upload-Ziel.
|
||||
- `install.sh` ist von ~73 Zeilen Kopf auf das Sourcen von `lib/common.sh`
|
||||
geschrumpft und nutzt durchgehend `SYSTEMD_DIR`/`LXC_DROPIN` statt
|
||||
hartkodierter Pfade.
|
||||
|
||||
### Docs
|
||||
- **`docs/INSTALLATION.md`, Systemanforderungen: Dateisystem festgelegt.** Der
|
||||
Dienst laeuft ausschliesslich auf **ext4, xfs oder zfs**. `incoming/` gehoert
|
||||
auf ein lokales, inotify-faehiges Dateisystem — **kein CIFS/NFS-Mount**: dort
|
||||
liefert inotify grundsaetzlich keine Events, weil Schreibzugriffe anderer
|
||||
Rechner am lokalen Kernel vorbeigehen. Der Dienst wuerde dann nur noch beim
|
||||
Start etwas verarbeiten und im laufenden Betrieb nichts mehr bemerken.
|
||||
- **Speicher-Messwerte konkretisiert.** Zur bestehenden 512-MB-Messung kommen
|
||||
Zahlen von einem **2-GB-Container** (Debian 13, 300-dpi-A4-Seite mit
|
||||
`deskew`, `oversample = 300`, `jobs = 4`): Laufzeit **19 s**, `MemoryPeak`
|
||||
des Dienstes **380 MB**, `memory.peak` des ganzen Containers **503 MB**,
|
||||
`oom_kill 0`. Auf derselben Maschine mit 512 MB war genau das der OOM-Kill.
|
||||
Die Empfehlung "mindestens 2 GB" ist damit belegt statt geschaetzt.
|
||||
- **Debian 13 in LXC auf Proxmox: journald scheitert — verifizierter Befund,
|
||||
betrifft jede Debian-13-LXC auf Proxmox 8.4.** In 0.6.3 stand noch, das sei
|
||||
ein Schaden auf genau einer Maschine; das war falsch. Ursache: systemd >= 255
|
||||
(Debian 13 hat 257) setzt `ImportCredential=journal.*` in der
|
||||
journald-Unit, der Hilfsprozess `(sd-mkdcreds)` mountet dafuer, und das
|
||||
AppArmor-Profil des Proxmox-Hosts blockiert das
|
||||
(`apparmor="DENIED" operation="mount" profile="lxc-<id>_</var/lib/lxc>"
|
||||
name="/dev/" comm="(sd-mkdcreds)"` im Host-Log). Debian 12 (systemd 252)
|
||||
kennt `ImportCredential` nicht und ist nicht betroffen. Dokumentiert sind
|
||||
die reboot-feste Abhilfe im Container (Drop-in
|
||||
`systemd-journald.service.d/no-credentials.conf` mit leerem
|
||||
`ImportCredential=`), der Hinweis auf die weiteren betroffenen Units
|
||||
(logind, networkd, console-getty, tmpfiles-setup) und der saubere Weg
|
||||
host-seitig.
|
||||
- **Exit-Code-Tabelle 0/1/2/3** in `docs/INSTALLATION.md`, verlinkt aus
|
||||
README, `docs/UPDATE.md` und dem Troubleshooting — mit dem Hinweis, dass die
|
||||
Unit bei 2 **nicht** neu startet und bei 3 gerade doch.
|
||||
- Die fuenf Ueberschriften der Instanz-Abfragen in `docs/INSTALLATION.md` sind
|
||||
**entnummeriert** (`### 4. OCR-Sprachen` -> `### OCR-Sprachen`). Die Anker
|
||||
hiessen vorher `#4-ocr-sprachen` und waeren bei jeder Umsortierung
|
||||
gebrochen; die Reihenfolge steht weiterhin im Text.
|
||||
- `AI_AGENT_BRIEFING.md` auf den heutigen Stand gezogen: Dateibaum mit `lib/`
|
||||
und allen 20 Test-Dateien, nur noch **ein** `venv_is_healthy()`,
|
||||
Paketliste in `lib/common.sh`, veraPDF-Preflight, Kollisionsschutz,
|
||||
Exit-Codes, Observer-Bewachung, Testzahl 254.
|
||||
- Testzahl ueberall von 152 auf **254** korrigiert (README,
|
||||
`AI_AGENT_BRIEFING.md`, `docs/OS-UPGRADE.md`).
|
||||
|
||||
## [0.6.3] - 2026-09-23
|
||||
|
||||
Reine Doku-Version — kein Code, kein Installer, kein Updater, keine Unit, keine
|
||||
|
||||
Reference in New Issue
Block a user