465ff8873f
Befunde aus dem ersten echten Erstinstallations-Test auf frischen Debian-12- und Debian-13-Containern. Der Weg selbst hat getragen (Basis-Install, Instanz-Anlage, zweite Instanz, Update mit Rauchtest, Rollback) — diese Stellen haben gelogen oder gefehlt: - Das Ghostscript-Backports-Angebot auf Debian 12 war ein garantierter Leerlauf, der "aktualisiert ✓" meldete: bookworm-backports enthaelt gar kein ghostscript (am Paketindex verifiziert). Die Routine sucht jetzt den echten Kandidaten, vergleicht vorher/nachher und raeumt eine nur zur Probe angelegte Quelle wieder weg. - Derselbe untaugliche Rat stand in der Preflight-Meldung, der pdfa_level-Warnung, config.example.toml und vier Doku-Dateien — ueberall ersetzt durch die echten Optionen. - Mehrzeilige Log-Hinweise waren durch "echo -e" zerrissen und nicht kopierbar; log_* nutzt jetzt printf mit %s. - pip-freeze.txt landete beim Rollback als /pip-freeze.txt im Wurzelverzeichnis, liegt jetzt unter opt/pdf-ocr-hotfolder/. - git und sudo fehlen auf dem Proxmox-Debian-Template; "sudo ./install.sh" scheitert dort. Beide Wege dokumentiert, git als Voraussetzung ergaenzt, HTTPS-Clone als Normalfall. - Rollback: systemctl start kann kein Glob. journald-Reparatur: Instanzen danach neu starten, sonst bleibt das Journal leer. 254 Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
732 lines
43 KiB
Markdown
732 lines
43 KiB
Markdown
# Changelog
|
||
|
||
## [0.7.1] - 2026-09-23
|
||
|
||
Gefunden beim ersten echten Erstinstallations-Test auf frischen Debian-12-
|
||
und Debian-13-Containern. Der Installationsweg selbst hat getragen; diese
|
||
drei Stellen haben gelogen oder gefehlt.
|
||
|
||
### Fixed
|
||
- **Das Ghostscript-Angebot auf Debian 12 war ein No-Op, der sich als Erfolg
|
||
meldete** (`Ghostscript aktualisiert: 10.00.0 -> 10.00.0 ✓`). Grund:
|
||
`bookworm-backports` enthaelt ueberhaupt kein `ghostscript` — am echten
|
||
Paketindex verifiziert (2606 Pakete, ghostscript nicht dabei, Kontroll-
|
||
pakete wie systemd/golang-go sehr wohl). Zurueck blieb eine nutzlose
|
||
`sources.list.d`-Quelle. Die Routine (jetzt `check_ghostscript` in
|
||
`lib/common.sh`) sucht nun erst den tatsaechlichen Kandidaten, vergleicht
|
||
die Version vorher/nachher, meldet "unveraendert" als Warnung statt als
|
||
Erfolg und entfernt eine nur zur Probe angelegte Quelle wieder. Eine
|
||
bereits vorhandene Backports-Quelle bleibt unangetastet.
|
||
- **Der Rat "Ghostscript aus bookworm-backports" ist ueberall raus** — er
|
||
stand auch in der Preflight-Fehlermeldung, der pdfa_level-Warnung,
|
||
config.example.toml und vier Doku-Dateien. Ersetzt durch die echten
|
||
Optionen: pdfa_level leer lassen (Default), skip_text = false, oder
|
||
Debian 13 (gs 10.05.1). Ein Test prueft negativ, dass der Rat nicht
|
||
zurueckkommt.
|
||
- **Mehrzeilige Log-Hinweise waren zerrissen und nicht kopierbar**: die
|
||
log-Funktionen nutzten `echo -e`, wodurch `\n` in Hinweistexten zu echten
|
||
Zeilenumbruechen ohne `[WARN]`-Praefix wurden. Jetzt `printf` mit `%s`
|
||
fuer den Text — das Problem kann strukturell nicht wiederkommen.
|
||
- **`pip-freeze.txt` landete beim Rollback im Wurzelverzeichnis.** Sie liegt
|
||
im Backup jetzt unter `opt/pdf-ocr-hotfolder/` und damit nach dem
|
||
Entpacken neben der Installation.
|
||
|
||
### Added (Doku)
|
||
- Voraussetzungen, die auf einem frischen Proxmox-Debian-Template fehlen:
|
||
**`git` und `sudo`** sind dort nicht installiert — der dokumentierte
|
||
Aufruf `sudo ./install.sh` scheitert mit `sudo: command not found`.
|
||
Beide Wege (root direkt / sudo) sind jetzt beschrieben.
|
||
- HTTPS- statt SSH-Clone als Normalfall (funktioniert ohne Credentials);
|
||
SSH-Variante mit dem Hinweis, dass der User `gitea` heisst, nicht `git`.
|
||
- Entscheidungstabelle zu PDF/A auf Debian 12 vs. 13. Praezisierung: die
|
||
Sackgasse ist PDF/A **zusammen mit** `skip_text = true`; mit
|
||
`skip_text = false` geht PDF/A auch auf Debian 12, nur langsamer.
|
||
- Rollback: `systemctl start` kann kein Glob (anders als `stop`), bei
|
||
mehreren Instanzen jede einzeln starten. Dazu, was ein Rollback
|
||
nachweislich zurueckholt und was nicht.
|
||
- journald-Reparatur: die Instanzen danach einmal neu starten, sonst bleibt
|
||
das Journal leer und die Reparatur sieht gescheitert aus.
|
||
- "So sieht ein Erstlauf aus" — die Reihenfolge der Abfragen.
|
||
|
||
## [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
|
||
Tests angefasst (ausser dem Versionsstring). Beide Erkenntnisse stammen aus den
|
||
Testlaeufen auf Debian 12 und Debian 13.
|
||
|
||
### Added
|
||
- **Abschnitt "Systemanforderungen" in `docs/INSTALLATION.md`.** Die
|
||
Dimensionierung fehlte bisher komplett. Empfohlen werden **mindestens 2 GB
|
||
RAM**; 512 MB reichen fuer 300-dpi-Scans nachweislich nicht. Gemessen auf
|
||
einem LXC-Container mit 512 MB RAM + 512 MB Swap, Debian 13,
|
||
Ghostscript 10.05.1: eine **einzelne A4-Seite in 300 dpi** mit
|
||
`deskew = true` riss das cgroup-Limit und der Dienst wurde vom OOM-Killer
|
||
beendet (`oom-kill:constraint=CONSTRAINT_MEMCG, oom_memcg=/lxc/201,
|
||
task=python`, `total-vm:1168764kB, anon-rss:489676kB`). Dieselbe
|
||
Verarbeitung mit einer kleineren Seite (850x1100 px) lief in ca. 19 s sauber
|
||
durch.
|
||
- Dokumentiert ist der Zusammenhang **RAM <-> `max_workers` x `jobs` x
|
||
Aufloesung**: `max_workers` (Default 2) laesst zwei solcher Seiten
|
||
gleichzeitig laufen, der Spitzenbedarf multipliziert sich entsprechend.
|
||
Wer knapp dimensioniert, zieht zuerst `max_workers` herunter.
|
||
- Dazu, **wie sich ein OOM-Kill aeussert** (Dienst weg bzw. von systemd neu
|
||
gestartet, `NRestarts` steigt, Abbruch mitten in der Datei ohne Traceback,
|
||
PDF bleibt in `working/` liegen) und **wie man ihn nachweist**
|
||
(`/sys/fs/cgroup/memory.events` im Container, `dmesg` auf dem LXC-Host).
|
||
Ohne diese Pruefung sieht der Fall wie ein Anwendungsfehler aus und man
|
||
sucht in ocrmypdf, Tesseract oder der Config.
|
||
- Mit dem Hinweis, dass die Wiederaufnahme aus `working/` seit v0.6.0 den
|
||
Datenverlust abfaengt — der OOM selbst bleibt aber ein Problem: bei
|
||
unveraenderter Dimensionierung laeuft dieselbe Datei nach dem Neustart
|
||
erneut hinein.
|
||
- `README.md` bekommt im Schnellstart nur einen Einzeiler mit Verweis, keine
|
||
Dublette.
|
||
- **Troubleshooting-Eintrag "Keine Logs: No journal files were found" in
|
||
`docs/INSTALLATION.md`.** Auf einem der Testcontainer war
|
||
`systemd-journald.service` kaputt (`failed`, `status=243/CREDENTIALS`),
|
||
`journalctl` lieferte `No journal files were found.` Da der Dienst seit
|
||
v0.4.1 **ausschliesslich** nach journald loggt (kein FileHandler, kein
|
||
Logverzeichnis — bewusste Entscheidung), gibt es dann gar keine Dienstlogs:
|
||
ein blinder Fleck, der vor jeder Fehlersuche per
|
||
`systemctl status systemd-journald` auszuschliessen ist. Als Notbehelf ist
|
||
der Vordergrund-Aufruf dokumentiert
|
||
(`cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./venv/bin/python -m
|
||
pdf_ocr_hotfolder --config ...`, das `cd` ist zwingend, s. 0.6.2).
|
||
Ausdruecklich festgehalten: das ist **kein** generelles LXC-Muster — der
|
||
zweite Testcontainer war in Ordnung, es war Schaden auf genau dieser
|
||
Maschine.
|
||
|
||
### Changed
|
||
- `AI_AGENT_BRIEFING.md`: der Kommentar zu `requirements.txt` in der
|
||
Projektstruktur nannte noch "ocrmypdf 16.x!" — genau die Version, die seit
|
||
0.6.1 als unbrauchbar gilt und gegen die der Pin schuetzt. Korrigiert auf
|
||
17.x.
|
||
|
||
## [0.6.2] - 2026-09-22
|
||
|
||
### Fixed
|
||
- Der `--check-config`-Befehl, den `update.sh` in der Zusammenfassung ausgibt,
|
||
lief so wie gedruckt nicht (`No module named pdf_ocr_hotfolder`). Das Paket
|
||
wird nicht pip-installiert, sondern nach `/opt/pdf-ocr-hotfolder` kopiert und
|
||
nur ueber das Arbeitsverzeichnis gefunden — dem Hinweis fehlte das
|
||
vorangestellte `cd`. Betraf auch die Beispiele in README.md,
|
||
docs/INSTALLATION.md, docs/UPDATE.md und docs/OS-UPGRADE.md.
|
||
Gefunden beim Update-Test v0.3.1 -> v0.6.1 auf Debian 12.
|
||
|
||
## [0.6.1] - 2026-09-22
|
||
|
||
> **Fuer Bestandsinstallationen wichtig.** Wer 0.6.0 bereits eingespielt hat,
|
||
> laeuft auf Debian 12 mit hoher Wahrscheinlichkeit im Totalausfall: der Dienst
|
||
> meldet `active`, `--check-config` meldet "Preflight ok" — und **jede** PDF
|
||
> landet in `error/`. Nach dem Update auf 0.6.1 nachsehen, ob in `error/`
|
||
> unverarbeitete Dateien liegen, und diese zurueck nach `incoming/` schieben.
|
||
> Der Updater faehrt jetzt selbst einen Rauchtest, der so einen Zustand sofort
|
||
> aufdeckt.
|
||
|
||
### Fixed
|
||
- **ocrmypdf-Pin von 16.13.0 auf 17.4.1 korrigiert — das war ein stiller
|
||
Totalausfall.** 0.6.0 pinnte `ocrmypdf==16.13.0`. Auf Bestandssystemen mit
|
||
vorher `ocrmypdf>=16.0` war das ein **Downgrade** von 17.4.1, und auf
|
||
Debian 12 (Ghostscript 10.0.0) bricht ocrmypdf 16.13.0 bei jeder PDF ab:
|
||
|
||
```
|
||
MissingDependencyError: Ghostscript 10.0.0 through 10.02.0 (your version:
|
||
10.0.0) contain serious regressions that corrupt PDFs with existing text
|
||
```
|
||
|
||
Ursache, verifiziert im Quelltext von
|
||
`ocrmypdf/builtin_plugins/ghostscript.py::check_options()`: bis
|
||
einschliesslich 16.x laeuft die Ghostscript-Pruefung **bedingungslos** —
|
||
`skip_text=true` allein genuegt, `output_type` wird gar nicht geprueft,
|
||
obwohl die Fehlermeldung selbst `--output-type pdf` empfiehlt. Ab **17.0.0**
|
||
umschliesst denselben Block ein
|
||
`if options.output_type.startswith('pdfa'):`; ohne PDF/A wird Ghostscript
|
||
nicht angefasst. `run_ocr()` setzt bei leerem `pdfa_level` genau
|
||
`output_type="pdf"` und hielt sich damit faelschlich fuer sicher.
|
||
|
||
Betroffen war nicht nur das Update, sondern ebenso jede **Neuinstallation**:
|
||
`skip_text = true` ist der Default. — 17.4.1 ist die auf Debian 12 + gs 10.0.0
|
||
real verifizierte Version; `skip_text` bleibt in 17.x als Alias fuer
|
||
`mode='skip'` unterstuetzt, `run_ocr()` musste nicht angepasst werden.
|
||
|
||
- **Preflight prueft jetzt die reale Bedingung.** `check_preflight()` sah die
|
||
Ghostscript-Version bisher nur bei gesetztem `pdfa_level` an (`if pdfa_level:`)
|
||
— genau deshalb ging der kaputte Zustand als "Preflight ok" durch. Die neue
|
||
Bedingung (`_gs_block_reason()`) bildet ocrmypdf nach:
|
||
|
||
```
|
||
betroffene GS-Version UND skip_text UND (pdfa_level ODER ocrmypdf < 17)
|
||
```
|
||
|
||
Die Signatur ist jetzt `check_preflight(pdfa_level, skip_text)`; alle
|
||
Aufrufstellen (`run()`, `run_once()`, `--check-config`) reichen beides durch.
|
||
`redo_ocr` steht bewusst **nicht** in der Bedingung: die Config kennt keinen
|
||
solchen Key, und ein erfundener waere schlimmer als ein fehlender.
|
||
|
||
Ergebnis: der Dienst bricht beim **Start** mit Exit 2 ab statt bei der ersten
|
||
Datei, und `--check-config` meldet den Zustand als **Fehler** (Exit 2) — also
|
||
auch mitten im Update. Die Meldung nennt beide Auswege: Ghostscript >= 10.02.1
|
||
aus bookworm-backports (der Installer bietet das an) oder
|
||
`[ocr].skip_text = false`.
|
||
|
||
### Added
|
||
- **`update.sh` macht Versionsspruenge der Kernabhaengigkeiten sichtbar.** Die
|
||
Versionen der in `requirements.txt` gepinnten Pakete werden vor und nach
|
||
`pip install` gemessen; Downgrades erscheinen als `[WARN]`, Upgrades und neue
|
||
Pakete als `[INFO]`, beides zusaetzlich in der Abschluss-Zusammenfassung. Das
|
||
Downgrade 17.4.1 -> 16.13.0 verschwand bisher wortlos hinter
|
||
`[INFO] Dependencies ok ✓`.
|
||
- **Rauchtest in `update.sh`.** Nach dem Start jeder Instanz geht eine winzige
|
||
Test-PDF durch die **echte** Pipeline; der Test gilt als bestanden, wenn sie
|
||
in `outgoing/` ankommt. Erst das deckt einen Totalausfall auf, den systemd
|
||
nicht sieht.
|
||
- Die Test-PDF (694 Bytes, eine Seite) steckt als base64 im Skript — kein
|
||
Pillow, kein `gs`, kein `convert` noetig.
|
||
- Eindeutiger Dateiname (`__smoketest_update_<zeitstempel>_<pid>.pdf`), der
|
||
mit keiner Kundendatei kollidieren kann.
|
||
- **Raeumt restlos auf** — Testdatei und Ergebnis, in `incoming/`, `working/`
|
||
(inkl. `__ocr_`-Zwischendatei), `outgoing/`, `error/` und Archiv, auch bei
|
||
Fehlschlag und Timeout.
|
||
- **Uebersprungen** bei Instanzen mit aktivem `[upload.nextcloud]`,
|
||
`[upload.sftp]`, `[notify.email]` oder `[upload.folder]` mit gesetztem
|
||
`target`: dort wuerde die Testdatei nach aussen gehen, im Zweifel zum
|
||
Kunden. `[upload.folder]` ohne `target` schreibt nach `outgoing/` und ist
|
||
harmlos.
|
||
- Wartezeit `SMOKE_TIMEOUT` (Standard 90 s), danach durchgefallen — das
|
||
Skript haengt nicht.
|
||
- Ein Fehlschlag setzt den Exit-Code auf 1 und nennt den `journalctl`-Befehl,
|
||
rollt aber **nichts** zurueck.
|
||
- Abschaltbar mit `--no-smoke-test`, dokumentiert in `--help`.
|
||
- `--check-config` zeigt zusaetzlich `skip_text` sowie die installierte
|
||
ocrmypdf- und Ghostscript-Version an.
|
||
|
||
### Changed
|
||
- **Doku praezisiert.** `config.example.toml`, `config.py`,
|
||
`docs/INSTALLATION.md` und `docs/UPDATE.md` behaupteten sinngemaess,
|
||
`pdfa_level = ""` sei der sichere Default gegen den Ghostscript-Bug. Das
|
||
stimmt so nicht: die Entwarnung haengt an der ocrmypdf-Version und gilt erst
|
||
ab 17. Der Ghostscript-Abschnitt in `INSTALLATION.md` stellt die Bedingung
|
||
jetzt je ocrmypdf-Major gegenueber und nennt `skip_text = false` als zweiten
|
||
Weg; `UPDATE.md` beschreibt Rauchtest und Versionssprung-Meldung.
|
||
- Kommentarblock in `requirements.txt` korrigiert: der Schutz gilt gegen den
|
||
naechsten ungewollten Major-Sprung (18), **nicht** gegen 17 — samt
|
||
Begruendung, warum 16.x fuer uns unbrauchbar ist.
|
||
|
||
### Tests
|
||
- 152 statt 135 Tests. Neu: die Preflight-Matrix aus GS-Version x `skip_text` x
|
||
`pdfa_level` x ocrmypdf-Major — darunter der Fall, der durchrutschte
|
||
(betroffene GS-Version + `skip_text=true` + leeres `pdfa_level` +
|
||
ocrmypdf 16.x muss `PreflightError` ausloesen) und die Gegenprobe, dass
|
||
dieselbe Config mit ocrmypdf 17.x **nicht** ausloest (sonst startet keine
|
||
Debian-12-Bestandsinstanz mehr). Dazu `ocrmypdf_checks_gs_always()`, der
|
||
Abbruch in `run_once()` und Exit 2 bei `--check-config`.
|
||
|
||
## [0.6.0] - 2026-09-22
|
||
|
||
### Added
|
||
- **Wiederaufnahme aus `working/` beim Start.** `process_pdf()` verschiebt das
|
||
Original vor dem OCR nach `working/`. Wurde der Dienst dort hart abgeschossen
|
||
(SIGKILL nach `TimeoutStopSec`), blieb die Datei liegen und wurde **nie wieder
|
||
angefasst** — stiller Datenverlust. `_scan_working()` greift sie jetzt beim
|
||
Start auf (vor `incoming/`), das OCR laeuft fuer sie neu. Liegt in `incoming/`
|
||
eine gleichnamige, andere Datei, bekommt die wiederaufgenommene einen
|
||
Zeitstempel angehaengt, damit sich beide nicht ueberschreiben. Liegt in
|
||
`working/` bereits eine andere Datei desselben Namens, bricht `process_pdf()`
|
||
fuer die neue ab und laesst sie in `incoming/` liegen, statt den laufenden
|
||
Vorgang stillschweigend zu ueberschreiben.
|
||
- **Unvollstaendige OCR-Fragmente werden geloescht.** Die Zwischendatei, in die
|
||
ocrmypdf schreibt, traegt jetzt das Praefix `__ocr_` (`OCR_TEMP_PREFIX`).
|
||
Bleibt so eine Datei nach einem harten Stopp in `working/` liegen, ist sie als
|
||
Eingabe unbrauchbar und als Ergebnis wertlos — sie wird beim Start mit einer
|
||
Warnung entfernt, damit sie niemand fuer ein fertiges PDF haelt.
|
||
- **`--check-config`**: prueft eine Instanz-Config, ohne irgendetwas zu
|
||
verarbeiten (hat Vorrang vor `--once`). Zeigt die vier Pfade inkl. Hinweis auf
|
||
noch fehlende Verzeichnisse, Sprachen, Seiten-Timeout und PDF/A-Level, faehrt
|
||
Preflight und `[output]`-Validierung und gibt alle Warnungen aus.
|
||
Exit **0** = sauber, **1** = nur Warnungen, **2** = Fehler (Dienst wuerde nicht
|
||
starten).
|
||
- **Legacy-Warnungen fuer `[ocr].timeout` und `[ocr].pdfa_level`.** Ein
|
||
`timeout >= 900` stammt fast sicher aus einer Config vor 0.4.0, wo der Wert ein
|
||
wirkungsloses Gesamt-Timeout mit Default 1800 war — seither sind es Sekunden
|
||
**pro Seite** (Richtwert 300). Ein gesetztes `pdfa_level` weist auf den
|
||
Ghostscript-Bug hin. Die Texte stehen nur in `config.py`
|
||
(`legacy_warnings()`), weil sie sowohl beim Dienststart ins Log gehen als auch
|
||
von `--check-config` ausgegeben werden.
|
||
- **Unbekannte Config-Keys werden gemeldet** statt still verworfen.
|
||
`_collect_unknown_keys()` sammelt Tippfehler (`[ocr].langauges`), Optionen aus
|
||
aelteren Versionen, unbekannte Sektionen und unbekannte Upload-/Notify-Targets
|
||
in `Config.unknown_keys`; die Meldung nennt den vollen Pfad. Warnung, kein
|
||
Fehler — der Dienst startet, der Eintrag tut nur nichts.
|
||
- **`TimeoutStopSec=300` in der Template-Unit**: ein laufendes OCR darf beim
|
||
Stoppen zu Ende laufen. Ein `systemctl stop` kann dadurch pro Instanz bis zu
|
||
5 Minuten dauern — das ist gewollt, ein SIGKILL wuerde den Durchlauf kosten.
|
||
- **Feste Pins in `requirements.txt`** (`ocrmypdf==16.13.0`, `watchdog==6.0.0`,
|
||
`requests==2.33.1`, `paramiko==4.0.0`). Ohne Pins zieht ein
|
||
`pip install --upgrade` beim Update ungefragt einen Major-Sprung ein; ocrmypdf
|
||
16 -> 17 wuerde alle Instanzen auf einmal reissen. Geprueft gegen Python 3.11
|
||
(Debian 12) und 3.13 (Debian 13), Wheels fuer beide vorhanden.
|
||
- 40 neue Tests (Wiederaufnahme aus `working/`, `--check-config`,
|
||
Config-Warnungen). Suite jetzt **135 Tests**.
|
||
|
||
### Changed
|
||
- **Die Betriebsdoku ist in drei Dokumente aufgeteilt.** Der README ist wieder
|
||
der Einstieg (Kurzbeschreibung, Features, Schnellstart, Verzeichnis-Layout,
|
||
Config-Ueberblick) und verlinkt:
|
||
- `docs/INSTALLATION.md` — Erstinstallation, Basis-Install vs. Instanz-Anlage,
|
||
die Abfragen pro Instanz, Multi-Instanz-Betrieb, LXC (Error 226/NAMESPACE),
|
||
Ghostscript auf Debian 12, Instanz manuell loeschen und die vollstaendige
|
||
**Konfigurationsreferenz**.
|
||
- `docs/UPDATE.md` — Ablauf von `update.sh`, was es nicht anfasst,
|
||
Backup-Inhalt/-Rechte/-Rotation, Rollback und dessen Grenzen,
|
||
`--check-config` mit den Exit-Codes und Config-Drift.
|
||
- `docs/OS-UPGRADE.md` — Debian-Major-Upgrade als eigener Ablauf.
|
||
`AI_AGENT_BRIEFING.md` bleibt der Agent-Kontext (Aufbau und Begruendungen) und
|
||
verweist fuer Ablaeufe auf die drei Dokumente, statt sie zu wiederholen.
|
||
Nichts wird doppelt gepflegt.
|
||
- **`update.sh` komplett ueberarbeitet** (`--help`, `--rebuild-venv`,
|
||
`set -Eeuo pipefail`):
|
||
- **venv-Health-Check und Neubau.** Geprueft werden Existenz, Lauffaehigkeit
|
||
des Interpreters, `major.minor` gegen das System-Python und `pyvenv.cfg`.
|
||
Passt etwas nicht — typisch nach einem Debian-Major-Upgrade, systemd meldet
|
||
dann `203/EXEC` —, wird die venv neu gebaut, auch ohne `--rebuild-venv`. Der
|
||
Neubau ist ganz oder gar nicht: alte venv weg sichern, neu bauen,
|
||
Requirements installieren, **erst bei Erfolg** die alte loeschen; scheitert
|
||
etwas, wird zurueckgerollt und hart abgebrochen. Scheitert pip an einem Pin,
|
||
nennt das Skript das gescheiterte Paket und den naechsten Schritt
|
||
("requirements.txt anheben").
|
||
- **apt-Sync auch beim Update.** Die Paketliste wird aus `install.sh`
|
||
extrahiert (einzige Quelle, Marken `BEGIN/END apt-packages`) und
|
||
installiert; nachinstallierte Tesseract-Sprachpakete bleiben unangetastet
|
||
(kein purge, kein autoremove). Fehlschlaege warnen nur.
|
||
- **Haertere Verifikation.** Nach dem Start prueft `verify_unit()` nicht nur
|
||
`is-active`, sondern auch `is-failed` und den Restart-Zaehler — ein
|
||
Crash-Loop galt bei `Type=simple` bisher als Erfolg. Die Zusammenfassung
|
||
stellt Soll gegen Ist und meldet eine **Regression** namentlich.
|
||
- **Vollstaendiges Backup.** Gesichert werden Code, alle Instanz-Configs, die
|
||
Template-Unit, alle Drop-ins und ein `pip-freeze.txt` der alten venv — ohne
|
||
venv und ohne Datenverzeichnisse. Weil die Configs Klartext-Passwoerter
|
||
enthalten, wird das Archiv mit `umask 077` erzeugt und auf `0600 root:root`
|
||
gesetzt, das Verzeichnis auf `700`. Rotation: die letzten 5 Archive bleiben.
|
||
- **ERR-Trap.** Bricht das Update ab (Fehler, Strg-C, `kill`), sagt das Skript,
|
||
ob auf der Platte schon getauscht wurde, startet die vorher laufenden
|
||
Instanzen wieder und nennt Backup-Datei und Rollback-Befehl.
|
||
- **Instanz-Erfassung** deckt jetzt auch `activating` und `failed` ab (ueber
|
||
`list-units --all`, `list-unit-files` und die vorhandenen Configs). Vorher
|
||
kaputte Instanzen werden mitgestartet, gelten aber erst als Erfolg, wenn sie
|
||
danach wirklich laufen; bewusst gestoppte bleiben gestoppt.
|
||
- **Config-Pruefung vor dem Start**: `--check-config` je Instanz, Exit 2 zaehlt
|
||
als Fehler (Update-Exit 1), Exit 1 wird als Warnung samt Nachstell-Befehl
|
||
ausgegeben. Kennt der installierte Code das Flag noch nicht, wird die
|
||
Pruefung uebersprungen und das Update laeuft weiter.
|
||
- **`install.sh` repariert eine kaputte venv.** Bisher reichte das blosse
|
||
Vorhandensein von `venv/`, um den Basis-Install zu ueberspringen — nach einem
|
||
Distributions-Upgrade hat der Installer damit gar nichts repariert. Jetzt wird
|
||
die venv gegen das System-Python geprueft und bei Drift nach
|
||
`venv.old-<timestamp>` gesichert und neu gebaut.
|
||
- Die apt-Paketliste steht als **einzige Quelle** in `install.sh` in der Funktion
|
||
`pdf_ocr_apt_packages()` zwischen den Marken `# --- BEGIN apt-packages` /
|
||
`# --- END apt-packages`. `update.sh` schneidet den Block heraus und wertet ihn
|
||
aus — Marken und Funktionsname duerfen sich nicht ohne Anpassung aendern.
|
||
|
||
### Fixed
|
||
- **Dateien in `working/` gingen nach einem harten Stopp still verloren.** Siehe
|
||
Wiederaufnahme oben — der Fall trat bei jedem SIGKILL waehrend eines OCR-Laufs
|
||
auf, also auch bei einem Update ohne `TimeoutStopSec`.
|
||
- **Tippfehler in Config-Keys fielen nicht auf.** `load_config()` filterte
|
||
stumm gegen die Dataclass-Annotationen; `[ocr].langauges` lief damit
|
||
wirkungslos mit. Jetzt gibt es eine Warnung mit vollem Key-Pfad.
|
||
|
||
## [0.5.0] - 2026-09-22
|
||
|
||
### Added
|
||
- Der Installer weist einen Archiv-Pfad ab, der auf `incoming/`, `outgoing/`,
|
||
`working/` oder `error/` der Instanz zeigt — im Eingang wuerde das Original
|
||
sonst endlos neu aufgegriffen.
|
||
- **`install.sh` fragt beim Anlegen einer Instanz die OCR-Sprachen ab**
|
||
(`Tesseract-Sprachen [deu+eng]:`). Die Wahl gilt bewusst **pro Instanz** —
|
||
ein Hotfolder `buchhaltung` kann mit `deu` laufen, ein Hotfolder `export` mit
|
||
`deu+eng+fra`. Der Installer weist vorher darauf hin, dass jede zusaetzliche
|
||
Sprache Laufzeit **und** Erkennungsqualitaet kostet, die Liste also eng
|
||
gehalten werden sollte. Das Eingabeformat wird geprueft (Sprachcodes mit `+`
|
||
verbunden, `chi_sim` & Co. erlaubt); bei Unsinn wird erneut gefragt statt
|
||
abzubrechen.
|
||
- **Sprachpakete werden nachinstalliert.** Jeder eingegebene Code wird gegen
|
||
`tesseract --list-langs` geprueft. Fehlt eine Sprachdatei, bietet der
|
||
Installer das passende apt-Paket an (`tesseract-ocr-<code>`, Unterstrich wird
|
||
zum Bindestrich: `chi_sim` → `tesseract-ocr-chi-sim`). Lehnt der User ab oder
|
||
laesst sich das Paket nicht installieren, warnt der Installer, dass OCR mit
|
||
dieser Sprache **bei jeder Datei** scheitern wuerde, und fragt die Sprachen
|
||
erneut ab — so kann die Sprache einfach wieder rausgeworfen werden. Ist
|
||
`tesseract` nicht aufrufbar, wird die Pruefung uebersprungen und die Eingabe
|
||
unveraendert uebernommen.
|
||
- **Abfrage `Original nach erfolgreichem OCR archivieren? [j/N]:`** — Default
|
||
nein, also weiterhin `original_on_success = "delete"`. Bei ja wird der
|
||
Archiv-Pfad abgefragt (Vorschlag `<basis>/archive`), angelegt und auf den
|
||
Service-User gechownt; ein Archiv ausserhalb des Instanz-Basis-Pfads bekommt
|
||
ein eigenes `chown -R`.
|
||
|
||
### Changed
|
||
- Die Instanz-Config wird weiterhin per `sed` aus `config.example.toml`
|
||
erzeugt, substituiert jetzt aber zusaetzlich `[ocr].languages`,
|
||
`[output].original_on_success` und `[output].archive_dir` — bisher waren das
|
||
die Beispiel-Defaults, `archive_dir` musste von Hand nachgetragen werden.
|
||
Die Ausdruecke sind am Zeilenanfang verankert (`^key[[:space:]]*=`), damit die
|
||
deutschen Kommentarzeilen ueber den Keys unangetastet bleiben, und
|
||
Pfad-Variablen laufen durch `sed_escape_repl()` (maskiert `\`, `&`, `|`) —
|
||
Pfade mit Sonderzeichen landen damit korrekt in der Config.
|
||
- Nach dem sed-Lauf liest der Installer die drei Keys aus der erzeugten Config
|
||
zurueck und vergleicht sie mit der Eingabe. Erst wenn das passt, nennt die
|
||
Abschluss-Zusammenfassung zusaetzlich die gewaehlten **Sprachen** und (bei
|
||
Archivierung) das **Archiv-Verzeichnis**; sonst gibt es eine Warnung.
|
||
|
||
## [0.4.1] - 2026-09-22
|
||
|
||
### Fixed
|
||
- **veraPDF-FAIL hat das Original immer gelöscht.** Schlug die PDF/A-Validierung
|
||
fehl, wanderte das OCR-Ergebnis nach `error/` und das Original wurde per
|
||
`unlink()` entfernt — unabhängig von `[output].original_on_success`. Wer
|
||
`archive` konfiguriert hatte, verlor die Datei also ausgerechnet im
|
||
Fehlerfall. Der FAIL-Pfad nutzt jetzt dieselbe `_dispose_original()`-Logik
|
||
wie der Erfolgsfall: `archive` legt das Original samt
|
||
Timestamp-Kollisionsschutz im `archive_dir` ab, `delete` verhält sich wie
|
||
bisher. Die Log-Meldung nennt jetzt beides — wohin das OCR-Ergebnis ging und
|
||
was mit dem Original passiert ist.
|
||
|
||
### Removed
|
||
- Das nie benutzte Logverzeichnis `/var/log/pdf-ocr-hotfolder/` wird nicht mehr
|
||
vom Installer angelegt und ist aus README und Briefing entfernt. Es hat nie
|
||
ein Logfile enthalten: `_setup_logging()` nutzt `logging.basicConfig()` ohne
|
||
FileHandler, der Dienst loggt nach stdout → journald. **journald ist damit die
|
||
einzige Log-Quelle** (`journalctl -u pdf-ocr-hotfolder@<instanz> -f`).
|
||
Weder Installer noch Updater fassen das Verzeichnis an: ein vorhandenes,
|
||
leeres `/var/log/pdf-ocr-hotfolder/` kann auf bestehenden Installationen
|
||
gefahrlos von Hand entfernt werden (`sudo rmdir /var/log/pdf-ocr-hotfolder`).
|
||
|
||
### Added
|
||
- 3 neue Tests für den veraPDF-FAIL-Pfad (`delete`, `archive`,
|
||
Archiv-Namenskollision); veraPDF wird dabei gemockt. Suite jetzt 95 Tests.
|
||
|
||
## [0.4.0] - 2026-09-22
|
||
|
||
### Added
|
||
- `[ocr].timeout` ist jetzt wirksam: der Wert wird als `tesseract_timeout`
|
||
(Sekunden pro Seite) an ocrmypdf durchgereicht. Bisher war der Key zwar
|
||
dokumentiert, wurde aber nirgends gelesen.
|
||
- `check_output_config()` validiert zusätzlich `[output].name_mode`. Ein Tippfehler
|
||
führt jetzt beim Start zum Abbruch mit Exit-Code 2, statt erst pro Datei
|
||
zuzuschlagen — und zwar bisher **nach** dem Verschieben nach `working/`,
|
||
wo die Datei dann liegen blieb.
|
||
- Neue Exception `ConfigError` in `pdf_ocr_hotfolder.config` — fehlende
|
||
`[paths]`-Sektion oder ein fehlender Pfad-Eintrag liefern eine deutsche
|
||
Fehlermeldung mit Datei- und Key-Nennung statt eines nackten `KeyError`-Tracebacks.
|
||
Die CLI bricht damit sauber mit Exit-Code 2 ab.
|
||
- `pytest.ini` mit `testpaths = tests`, damit `pytest` aus dem Repo-Root läuft.
|
||
- 35 neue Tests: Fehlerzählung (Exception, Upload, Stabilitäts-Timeout),
|
||
Config-Fehlermeldungen, `tesseract_timeout`-Durchreichung (ocrmypdf gemockt)
|
||
und `upload_folder()`.
|
||
|
||
### Changed
|
||
- **`[ocr].timeout` hat eine neue Bedeutung — für bestehende Installationen relevant!**
|
||
Der Wert ist kein (nie implementiertes) Gesamt-Timeout pro PDF mehr, sondern
|
||
das Limit **pro Seite** für Tesseract. Der Default sinkt entsprechend von
|
||
`1800` auf `300`. Wer den alten Wert `1800` in seiner `config.toml` stehen hat,
|
||
gibt Tesseract damit 30 Minuten **je Seite** — bitte auf einen Seiten-Wert
|
||
anpassen (Richtwert 300).
|
||
`0` bedeutet "kein eigenes Limit": der Wert wird dann gar nicht erst
|
||
durchgereicht, weil ocrmypdf `tesseract_timeout=0` als "OCR komplett
|
||
überspringen" interpretiert.
|
||
- `_dispatch_uploads()` liefert jetzt die Namen der fehlgeschlagenen Upload-Ziele
|
||
zurück; die doppelte `enabled`-Prüfung (Service + Uploader) ist entfallen —
|
||
die Uploader prüfen das selbst.
|
||
- `upload_folder()` kopiert mit `shutil.copyfile()` statt
|
||
`read_bytes()`/`write_bytes()` — große PDFs landen nicht mehr komplett im
|
||
Speicher. Die Selbst-Ziel-Erkennung bleibt unverändert.
|
||
|
||
### Fixed
|
||
- `OcrConfig.pdfa_level` hatte im Code noch den Default `"2"`, obwohl
|
||
`config.example.toml` seit 0.2.2 bewusst `""` setzt (Ghostscript-Bug, Issue #3).
|
||
Eine Config ohne `[ocr]`-Sektion bzw. ohne den Key lief damit ungewollt in
|
||
PDF/A. Default im Code jetzt ebenfalls `""`.
|
||
- Eine Exception **nach** dem OCR (z.B. ein fehlgeschlagener
|
||
`shutil.move()` nach `outgoing/`) wurde nur im Worker-Callback geloggt.
|
||
`error_count` blieb 0 und `--once` lieferte trotz Fehlschlag Exit-Code 0.
|
||
Jede Exception aus `process_pdf()` zählt jetzt als Fehler, wird geloggt,
|
||
löst eine Fehler-Mail aus und die Datei wandert — soweit noch auffindbar
|
||
(`incoming/` oder `working/`) — nach `error/`.
|
||
- Fehlgeschlagene Uploads waren folgenlos: die Rückgabewerte der Uploader wurden
|
||
verworfen, es ging sogar eine Erfolgs-Mail raus. Jetzt zählt mindestens ein
|
||
fehlgeschlagenes Ziel als Fehler und die E-Mail geht als **FEHLER** raus, mit
|
||
Nennung der betroffenen Ziele. Das OCR-PDF bleibt bewusst in `outgoing/`
|
||
liegen (das OCR selbst war ja erfolgreich) — das steht so auch im Log.
|
||
- Lief der Stabilitäts-Check einer Datei in den 60-Sekunden-Timeout, gab es nur
|
||
ein `log.warning`; `--once` meldete Exit-Code 0. Jetzt `log.error` +
|
||
`error_count`. Die Datei bleibt bewusst in `incoming/` liegen und wird beim
|
||
nächsten Lauf erneut versucht. Eine zwischenzeitlich *verschwundene* Datei
|
||
wird davon unterschieden und zählt weiterhin nicht als Fehler.
|
||
|
||
## [0.3.1] - 2026-04-10
|
||
|
||
### Fixed
|
||
- **Issue #4**: LXC/Container-Kompatibilität — systemd-Hardening (`PrivateTmp`, `ProtectSystem`, etc.)
|
||
verursacht Error 226/NAMESPACE in LXC-Containern. Installer erkennt Container-Umgebung automatisch
|
||
und bietet ein Drop-in an. Zusätzlich liegt `systemd/lxc-compat.conf` als Vorlage im Repo.
|
||
- **Issue #5**: `WorkingDirectory=/opt/pdf-ocr-hotfolder` in der systemd Template-Unit ergänzt —
|
||
ohne diesen Eintrag konnte das Python-Modul nicht gefunden werden.
|
||
- **Issue #6**: Auf Debian 12 bietet der Installer bei betroffenen Ghostscript-Versionen (10.0.0–10.02.0)
|
||
jetzt automatisch an, bookworm-backports zu aktivieren und GS zu upgraden (statt nur zu warnen).
|
||
|
||
## [0.3.0] - 2026-04-09
|
||
|
||
### Added
|
||
- Neue Config-Sektion `[output]` mit:
|
||
- `name_mode` — Platzierung des Tags im Dateinamen: `"prefix"`, `"suffix"` (vor Extension), `"none"`
|
||
- `name_tag` — verbatim einzufügender String, z.B. `"OCR_"` oder `"_OCR"`
|
||
- `original_on_success` — `"delete"` (alter Default) oder `"archive"`
|
||
- `archive_dir` — Zielverzeichnis für `"archive"`, mit Kollisions-Schutz (Timestamp-Suffix)
|
||
- Runtime-Validierung der Output-Config in `check_output_config()`
|
||
- 20 neue Tests für `build_output_name()`, `check_output_config()` und `process_pdf()`
|
||
mit allen Kombinationen aus Modus + Original-Behandlung
|
||
|
||
### Changed
|
||
- `process_pdf()` nimmt jetzt `output_cfg: OutputConfig` als Pflicht-Argument
|
||
|
||
## [0.2.2] - 2026-04-09
|
||
|
||
### Fixed
|
||
- **Issue #3**: Ghostscript 10.0.0–10.02.0 (Debian 12 default) zerschießen OCR mit PDF/A + `skip_text=true`.
|
||
- `config.example.toml`: `pdfa_level = ""` als sicherer Default
|
||
- Runtime-Preflight: Prüft `gs --version` wenn `pdfa_level` gesetzt ist, bricht mit klarer Fehlermeldung ab
|
||
- `install.sh`: warnt bei betroffenen GS-Versionen mit Upgrade-Hinweis auf bookworm-backports
|
||
|
||
### Added
|
||
- `is_ghostscript_broken()` / `detect_ghostscript_version()` in `pdf_ocr_hotfolder.service`
|
||
- 19 weitere pytest-Tests für GS-Versions-Detection (parametrisiert) und Preflight-Kombinationen
|
||
|
||
## [0.2.1] - 2026-04-09
|
||
|
||
### Fixed
|
||
- **Issue #1**: Preflight-Check beim Start prüft jetzt `tesseract` und `gs` (Ghostscript). Fehlt eine Abhängigkeit, beendet sich der Service sofort mit Exit-Code 2 und klarer Fehlermeldung statt erst bei der ersten Datei.
|
||
- **Issue #2**: `--once`-Modus liefert jetzt Exit-Code `1`, sobald **mindestens ein** PDF fehlgeschlagen ist. Exit-Code `0` nur bei vollständigem Erfolg (inkl. "keine Dateien vorhanden"). Exit-Code `2` bei Preflight-Fehler.
|
||
|
||
### Added
|
||
- Public API: `HotfolderService.run_once()`, `.success_count`, `.error_count`, `.ensure_dirs()`
|
||
- `check_preflight()` / `PreflightError` in `pdf_ocr_hotfolder.service`
|
||
- pytest-Test-Suite (`tests/`) mit 11 Tests — deckt alle Szenarien aus Issue #1 und #2 ab
|
||
- `ocrmypdf`-Import in `processor.py` ist jetzt lazy (Tests ohne ocrmypdf-Installation möglich)
|
||
|
||
## [0.2.0] - 2026-04-08
|
||
|
||
### Added
|
||
- **Multi-Instanz-Support** via systemd Template-Unit `pdf-ocr-hotfolder@<name>.service`
|
||
- Pro Instanz: eigene Config (`/etc/pdf-ocr-hotfolder/<name>.toml`), eigene Datenverzeichnisse (`/var/lib/pdf-ocr-hotfolder/<name>/…`), optional eigener Service-User via Drop-in
|
||
- **Instanz-Manager in `install.sh`**: erkennt bestehende Instanzen bei Re-Run, fragt nach weiteren, listet Namen + Status
|
||
- `update.sh` stoppt/startet automatisch **alle** laufenden Instanzen
|
||
|
||
### Changed
|
||
- Single-Unit `pdf-ocr-hotfolder.service` durch Template-Unit `pdf-ocr-hotfolder@.service` ersetzt
|
||
- Installer fragt nicht mehr einmalig nach Service-User, sondern **pro Instanz**
|
||
|
||
### Removed
|
||
- Alte Single-Config unter `/etc/pdf-ocr-hotfolder/config.toml` — wird nicht mehr erzeugt
|
||
|
||
## [0.1.0] - 2026-04-08
|
||
|
||
### Added
|
||
- Initiale Version (Komplettes Rewrite des alten Bash-Tools `pdf-tool`)
|
||
- Python-Implementation auf Basis von `ocrmypdf` (Library, kein Subprozess)
|
||
- Hotfolder-Watcher mit `watchdog` (created/moved/closed Events)
|
||
- File-Stability-Check (wartet bis Scanner fertig geschrieben hat)
|
||
- ThreadPool für parallele PDF-Verarbeitung (`max_workers`)
|
||
- Upload-Targets: lokaler Ordner, Nextcloud (WebDAV via `requests`), SFTP (`paramiko`)
|
||
- E-Mail-Notify (`smtplib`, immer / nur Fehler / nie)
|
||
- Optional veraPDF-Validierung
|
||
- TOML-Konfiguration (`tomllib` aus stdlib, Python ≥3.11)
|
||
- systemd-Unit mit Hardening-Optionen
|
||
- `install.sh` mit interaktivem Service-User-Prompt
|
||
(lokal anlegen oder bestehenden lokalen/AD-User übernehmen)
|
||
- `update.sh` mit Backup, Code-Sync und Service-Reload
|
||
- README.md, AI_AGENT_BRIEFING.md
|