2 Commits

Author SHA1 Message Date
techadmin 04dc3c7b72 fix: gedrucktem --check-config-Befehl fehlte das cd ins Installationsverzeichnis (v0.6.2)
Das Paket ist nicht pip-installiert, sondern liegt unter /opt/pdf-ocr-hotfolder
und wird nur ueber das Arbeitsverzeichnis gefunden. Der Hinweis, den update.sh
in der Zusammenfassung ausgibt, lief deshalb so wie gedruckt nicht
("No module named pdf_ocr_hotfolder"). Gleiches galt fuer 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 CT 200 (Debian 12).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 23:05:29 +02:00
techadmin aa9918adba fix: ocrmypdf-Pin auf 17.4.1, Preflight erkennt den GS-Fall, Rauchtest (v0.6.1)
v0.6.0 hat ocrmypdf auf 16.13.0 gepinnt, um einen ungewollten Major-Sprung
zu verhindern. Auf Bestandsinstallationen war das ein DOWNGRADE (dort lief
via ">=16.0" bereits 17.x) — und 16.13.0 bricht auf Debian 12 mit dem
Bord-Ghostscript 10.0.0 bei JEDER PDF ab, sobald skip_text gesetzt ist.
Im Test auf CT 200 lief das Update mit Exit 0 durch, der Dienst blieb
"active", --check-config meldete "Preflight ok" — und jede Datei landete
in error/. Stiller Totalausfall.

- requirements.txt: ocrmypdf==17.4.1 (real auf Debian 12 + gs 10.0.0
  verifiziert). Ab 17.0.0 steht die GS-Pruefung in ocrmypdf unter einem
  `if options.output_type.startswith('pdfa')`; bis 16.x lief sie ohne
  diesen Guard und schlug auch bei output_type="pdf" zu.
- check_preflight() prueft Ghostscript nicht mehr nur bei gesetztem
  pdfa_level, sondern bildet die reale Bedingung ab:
  betroffene GS-Version UND skip_text UND (PDF/A ODER ocrmypdf < 17).
  Der Dienst bricht damit beim Start ab statt bei der ersten Datei.
- update.sh zeigt Versionsspruenge der gepinnten Pakete; Downgrades als
  WARN, auch in der Abschluss-Zusammenfassung.
- update.sh faehrt nach dem Start einen Rauchtest (eingebettete Mini-PDF
  durch die echte Pipeline) und raeumt restlos auf. Uebersprungen, wenn
  Upload-Ziele oder E-Mail-Notify aktiv sind, damit kein Testmuell zum
  Kunden geht. Abschaltbar mit --no-smoke-test.
- Doku korrigiert: pdfa_level = "" allein ist keine Entwarnung, die haengt
  an der ocrmypdf-Version.

152 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 22:55:47 +02:00
15 changed files with 1064 additions and 93 deletions
+11 -7
View File
@@ -1,8 +1,8 @@
# AI Agent Briefing — PDF OCR Hotfolder # AI Agent Briefing — PDF OCR Hotfolder
**Zuletzt aktualisiert:** 2026-09-22 **Zuletzt aktualisiert:** 2026-09-22
**Version:** 0.6.0 **Version:** 0.6.2
**Status:** Multi-Instanz-Betrieb, Preflight-Checks, Fehlerzählung, Wiederaufnahme aus `working/` und ein abgesicherter Updater (venv-Rebuild, Backup, Verifikation). Test-Suite grün (135 pytest-Tests). Ein Produktiv-Einsatz ist im Repo (README/CHANGELOG) nicht dokumentiert — die bisherigen Fixes stammen aus Issues #1–#6 und aus Vorbereitungen auf Debian 13, nicht aus einem belegten Dauerbetrieb. **Status:** Multi-Instanz-Betrieb, Preflight-Checks, Fehlerzählung, Wiederaufnahme aus `working/` und ein abgesicherter Updater (venv-Rebuild, Backup, Verifikation, Versionssprung-Meldung, Rauchtest). Test-Suite grün (152 pytest-Tests). Ein Produktiv-Einsatz ist im Repo (README/CHANGELOG) nicht dokumentiert — die bisherigen Fixes stammen aus Issues #1–#6 und aus Vorbereitungen auf Debian 13, nicht aus einem belegten Dauerbetrieb.
> **Betriebsabläufe stehen nicht hier**, sondern in: > **Betriebsabläufe stehen nicht hier**, sondern in:
> [README.md](README.md) (Einstieg, Layout, Config-Überblick) · > [README.md](README.md) (Einstieg, Layout, Config-Überblick) ·
@@ -26,7 +26,7 @@ pdf-ocr-hotfolder/
│ ├── service.py # HotfolderService (watchdog + ThreadPool), Preflight, Resume, Zähler │ ├── service.py # HotfolderService (watchdog + ThreadPool), Preflight, Resume, Zähler
│ ├── processor.py # ocrmypdf-Call, veraPDF, Ausgabename, Original-Entsorgung │ ├── processor.py # ocrmypdf-Call, veraPDF, Ausgabename, Original-Entsorgung
│ └── uploaders.py # folder, nextcloud (WebDAV), sftp, E-Mail-Notify │ └── uploaders.py # folder, nextcloud (WebDAV), sftp, E-Mail-Notify
├── tests/ # pytest-Suite (135 Tests, ocrmypdf wird gemockt) ├── tests/ # pytest-Suite (152 Tests, ocrmypdf wird gemockt)
│ ├── conftest.py # Fixtures tmp_config / dummy_pdf │ ├── conftest.py # Fixtures tmp_config / dummy_pdf
│ ├── test_check_config.py # --check-config, Exit 0/1/2 │ ├── test_check_config.py # --check-config, Exit 0/1/2
│ ├── test_config_errors.py │ ├── test_config_errors.py
@@ -230,7 +230,7 @@ wenn der installierte Code das Flag noch nicht kennt.
## 🔄 Verarbeitungs-Flow ## 🔄 Verarbeitungs-Flow
**Beim Start (`run()` wie `run_once()`), vor allem anderen:** **Beim Start (`run()` wie `run_once()`), vor allem anderen:**
1. `check_preflight()` — `tesseract` und `gs` müssen im PATH sein; ist `pdfa_level` gesetzt, wird zusätzlich die Ghostscript-Version gegen den 10.0.0–10.02.0-Bug geprüft 1. `check_preflight(pdfa_level, skip_text)` — `tesseract` und `gs` müssen im PATH sein; zusätzlich wird die Ghostscript-Version gegen den 10.0.0–10.02.0-Bug geprüft, und zwar unter genau der Bedingung, unter der ocrmypdf selbst abbricht (`_gs_block_reason()`: betroffene GS-Version **und** `skip_text` **und** (`pdfa_level` gesetzt **oder** ocrmypdf < 17))
2. `check_output_config()` — validiert `original_on_success`, `archive_dir` (Pflicht bei `archive`) und `name_mode` 2. `check_output_config()` — validiert `original_on_success`, `archive_dir` (Pflicht bei `archive`) und `name_mode`
3. Scheitert eines davon → `PreflightError`, CLI beendet sich mit **Exit-Code 2** (ebenso bei kaputter/fehlender Config) 3. Scheitert eines davon → `PreflightError`, CLI beendet sich mit **Exit-Code 2** (ebenso bei kaputter/fehlender Config)
4. `ensure_dirs()`, dann `_scan_existing()`: **zuerst `working/`**, danach `incoming/` 4. `ensure_dirs()`, dann `_scan_existing()`: **zuerst `working/`**, danach `incoming/`
@@ -289,7 +289,11 @@ Der Service läuft in allen Fällen weiter (kein `exit 1` wie im alten Bash-Tool
## ⚠️ Fallstricke ## ⚠️ Fallstricke
- **Ghostscript 10.0.0–10.02.0 zerschießt OCR.** Das ist der Debian-12-Default. In Kombination aus `[ocr].pdfa_level` + `skip_text = true` blockiert ocrmypdf komplett (Issue #3). Deshalb ist `pdfa_level = ""` der sichere Default, der Preflight bricht mit Exit 2 ab, wenn `pdfa_level` gesetzt **und** die GS-Version betroffen ist, und `--check-config` warnt bei gesetztem `pdfa_level` grundsätzlich. Abhilfe: Ghostscript ≥ 10.02.1 aus bookworm-backports (der Installer bietet das an). - **Ghostscript 10.0.0–10.02.0 zerschießt OCR.** Das ist der Debian-12-Default. ocrmypdf verweigert damit die Arbeit — aber die Bedingung dafür hängt an der **ocrmypdf-Version**, und genau daran ist 0.6.0 gescheitert:
- **ocrmypdf ≤ 16.x**: die Prüfung in `builtin_plugins/ghostscript.py::check_options()` läuft **bedingungslos**. `skip_text = true` allein reicht — `output_type` wird nicht geprüft. Auf Debian 12 scheitert damit **jede** Datei.
- **ocrmypdf ≥ 17.0**: derselbe Block steckt in einem `if options.output_type.startswith('pdfa'):`. Ohne PDF/A wird Ghostscript nicht angefasst.
`pdfa_level = ""` ist deshalb **kein** Schutz für sich genommen — es wirkt nur mit ocrmypdf ≥ 17. `requirements.txt` pinnt daher 17.x; ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar, bei grünem `systemctl status`. Der Preflight bildet die reale Bedingung ab (`_gs_block_reason()`) und bricht mit Exit 2 ab, `--check-config` meldet denselben Zustand als Fehler. `redo_ocr` ist bewusst **nicht** in der Bedingung: die Config kennt keinen solchen Key. Abhilfe: Ghostscript ≥ 10.02.1 aus bookworm-backports (der Installer bietet das an) oder `skip_text = false`.
- **`[ocr].timeout` ist ein Timeout PRO SEITE**, kein Gesamt-Timeout pro PDF. Der Wert geht als `tesseract_timeout` (ocrmypdf-Option `--tesseract-timeout`) durch; ocrmypdf kennt kein Dokument-Timeout. Wer noch den alten Default `1800` in einer Config stehen hat, gibt Tesseract 30 Minuten **je Seite** — Richtwert ist 300, ab 900 warnt `--check-config`. Ein durchgereichtes `0` würde ocrmypdf dazu bringen, OCR **still zu überspringen**, deshalb wird bei `0` (oder negativ) gar nichts übergeben und der ocrmypdf-Default greift. - **`[ocr].timeout` ist ein Timeout PRO SEITE**, kein Gesamt-Timeout pro PDF. Der Wert geht als `tesseract_timeout` (ocrmypdf-Option `--tesseract-timeout`) durch; ocrmypdf kennt kein Dokument-Timeout. Wer noch den alten Default `1800` in einer Config stehen hat, gibt Tesseract 30 Minuten **je Seite** — Richtwert ist 300, ab 900 warnt `--check-config`. Ein durchgereichtes `0` würde ocrmypdf dazu bringen, OCR **still zu überspringen**, deshalb wird bei `0` (oder negativ) gar nichts übergeben und der ocrmypdf-Default greift.
- **`TimeoutStopSec=300` in der Unit ist Absicht.** Ein laufendes OCR soll beim Stoppen zu Ende laufen dürfen — ein `systemctl stop` kann deshalb pro Instanz bis zu 5 Minuten dauern, und `update.sh` (das nacheinander stoppt) entsprechend länger. Bei SIGKILL bliebe das Original in `working/` liegen; das wird zwar wiederaufgenommen, kostet aber den kompletten Durchlauf. - **`TimeoutStopSec=300` in der Unit ist Absicht.** Ein laufendes OCR soll beim Stoppen zu Ende laufen dürfen — ein `systemctl stop` kann deshalb pro Instanz bis zu 5 Minuten dauern, und `update.sh` (das nacheinander stoppt) entsprechend länger. Bei SIGKILL bliebe das Original in `working/` liegen; das wird zwar wiederaufgenommen, kostet aber den kompletten Durchlauf.
- **Die venv hängt an der Python-Version der Distribution.** Nach einem Debian-Major-Upgrade ist `venv/bin/python` tot (systemd: `203/EXEC`) oder eine andere Version als das System-Python. Der Weg dahin und zurück steht in [docs/OS-UPGRADE.md](docs/OS-UPGRADE.md); im Code prüfen `install.sh` und `update.sh` das je mit einem eigenen `venv_is_healthy()` (die Variante in `update.sh` ist die gründlichere und schaut zusätzlich in `pyvenv.cfg`). - **Die venv hängt an der Python-Version der Distribution.** Nach einem Debian-Major-Upgrade ist `venv/bin/python` tot (systemd: `203/EXEC`) oder eine andere Version als das System-Python. Der Weg dahin und zurück steht in [docs/OS-UPGRADE.md](docs/OS-UPGRADE.md); im Code prüfen `install.sh` und `update.sh` das je mit einem eigenen `venv_is_healthy()` (die Variante in `update.sh` ist die gründlichere und schaut zusätzlich in `pyvenv.cfg`).
@@ -312,14 +316,14 @@ python -m pdf_ocr_hotfolder --config /tmp/config.toml
Tests (aus dem Repo-Root, `pytest.ini` setzt `testpaths = tests`): Tests (aus dem Repo-Root, `pytest.ini` setzt `testpaths = tests`):
```bash ```bash
pytest # aktuell 135 Tests pytest # aktuell 152 Tests
``` ```
`ocrmypdf` muss dafür **nicht** installiert sein: der Import in `processor.py` ist lazy, und `tests/test_ocr_timeout.py` schiebt ein Dummy-Modul in `sys.modules`. Die übrigen Tests mocken `process_pdf` bzw. arbeiten nur auf Config-Ebene. `ocrmypdf` muss dafür **nicht** installiert sein: der Import in `processor.py` ist lazy, und `tests/test_ocr_timeout.py` schiebt ein Dummy-Modul in `sys.modules`. Die übrigen Tests mocken `process_pdf` bzw. arbeiten nur auf Config-Ebene.
## 📋 Roadmap / TODO ## 📋 Roadmap / TODO
- [x] Tests (`pytest`) für `processor` und `uploaders` — 135 Tests - [x] Tests (`pytest`) für `processor` und `uploaders` — 152 Tests
- [x] Wiederaufnahme abgebrochener Läufe aus `working/` - [x] Wiederaufnahme abgebrochener Läufe aus `working/`
- [x] Config-Prüfung ohne Verarbeitung (`--check-config`) + Auswertung im Updater - [x] Config-Prüfung ohne Verarbeitung (`--check-config`) + Auswertung im Updater
- [x] Updater übersteht Debian-Major-Upgrades (venv-Rebuild, Pins, Rollback) - [x] Updater übersteht Debian-Major-Upgrades (venv-Rebuild, Pins, Rollback)
+119
View File
@@ -1,5 +1,124 @@
# Changelog # Changelog
## [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 ## [0.6.0] - 2026-09-22
### Added ### Added
+3 -3
View File
@@ -93,7 +93,7 @@ deshalb `640 root:<service-gruppe>` und beim Debuggen nicht in Tickets kopieren.
Config prüfen, ohne etwas zu verarbeiten: Config prüfen, ohne etwas zu verarbeiten:
```bash ```bash
sudo /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \ cd /opt/pdf-ocr-hotfolder && sudo ./venv/bin/python -m pdf_ocr_hotfolder \
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml --check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
``` ```
@@ -146,7 +146,7 @@ gelöscht.
## Tests ## Tests
```bash ```bash
pytest # 135 Tests pytest # 152 Tests
``` ```
`ocrmypdf` muss dafür nicht installiert sein — der Import ist lazy und wird in `ocrmypdf` muss dafür nicht installiert sein — der Import ist lazy und wird in
@@ -158,5 +158,5 @@ MIT — © Sonith UG
--- ---
**Version:** 0.6.0 **Version:** 0.6.2
**Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder **Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
+1 -1
View File
@@ -1 +1 @@
0.6.0 0.6.2
+11 -3
View File
@@ -21,9 +21,17 @@ skip_text = true
# Auflösung für gerasterte Seiten # Auflösung für gerasterte Seiten
oversample = 300 oversample = 300
# PDF/A-Konformitätsstufe ("1", "2", "3" oder leer für keinen PDF/A-Output) # PDF/A-Konformitätsstufe ("1", "2", "3" oder leer für keinen PDF/A-Output)
# ACHTUNG: Ghostscript 10.0.0 bis 10.02.0 (Debian 12 default!) haben einen Bug, # ACHTUNG: Ghostscript 10.0.0 bis 10.02.0 (Debian 12 default!) haben einen Bug;
# der mit pdfa_level + skip_text=true ocrmypdf komplett blockiert. # ocrmypdf lehnt damit die Kombination pdfa_level + skip_text=true komplett ab.
# Sicherer Default ist "" — nur auf "1"/"2"/"3" setzen, wenn gs >= 10.02.1 installiert ist. # Nur auf "1"/"2"/"3" setzen, wenn gs >= 10.02.1 installiert ist.
#
# pdfa_level = "" ist deshalb der Default — aber KEIN genereller Schutz gegen
# den Ghostscript-Bug: das gilt erst zusammen mit ocrmypdf >= 17. Bis
# ocrmypdf 16.x läuft dieselbe Prüfung auch ohne PDF/A, und dann scheitert mit
# skip_text = true jede einzelne Datei. Die Entwarnung hängt also an der
# ocrmypdf-Version, nicht an dieser Zeile; requirements.txt pinnt darum 17.x.
# Der Preflight prüft beides zusammen und lässt den Dienst gar nicht erst
# starten, wenn die Kombination nicht trägt.
pdfa_level = "" pdfa_level = ""
# Schiefe Scans automatisch begradigen # Schiefe Scans automatisch begradigen
deskew = true deskew = true
+44 -12
View File
@@ -249,19 +249,47 @@ Container-Instanzen reißt.
## Ghostscript-Bug auf Debian 12 ## Ghostscript-Bug auf Debian 12
Ghostscript 10.0.0 bis einschließlich 10.02.0 — der **Debian-12-Default** — Ghostscript 10.0.0 bis einschließlich 10.02.0 — der **Debian-12-Default** —
zerschießt OCR in der Kombination `[ocr].pdfa_level` + `skip_text = true`: enthält Regressionen, die PDFs mit vorhandenem Text beschädigen. ocrmypdf
ocrmypdf blockiert komplett. verweigert deshalb den Dienst, statt ein kaputtes Ergebnis zu liefern.
Deshalb: ### Wann genau ocrmypdf abbricht
- `pdfa_level = ""` ist der sichere Default (kein PDF/A-Output). Die Bedingung steht in `ocrmypdf/builtin_plugins/ghostscript.py`
- Der Preflight beim Dienststart bricht mit **Exit 2** ab, wenn `pdfa_level` (`check_options()`) und hängt an **zwei** Dingen — an der Config *und* an der
gesetzt **und** die installierte Ghostscript-Version betroffen ist. ocrmypdf-Version:
- `--check-config` meldet ein gesetztes `pdfa_level` als Warnung (siehe
| ocrmypdf | Die Prüfung greift bei | Heißt für uns |
|----------|------------------------|---------------|
| **≤ 16.x** | `skip_text` oder `redo_ocr` — **unabhängig vom `output_type`** | Auch ohne PDF/A scheitert **jede** Datei, denn `skip_text = true` ist unser Default |
| **≥ 17.0** | dasselbe, aber nur innerhalb von `if options.output_type.startswith('pdfa')` | Ohne PDF/A wird Ghostscript gar nicht angefasst — unkritisch |
> ⚠️ **`pdfa_level = ""` allein ist damit kein Schutz.** Die Entwarnung gilt nur
> zusammen mit **ocrmypdf ≥ 17**. Das war der Fehler in 0.6.0: der Pin stand auf
> `ocrmypdf==16.13.0`, und auf Debian 12 landete daraufhin jede PDF in `error/` —
> bei grünem `systemctl status` und „Preflight ok".
> `requirements.txt` pinnt deshalb 17.x.
### Was das Tool dagegen tut
- `pdfa_level = ""` ist der Default (kein PDF/A-Output).
- `requirements.txt` pinnt **ocrmypdf 17.x**. Ein Downgrade auf 16.x macht jede
Debian-12-Instanz unbrauchbar; `update.sh` weist Versionssprünge der
gepinnten Pakete deshalb ausdrücklich aus.
- Der Preflight bricht beim **Dienststart** mit **Exit 2** ab, wenn die
Ghostscript-Version betroffen ist **und** die Kombination aus `skip_text`,
`pdfa_level` und installierter ocrmypdf-Version tatsächlich zum Abbruch
führen würde. Der Dienst startet dann gar nicht erst, statt jede Datei
einzeln scheitern zu lassen.
- `--check-config` meldet denselben Zustand als **Fehler (Exit 2)** und zeigt
ocrmypdf- und Ghostscript-Version an (siehe
[UPDATE.md](UPDATE.md#config-prüfung-per---check-config)). [UPDATE.md](UPDATE.md#config-prüfung-per---check-config)).
- Der [Rauchtest](UPDATE.md#rauchtest) in `update.sh` schiebt nach dem Update
eine Test-PDF durch die echte Pipeline — er hätte den Ausfall sofort gezeigt.
Der Installer erkennt betroffene Versionen und bietet auf Debian 12 ### Abhilfe
bookworm-backports an. Manuell:
**Weg 1 — Ghostscript anheben** (empfohlen). Der Installer erkennt betroffene
Versionen und bietet auf Debian 12 bookworm-backports an. Manuell:
```bash ```bash
echo 'deb http://deb.debian.org/debian bookworm-backports main' | \ echo 'deb http://deb.debian.org/debian bookworm-backports main' | \
@@ -272,6 +300,10 @@ sudo apt update && sudo apt install -t bookworm-backports ghostscript
Ab Ghostscript 10.02.1 ist alles in Ordnung; PDF/A kann dann eingeschaltet Ab Ghostscript 10.02.1 ist alles in Ordnung; PDF/A kann dann eingeschaltet
werden. werden.
**Weg 2 — `skip_text = false` setzen.** Dann wird vorhandener Text neu erkannt
statt übersprungen, und die Bedingung greift nicht mehr. Das kostet Laufzeit bei
PDFs, die bereits eine Textebene haben.
--- ---
## Instanz manuell löschen ## Instanz manuell löschen
@@ -325,7 +357,7 @@ startet nicht.
| `jobs` | `4` | Threads, die ocrmypdf innerhalb **einer** PDF nutzt | | `jobs` | `4` | Threads, die ocrmypdf innerhalb **einer** PDF nutzt |
| `skip_text` | `true` | Seiten, die schon Text haben, nicht neu OCRen | | `skip_text` | `true` | Seiten, die schon Text haben, nicht neu OCRen |
| `oversample` | `300` | Auflösung für gerasterte Seiten | | `oversample` | `300` | Auflösung für gerasterte Seiten |
| `pdfa_level` | `""` | `"1"`, `"2"`, `"3"` oder leer für reines PDF — leer wegen des [Ghostscript-Bugs](#ghostscript-bug-auf-debian-12) | | `pdfa_level` | `""` | `"1"`, `"2"`, `"3"` oder leer für reines PDF — leer wegen des [Ghostscript-Bugs](#ghostscript-bug-auf-debian-12). Achtung: leer allein schützt nur zusammen mit ocrmypdf ≥ 17 |
| `deskew` | `true` | schiefe Scans begradigen | | `deskew` | `true` | schiefe Scans begradigen |
| `clean` | `false` | Hintergrund säubern (unpaper) | | `clean` | `false` | Hintergrund säubern (unpaper) |
| `max_workers` | `2` | wie viele PDFs **parallel** verarbeitet werden | | `max_workers` | `2` | wie viele PDFs **parallel** verarbeitet werden |
@@ -439,7 +471,7 @@ Exit 2 heißt immer: Config oder Preflight. Die Ursache steht im journal und
ausführlicher in: ausführlicher in:
```bash ```bash
sudo /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \ cd /opt/pdf-ocr-hotfolder && sudo ./venv/bin/python -m pdf_ocr_hotfolder \
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml --check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
``` ```
@@ -456,7 +488,7 @@ Bestehende PDFs einer Instanz einmalig verarbeiten und beenden — greift auch
Dateien auf, die in `working/` liegen geblieben sind: Dateien auf, die in `working/` liegen geblieben sind:
```bash ```bash
sudo -u pdfocr /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \ cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./venv/bin/python -m pdf_ocr_hotfolder \
--config /etc/pdf-ocr-hotfolder/kunde-a.toml --once --config /etc/pdf-ocr-hotfolder/kunde-a.toml --once
``` ```
+28 -11
View File
@@ -153,9 +153,9 @@ Instanz, die vorher lief, auch wieder läuft.
**Sind die Configs sauber?** **Sind die Configs sauber?**
```bash ```bash
cd /opt/pdf-ocr-hotfolder
for f in /etc/pdf-ocr-hotfolder/*.toml; do for f in /etc/pdf-ocr-hotfolder/*.toml; do
sudo /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \ sudo ./venv/bin/python -m pdf_ocr_hotfolder --check-config --config "$f"
--check-config --config "$f"
done done
``` ```
@@ -191,28 +191,45 @@ Upgrade entfernt. Hintergrund:
Die Python-Abhängigkeiten sind **bewusst fest gepinnt**: Die Python-Abhängigkeiten sind **bewusst fest gepinnt**:
``` ```
ocrmypdf==16.13.0 ocrmypdf==17.4.1
watchdog==6.0.0 watchdog==6.0.0
requests==2.33.1 requests==2.33.1
paramiko==4.0.0 paramiko==4.0.0
``` ```
Ohne Pins würde ein `pip install --upgrade` bei jedem Update ungefragt eine neue Ohne Pins würde ein `pip install --upgrade` bei jedem Update ungefragt eine neue
Major-Version ziehen — ein Sprung von ocrmypdf **16 auf 17** reißt sonst alle Major-Version ziehen — der nächste Sprung wäre ocrmypdf **17 auf 18**, und der
Instanzen auf einmal, und zwar im Moment des Updates, nicht zu einem Zeitpunkt, reißt sonst alle Instanzen auf einmal, und zwar im Moment des Updates, nicht zu
den man sich ausgesucht hat. einem Zeitpunkt, den man sich ausgesucht hat.
> ⚠️ **ocrmypdf darf nicht unter 17 fallen.** Bis einschließlich 16.x prüft
> ocrmypdf die Ghostscript-Version auch dann, wenn gar kein PDF/A erzeugt wird —
> auf Debian 12 (Ghostscript 10.0.0) scheitert damit **jede** PDF, weil
> `skip_text = true` unser Default ist. Genau das war der Ausfall in 0.6.0.
> Hintergrund: [INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
Die aktuellen Pins sind gegen Python 3.11 (Debian 12) und 3.13 (Debian 13) Die aktuellen Pins sind gegen Python 3.11 (Debian 12) und 3.13 (Debian 13)
geprüft; für beide gibt es fertige Wheels, es wird nichts kompiliert. geprüft; für beide gibt es fertige Wheels, es wird nichts kompiliert. Das gilt
auch für die Abhängigkeiten, die ocrmypdf 17 zusätzlich mitbringt (`pydantic`,
`pypdfium2`, `fpdf2`, `uharfbuzz`).
**Beim Anheben:** **Beim Anheben:**
1. **Testmaschine benutzen** — nie direkt auf dem produktiven Hotfolder. 1. **Testmaschine benutzen** — nie direkt auf dem produktiven Hotfolder.
2. Dort `update.sh --rebuild-venv` fahren, damit die Pakete wirklich frisch 2. Prüfen, dass es für die neue Version auf **beiden** Python-Versionen fertige
Wheels gibt, sonst wird auf dem Zielsystem kompiliert:
```bash
pip install --dry-run --only-binary=:all: --python-version 3.11 \
--target /tmp/wheelcheck ocrmypdf==<version>
pip install --dry-run --only-binary=:all: --python-version 3.13 \
--target /tmp/wheelcheck ocrmypdf==<version>
```
3. Dort `update.sh --rebuild-venv` fahren, damit die Pakete wirklich frisch
aufgelöst werden. aufgelöst werden.
3. `pytest` muss grün bleiben (135 Tests). 4. `pytest` muss grün bleiben (152 Tests).
4. Eine echte PDF durchschieben — die Test-Suite mockt ocrmypdf, ein Major-Sprung 5. Eine echte PDF durchschieben — die Test-Suite mockt ocrmypdf, ein Major-Sprung
fällt dort also nicht auf. fällt dort also nicht auf. Der [Rauchtest](UPDATE.md#rauchtest) in `update.sh`
macht genau das automatisch.
5. Erst dann committen und auf die produktiven Systeme geben. 5. Erst dann committen und auf die produktiven Systeme geben.
Der ocrmypdf-Sprung 16 → 17 ist ein **Major-Sprung** und gehört in einen eigenen Der ocrmypdf-Sprung 16 → 17 ist ein **Major-Sprung** und gehört in einen eigenen
+110 -10
View File
@@ -19,8 +19,9 @@ sudo ./update.sh
``` ```
``` ```
sudo ./update.sh --help # Optionen anzeigen sudo ./update.sh --help # Optionen anzeigen
sudo ./update.sh --rebuild-venv # venv zwingend neu bauen (nach dist-upgrade) sudo ./update.sh --rebuild-venv # venv zwingend neu bauen (nach dist-upgrade)
sudo ./update.sh --no-smoke-test # ohne Rauchtest durchlaufen
``` ```
`update.sh` muss aus dem Repo laufen. Findet es sich nicht selbst im Repo, liest `update.sh` muss aus dem Repo laufen. Findet es sich nicht selbst im Repo, liest
@@ -37,12 +38,13 @@ muss also liegen bleiben**, das Tool kopiert daraus.
| 4 | **Instanzen stoppen** | nur die, die vorher liefen oder kaputt waren | | 4 | **Instanzen stoppen** | nur die, die vorher liefen oder kaputt waren |
| 5 | **Backup** | [Inhalt und Ort](#backup) | | 5 | **Backup** | [Inhalt und Ort](#backup) |
| 6 | **Code kopieren** | `pdf_ocr_hotfolder/`, `requirements.txt`, `VERSION`, `config.example.toml`, `.repo_path` | | 6 | **Code kopieren** | `pdf_ocr_hotfolder/`, `requirements.txt`, `VERSION`, `config.example.toml`, `.repo_path` |
| 7 | **Dependencies** | `pip install --upgrade -r requirements.txt` — oder venv-Neubau, falls nötig | | 7 | **Dependencies** | `pip install --upgrade -r requirements.txt` — oder venv-Neubau, falls nötig. Versionssprünge der gepinnten Pakete werden [benannt](#versionssprünge-der-kernabhängigkeiten) |
| 8 | **systemd-Units** | Template-Unit aus dem Repo, LXC-Drop-in nachziehen, `daemon-reload` | | 8 | **systemd-Units** | Template-Unit aus dem Repo, LXC-Drop-in nachziehen, `daemon-reload` |
| 9 | **Berechtigungen** | Code gehört dem primären User (i.d.R. `pdfocr`) | | 9 | **Berechtigungen** | Code gehört dem primären User (i.d.R. `pdfocr`) |
| 10 | **Configs prüfen** | `--check-config` je Instanz, siehe [unten](#config-prüfung-per---check-config) | | 10 | **Configs prüfen** | `--check-config` je Instanz, siehe [unten](#config-prüfung-per---check-config) |
| 11 | **Instanzen starten + verifizieren** | mit Wartezeit und Crash-Loop-Erkennung | | 11 | **Instanzen starten + verifizieren** | mit Wartezeit und Crash-Loop-Erkennung |
| 12 | **Zusammenfassung** | Soll gegen Ist | | 12 | **Rauchtest** | eine Test-PDF durch die echte Pipeline, siehe [unten](#rauchtest) |
| 13 | **Zusammenfassung** | Soll gegen Ist |
Ab Schritt 2 gilt: **System-Pakete werden auch beim Update nachgezogen**, nicht Ab Schritt 2 gilt: **System-Pakete werden auch beim Update nachgezogen**, nicht
nur bei der Installation. Bereits installierte Tesseract-Sprachpakete bleiben nur bei der Installation. Bereits installierte Tesseract-Sprachpakete bleiben
@@ -187,20 +189,112 @@ Ein abgebrochenes Update lässt also keinen Hotfolder stumm gestoppt zurück.
--- ---
## Versionssprünge der Kernabhängigkeiten
`update.sh` misst die Versionen der in `requirements.txt` gepinnten Pakete
**vor** und **nach** `pip install` und benennt jede Änderung:
```
[WARN] DOWNGRADE: ocrmypdf: 17.4.1 -> 16.13.0
[INFO] Upgrade: watchdog: 5.0.0 -> 6.0.0
[INFO] Neu: requests 2.33.1
```
Beides steht auch noch einmal in der Abschluss-Zusammenfassung, weil es im
Fließtext zwischen den pip-Ausgaben untergeht.
**Downgrades sind der interessante Fall.** Sie entstehen, wenn ein Pin in
`requirements.txt` gesenkt wurde. Genau so ist der Totalausfall in 0.6.0
entstanden: `ocrmypdf` wurde von 17.4.1 auf 16.13.0 heruntergezogen, das Update
lief mit Exit 0 durch, der Dienst meldete `active` — und jede PDF landete in
`error/`. Sichtbar war davon nichts außer `[INFO] Dependencies ok ✓`.
War ein Downgrade nicht beabsichtigt: Pin korrigieren und
`sudo ./update.sh --rebuild-venv` erneut fahren.
---
## Rauchtest
Nach dem Start schiebt `update.sh` pro Instanz eine winzige Test-PDF durch die
**echte** Pipeline und prüft, ob sie in `outgoing/` ankommt.
Das ist der Schritt, den 0.6.0 gefehlt hat: `systemctl` sagt `active`,
`--check-config` sagt `Preflight ok` — und trotzdem scheitert jede einzelne
Datei. Ein laufender Dienst ist eben kein Beleg dafür, dass er etwas
verarbeitet.
- Die Test-PDF steckt als base64 **im Skript** (694 Bytes, eine Seite). Es
braucht also kein Pillow, kein `gs` und kein `convert` auf dem Zielsystem.
- Der Dateiname ist eindeutig (`__smoketest_update_<zeitstempel>_<pid>.pdf`) und
kann mit keiner Kundendatei kollidieren.
- Wartezeit: `SMOKE_TIMEOUT` Sekunden (Standard 90), danach gilt der Test als
durchgefallen. Das Skript hängt nicht.
### Aufräumen
Test-PDF **und** Ergebnis werden danach restlos entfernt — in jedem Ausgang,
auch bei Fehlschlag und Timeout. Angefasst werden dabei ausschließlich Dateien
mit dem Testnamen, in `incoming/`, `working/` (inkl. `__ocr_`-Zwischendatei),
`outgoing/`, `error/` und im Archivverzeichnis. In keinem dieser Verzeichnisse
bleibt etwas vom Test liegen.
### Wann der Rauchtest übersprungen wird
Hat eine Instanz ein aktives Ziel, würde die Testdatei **nach außen** gehen —
im Zweifel zum Kunden. Solche Instanzen werden mit klarer Meldung übersprungen:
| Übersprungen bei | Grund |
|------------------|-------|
| `[upload.nextcloud].enabled = true` | Testdatei landete in der Nextcloud |
| `[upload.sftp].enabled = true` | Testdatei landete auf dem SFTP-Ziel |
| `[upload.folder]` mit gesetztem `target` | Zielordner liegt außerhalb von `outgoing/`, oft eine Kundenfreigabe |
| `[notify.email].enabled = true` | löst eine Benachrichtigungs-Mail aus |
`[upload.folder]` **ohne** `target` schreibt nach `outgoing/` und ist damit
harmlos — dort läuft der Test normal.
Für diese Instanzen bleibt der manuelle Weg: eine eigene PDF in `incoming/`
legen und `journalctl -u pdf-ocr-hotfolder@<instanz> -f` mitlesen.
### Wenn der Rauchtest fehlschlägt
Der Rauchtest setzt den **Exit-Code** des Updates auf 1 und nennt den
Journal-Befehl:
```
[ERROR] RAUCHTEST FEHLGESCHLAGEN: kunde1
[ERROR] Diese Instanzen laufen, verarbeiten aber keine PDFs.
[ERROR] Es wurde NICHT zurueckgerollt. Journal ansehen:
[ERROR] journalctl -u pdf-ocr-hotfolder@kunde1.service -n 80 --no-pager
```
**Es wird nichts automatisch zurückgerollt.** Der Code ist getauscht, die
Instanzen laufen. Rollback nur von Hand und nur bewusst — siehe
[Rollback](#rollback).
Abschalten: `sudo ./update.sh --no-smoke-test`. Dann fällt ein Totalausfall
erst der ersten echten Kundendatei auf.
---
## Config-Prüfung per `--check-config` ## Config-Prüfung per `--check-config`
Nach dem Code-Update und vor dem Start prüft `update.sh` jede Instanz-Config mit Nach dem Code-Update und vor dem Start prüft `update.sh` jede Instanz-Config mit
dem neuen Code: dem neuen Code:
```bash ```bash
/opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \ cd /opt/pdf-ocr-hotfolder && ./venv/bin/python -m pdf_ocr_hotfolder \
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml --check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
``` ```
`--check-config` verarbeitet nichts, es hat sogar Vorrang vor `--once`. Es lädt `--check-config` verarbeitet nichts, es hat sogar Vorrang vor `--once`. Es lädt
die Config, zeigt die vier Pfade (inkl. Hinweis, falls ein Verzeichnis noch die Config, zeigt die vier Pfade (inkl. Hinweis, falls ein Verzeichnis noch
fehlt), Sprachen, Seiten-Timeout und PDF/A-Level, fährt den Preflight fehlt), Sprachen, Seiten-Timeout, PDF/A-Level, `skip_text` sowie die
(`tesseract`, `gs`, Ghostscript-Version bei gesetztem `pdfa_level`) und installierte ocrmypdf- und Ghostscript-Version, fährt den Preflight
(`tesseract`, `gs`, und die Ghostscript-Version gegen die tatsächliche
ocrmypdf-Bedingung — siehe [Rauchtest](#rauchtest) und
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12)) und
validiert die `[output]`-Sektion. validiert die `[output]`-Sektion.
| Exit | Bedeutung | Was der Admin tun soll | | Exit | Bedeutung | Was der Admin tun soll |
@@ -257,9 +351,15 @@ ocrmypdf `tesseract_timeout=0` als "OCR komplett überspringen" interpretiert.
`pdfa_level` gehört auf `""` (reines PDF, kein PDF/A). Ist es gesetzt, warnt `pdfa_level` gehört auf `""` (reines PDF, kein PDF/A). Ist es gesetzt, warnt
`--check-config`, weil Ghostscript 10.0.0–10.02.0 — der Debian-12-Default — in `--check-config`, weil Ghostscript 10.0.0–10.02.0 — der Debian-12-Default — in
Kombination mit `skip_text` das OCR blockiert. Der Preflight bricht in dem Fall Kombination mit `skip_text` von ocrmypdf abgelehnt wird. Der Preflight bricht in
mit Exit 2 ab; ab Ghostscript 10.02.1 ist PDF/A unproblematisch. Hintergrund: dem Fall mit Exit 2 ab; ab Ghostscript 10.02.1 ist PDF/A unproblematisch.
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
> **`pdfa_level = ""` allein ist kein Schutz gegen den Ghostscript-Bug.** Das
> gilt erst zusammen mit **ocrmypdf ≥ 17**. Bis ocrmypdf 16.x läuft dieselbe
> Prüfung auch ohne PDF/A, und dann scheitert mit `skip_text = true` jede
> einzelne Datei. Die Entwarnung hängt also an der ocrmypdf-Version.
> `requirements.txt` pinnt darum 17.x, und der Preflight prüft beides zusammen.
> Hintergrund: [INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
### Unbekannte Keys ### Unbekannte Keys
+1 -1
View File
@@ -1,3 +1,3 @@
"""PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen.""" """PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen."""
__version__ = "0.6.0" __version__ = "0.6.2"
+8 -2
View File
@@ -14,6 +14,8 @@ from .service import (
PreflightError, PreflightError,
check_output_config, check_output_config,
check_preflight, check_preflight,
detect_ghostscript_version,
detect_ocrmypdf_version,
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -70,11 +72,15 @@ def check_config(cfg_path: Path) -> int:
print(f" OCR-Sprachen = {cfg.ocr.languages}") print(f" OCR-Sprachen = {cfg.ocr.languages}")
print(f" Seiten-Timeout= {cfg.ocr.timeout} s") print(f" Seiten-Timeout= {cfg.ocr.timeout} s")
print(f" PDF/A-Level = {cfg.ocr.pdfa_level or '(aus)'}") print(f" PDF/A-Level = {cfg.ocr.pdfa_level or '(aus)'}")
print(f" skip_text = {'an' if cfg.ocr.skip_text else 'aus'}")
print(f" ocrmypdf = {detect_ocrmypdf_version() or '(nicht installiert)'}")
print(f" Ghostscript = {detect_ghostscript_version() or '(nicht gefunden)'}")
errors: list[str] = [] errors: list[str] = []
try: try:
check_preflight(cfg.ocr.pdfa_level) check_preflight(cfg.ocr.pdfa_level, cfg.ocr.skip_text)
print(" Preflight ok (tesseract, gs vorhanden).") print(" Preflight ok (tesseract, gs vorhanden, Ghostscript-Version "
"passt zu ocrmypdf + [ocr]-Einstellungen).")
except PreflightError as e: except PreflightError as e:
errors.append(str(e)) errors.append(str(e))
try: try:
+11 -5
View File
@@ -25,8 +25,13 @@ class OcrConfig:
jobs: int = 4 jobs: int = 4
skip_text: bool = True skip_text: bool = True
oversample: int = 300 oversample: int = 300
# Default bewusst leer: pdfa_level + skip_text zerschießt OCR mit # Default bewusst leer: mit Ghostscript 10.0.0-10.02.0 (Debian-12-Default)
# Ghostscript 10.0.0-10.02.0 (Debian-12-Default), siehe Issue #3 # lehnt ocrmypdf die Kombination pdfa_level + skip_text ab (Issue #3).
# ACHTUNG, kein Freibrief: pdfa_level = "" allein schützt nur zusammen mit
# ocrmypdf >= 17. Bis 16.x läuft dieselbe Prüfung auch ohne PDF/A und
# blockiert dann JEDE Datei — deshalb pinnt requirements.txt 17.x und der
# Preflight prüft die installierte ocrmypdf-Version mit (siehe
# service._gs_block_reason).
pdfa_level: str = "" pdfa_level: str = ""
deskew: bool = True deskew: bool = True
clean: bool = False clean: bool = False
@@ -251,9 +256,10 @@ def legacy_warnings(cfg: Config) -> list[str]:
out.append( out.append(
f"[ocr].pdfa_level = {cfg.ocr.pdfa_level!r}: PDF/A-Ausgabe ist " f"[ocr].pdfa_level = {cfg.ocr.pdfa_level!r}: PDF/A-Ausgabe ist "
"aktiv. Ghostscript 10.0.0-10.02.0 (Debian-12-Default) hat einen " "aktiv. Ghostscript 10.0.0-10.02.0 (Debian-12-Default) hat einen "
"Bug, der zusammen mit skip_text das OCR blockiert (Issue #3). Der " "Bug, wegen dem ocrmypdf die Kombination mit skip_text ablehnt "
"Preflight bricht ab, falls die installierte Ghostscript-Version " "(Issue #3). Der Preflight bricht ab, falls die installierte "
"betroffen ist; ab 10.02.1 ist alles in Ordnung." "Ghostscript-Version betroffen ist; ab 10.02.1 ist alles in "
"Ordnung."
) )
return out return out
+113 -15
View File
@@ -35,11 +35,20 @@ class PreflightError(RuntimeError):
# Pflicht-Binaries für ocrmypdf # Pflicht-Binaries für ocrmypdf
_REQUIRED_BINARIES = ("tesseract", "gs") _REQUIRED_BINARIES = ("tesseract", "gs")
# Ghostscript-Versionen mit bekanntem PDF/A+skip_text Bug (Issue #3): # Ghostscript-Versionen mit bekanntem Bug (Issue #3):
# 10.0.0 .. 10.02.0 (inklusive). Ab 10.02.1 wieder nutzbar. # 10.0.0 .. 10.02.0 (inklusive). Ab 10.02.1 wieder nutzbar.
_GS_BROKEN_MIN = (10, 0, 0) _GS_BROKEN_MIN = (10, 0, 0)
_GS_BROKEN_MAX = (10, 2, 0) _GS_BROKEN_MAX = (10, 2, 0)
# Ab dieser ocrmypdf-Major steht die Ghostscript-Pruefung hinter
# `if options.output_type.startswith('pdfa')` — mit pdfa_level = "" wird
# Ghostscript gar nicht angefasst und die Pruefung greift nicht.
# Darunter (16.x und aelter) laeuft sie BEDINGUNGSLOS, also auch bei
# output_type="pdf": dort reicht skip_text=true, um auf Debian 12 jede
# einzelne PDF scheitern zu lassen. Quelle jeweils
# ocrmypdf/builtin_plugins/ghostscript.py::check_options().
_OCRMYPDF_GS_GUARD_MAJOR = 17
def _parse_version(text: str) -> tuple[int, ...] | None: def _parse_version(text: str) -> tuple[int, ...] | None:
"""Extrahiert die erste X.Y[.Z] Version aus einem String.""" """Extrahiert die erste X.Y[.Z] Version aus einem String."""
@@ -50,7 +59,7 @@ def _parse_version(text: str) -> tuple[int, ...] | None:
def is_ghostscript_broken(version: str | None) -> bool: def is_ghostscript_broken(version: str | None) -> bool:
"""Prüft, ob eine Ghostscript-Version vom PDF/A+skip_text Bug betroffen ist. """Prüft, ob eine Ghostscript-Version vom bekannten Bug betroffen ist.
Betrifft 10.0.0 bis einschließlich 10.02.0. Ab 10.02.1 wieder sicher. Betrifft 10.0.0 bis einschließlich 10.02.0. Ab 10.02.1 wieder sicher.
""" """
@@ -79,6 +88,36 @@ def detect_ghostscript_version() -> str | None:
return result.stdout.strip() or None return result.stdout.strip() or None
def detect_ocrmypdf_version() -> str | None:
"""Liest die installierte ocrmypdf-Version aus den Paket-Metadaten.
Bewusst über `importlib.metadata` statt über einen Import: das ist
billiger und funktioniert auch in den Tests, in denen ocrmypdf gar nicht
installiert ist (dann None).
"""
try:
from importlib.metadata import version
return version("ocrmypdf")
except Exception: # noqa: BLE001 - fehlende Metadaten dürfen nichts umwerfen
return None
def ocrmypdf_checks_gs_always(version: str | None) -> bool:
"""True, wenn ocrmypdf die Ghostscript-Pruefung unabhaengig vom output_type fährt.
Das ist bei 16.x und aelter der Fall (siehe `_OCRMYPDF_GS_GUARD_MAJOR`).
Ist die Version unbekannt, wird `False` angenommen: der Pin in
requirements.txt steht auf 17.x, und ein Fehlalarm, der den Dienst nicht
starten laesst, waere schlimmer als die fehlende Warnung.
"""
if not version:
return False
parsed = _parse_version(version)
if parsed is None:
return False
return parsed[0] < _OCRMYPDF_GS_GUARD_MAJOR
def check_output_config(mode: str, archive_dir: str, def check_output_config(mode: str, archive_dir: str,
name_mode: str = "prefix") -> None: name_mode: str = "prefix") -> None:
"""Validiert die [output]-Section. Wirft PreflightError bei Problemen.""" """Validiert die [output]-Section. Wirft PreflightError bei Problemen."""
@@ -101,12 +140,13 @@ def check_output_config(mode: str, archive_dir: str,
) )
def check_preflight(pdfa_level: str = "") -> None: def check_preflight(pdfa_level: str = "", skip_text: bool = False) -> None:
"""Prüft externe Abhängigkeiten. """Prüft externe Abhängigkeiten.
- Tesseract und Ghostscript müssen im PATH sein - Tesseract und Ghostscript müssen im PATH sein
- Bei gesetztem pdfa_level wird die Ghostscript-Version gegen den - Die Ghostscript-Version wird gegen den bekannten 10.0.0–10.02.0 Bug
bekannten 10.0.0–10.02.0 Bug geprüft geprüft, und zwar genau unter der Bedingung, unter der ocrmypdf selbst
abbricht (siehe `_gs_block_reason`).
Wirft PreflightError bei fehlenden Binaries oder unsicherem Ghostscript. Wirft PreflightError bei fehlenden Binaries oder unsicherem Ghostscript.
""" """
@@ -117,15 +157,73 @@ def check_preflight(pdfa_level: str = "") -> None:
+ ". Bitte installieren: sudo apt install tesseract-ocr ghostscript" + ". Bitte installieren: sudo apt install tesseract-ocr ghostscript"
) )
reason = _gs_block_reason(pdfa_level, skip_text)
if reason:
raise PreflightError(reason)
def _gs_block_reason(pdfa_level: str, skip_text: bool) -> str | None:
"""Liefert die Fehlermeldung, wenn ocrmypdf mit diesem Ghostscript abbricht.
Abgebildet wird die reale Bedingung aus
`ocrmypdf/builtin_plugins/ghostscript.py::check_options()`:
betroffene GS-Version UND (skip_text ODER redo_ocr)
UND (PDF/A-Ausgabe ODER ocrmypdf < 17)
Die letzte Klammer ist der Teil, der v0.6.0 durchrutschen ließ: bis
einschließlich ocrmypdf 16.x steht die Pruefung ohne jeden Guard in
`check_options()` und schlaegt deshalb auch bei `output_type="pdf"` zu.
Ab 17.0.0 umschliesst sie ein
`if options.output_type.startswith('pdfa'):` — ohne PDF/A wird
Ghostscript nicht angefasst.
`redo_ocr` kennt unsere Config nicht (es gibt keinen entsprechenden Key in
`OcrConfig`), deshalb steht es hier bewusst nicht in der Bedingung.
Returns:
Fehlermeldung oder None, wenn die Kombination unkritisch ist.
"""
if not skip_text:
# Weder skip_text noch redo_ocr — ocrmypdf fasst den Pfad nicht an.
return None
gs_version = detect_ghostscript_version()
if not is_ghostscript_broken(gs_version):
return None
ocrmypdf_version = detect_ocrmypdf_version()
always = ocrmypdf_checks_gs_always(ocrmypdf_version)
if not pdfa_level and not always:
return None
if pdfa_level: if pdfa_level:
gs_version = detect_ghostscript_version() ursache = (
if is_ghostscript_broken(gs_version): f"[ocr].pdfa_level = {pdfa_level!r} (PDF/A-Ausgabe) zusammen mit "
raise PreflightError( "[ocr].skip_text = true"
f"Ghostscript {gs_version} ist mit pdfa_level='{pdfa_level}' nicht " )
"kompatibel (bekannter Bug in 10.0.0–10.02.0). " else:
"Entweder ghostscript auf >=10.02.1 upgraden (z.B. via bookworm-backports) " ursache = (
"oder in der Config [ocr].pdfa_level = \"\" setzen." f"[ocr].skip_text = true und ocrmypdf {ocrmypdf_version} — bis "
) f"einschließlich {_OCRMYPDF_GS_GUARD_MAJOR - 1}.x prüft ocrmypdf "
"Ghostscript auch dann, wenn gar kein PDF/A erzeugt wird. Jede "
"einzelne PDF würde in error/ landen"
)
return (
f"Ghostscript {gs_version} ist von einem bekannten Fehler betroffen "
"(10.0.0–10.02.0, der Debian-12-Standard) und wird von ocrmypdf "
f"abgelehnt: {ursache}. "
"Abhilfe — eines von beidem: "
"(1) Ghostscript >= 10.02.1 aus bookworm-backports installieren "
"(install.sh bietet das an): "
"echo 'deb http://deb.debian.org/debian bookworm-backports main' | "
"sudo tee /etc/apt/sources.list.d/bookworm-backports.list && "
"sudo apt update && sudo apt install -t bookworm-backports ghostscript — "
"oder (2) in der Config [ocr].skip_text = false setzen "
"(dann wird vorhandener Text neu erkannt statt übersprungen)"
+ (" bzw. [ocr].pdfa_level = \"\"." if pdfa_level else ".")
)
def _is_pdf(path: Path) -> bool: def _is_pdf(path: Path) -> bool:
@@ -201,7 +299,7 @@ class HotfolderService:
# ---- Lifecycle ---- # ---- Lifecycle ----
def run(self) -> None: def run(self) -> None:
check_preflight(self.cfg.ocr.pdfa_level) check_preflight(self.cfg.ocr.pdfa_level, self.cfg.ocr.skip_text)
check_output_config(self.cfg.output.original_on_success, check_output_config(self.cfg.output.original_on_success,
self.cfg.output.archive_dir, self.cfg.output.archive_dir,
self.cfg.output.name_mode) self.cfg.output.name_mode)
@@ -228,7 +326,7 @@ class HotfolderService:
Returns: Returns:
Anzahl fehlgeschlagener PDFs (0 = alles ok). Anzahl fehlgeschlagener PDFs (0 = alles ok).
""" """
check_preflight(self.cfg.ocr.pdfa_level) check_preflight(self.cfg.ocr.pdfa_level, self.cfg.ocr.skip_text)
check_output_config(self.cfg.output.original_on_success, check_output_config(self.cfg.output.original_on_success,
self.cfg.output.archive_dir, self.cfg.output.archive_dir,
self.cfg.output.name_mode) self.cfg.output.name_mode)
+17 -2
View File
@@ -1,9 +1,24 @@
# Feste Pins: ein Update darf nicht ungefragt einen Major-Sprung einziehen # Feste Pins: ein Update darf nicht ungefragt einen Major-Sprung einziehen
# (ein ocrmypdf 16 -> 17 reisst sonst alle Instanzen auf einmal). # (der naechste waere ocrmypdf 18 — der reisst sonst alle Instanzen auf einmal).
# Geprueft gegen Python 3.11 (Debian 12) und 3.13 (Debian 13) — fuer beide # Geprueft gegen Python 3.11 (Debian 12) und 3.13 (Debian 13) — fuer beide
# gibt es fertige Wheels, es wird nichts kompiliert. # gibt es fertige Wheels, es wird nichts kompiliert.
# Beim Anheben: update.sh --rebuild-venv auf einer Testmaschine fahren. # Beim Anheben: update.sh --rebuild-venv auf einer Testmaschine fahren.
ocrmypdf==16.13.0 #
# ocrmypdf: MUSS 17.x sein, 16.x ist fuer uns unbrauchbar (v0.6.1).
# In ocrmypdf 16.x laeuft die Ghostscript-Pruefung in
# builtin_plugins/ghostscript.py:check_options() BEDINGUNGSLOS, also auch bei
# output_type="pdf". Auf Debian 12 (Ghostscript 10.0.0) bricht damit
# JEDE PDF ab, sobald skip_text=true gesetzt ist — und das ist unser Default:
# if Version('10.0.0') <= gs_version < Version('10.02.1') and (
# options.skip_text or options.redo_ocr
# ): raise MissingDependencyError(...)
# Ab 17.0.0 steckt genau dieser Block in einem
# `if options.output_type.startswith('pdfa'):` — bei pdfa_level = "" wird
# Ghostscript gar nicht erst angefasst und die Pruefung greift nicht mehr.
# Deshalb hier 17.x. 17.4.1 ist die im Feld auf Debian 12 + gs 10.0.0
# verifizierte Version; 17.12.1 traegt denselben Guard und waere der
# naechste Kandidat, ist aber noch nicht auf einer Testmaschine gefahren.
ocrmypdf==17.4.1
watchdog==6.0.0 watchdog==6.0.0
requests==2.33.1 requests==2.33.1
paramiko==4.0.0 paramiko==4.0.0
+148 -19
View File
@@ -1,6 +1,16 @@
"""Tests für Issue #3: Ghostscript 10.0.0–10.02.0 PDF/A-Bug-Erkennung.""" """Tests für Issue #3: Ghostscript 10.0.0–10.02.0 Bug-Erkennung.
Seit v0.6.1 bildet der Preflight die reale ocrmypdf-Bedingung ab:
betroffene GS-Version UND skip_text UND (pdfa_level ODER ocrmypdf < 17)
Der letzte Teil ist der Fall, der in v0.6.0 durchrutschte: mit ocrmypdf 16.x
greift die Ghostscript-Prüfung auch ohne PDF/A, und der Dienst meldete
trotzdem "Preflight ok", während jede einzelne PDF in error/ landete.
"""
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@@ -9,9 +19,21 @@ from pdf_ocr_hotfolder.service import (
PreflightError, PreflightError,
check_preflight, check_preflight,
is_ghostscript_broken, is_ghostscript_broken,
ocrmypdf_checks_gs_always,
) )
@contextmanager
def _env(gs_version: str, ocrmypdf_version: str):
"""Binaries vorhanden, Ghostscript- und ocrmypdf-Version vorgegeben."""
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
return_value=gs_version), \
patch("pdf_ocr_hotfolder.service.detect_ocrmypdf_version",
return_value=ocrmypdf_version):
yield
@pytest.mark.parametrize("version,expected", [ @pytest.mark.parametrize("version,expected", [
# Betroffene Versionen # Betroffene Versionen
("10.0.0", True), ("10.0.0", True),
@@ -36,29 +58,92 @@ def test_is_ghostscript_broken(version, expected) -> None:
assert is_ghostscript_broken(version) is expected assert is_ghostscript_broken(version) is expected
def test_check_preflight_without_pdfa_passes_with_broken_gs() -> None: @pytest.mark.parametrize("version,expected", [
"""Ohne pdfa_level darf der betroffene GS verwendet werden.""" ("16.13.0", True), # der Pin aus v0.6.0, der den Ausfall ausgeloest hat
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \ ("16.0.0", True),
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version", ("15.4.4", True),
return_value="10.0.0"): ("17.0.0", False), # ab hier steckt die Pruefung hinter output_type
check_preflight(pdfa_level="") # darf nicht werfen ("17.4.1", False), # unser Pin
("17.12.1", False),
("18.0.0", False),
(None, False), # unbekannt -> kein Fehlalarm
("", False),
("garbage", False),
])
def test_ocrmypdf_checks_gs_always(version, expected) -> None:
assert ocrmypdf_checks_gs_always(version) is expected
def test_check_preflight_with_pdfa_fails_on_broken_gs() -> None: # ---------------- Preflight: ocrmypdf 17.x (unser Pin) ----------------
"""Mit pdfa_level + kaputtem GS → PreflightError mit hilfreicher Meldung."""
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \ def test_broken_gs_skip_text_without_pdfa_passes_on_ocrmypdf_17() -> None:
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version", """Der Debian-12-Standardfall: ohne PDF/A fasst ocrmypdf 17 gs nicht an.
return_value="10.0.0"):
Das ist die Default-Config (skip_text=true, pdfa_level="") auf Debian 12 —
sie muss laufen, sonst startet keine einzige Bestandsinstanz mehr.
"""
with _env("10.0.0", "17.4.1"):
check_preflight(pdfa_level="", skip_text=True) # darf nicht werfen
def test_broken_gs_with_pdfa_and_skip_text_fails() -> None:
"""Mit pdfa_level + skip_text + kaputtem GS → PreflightError."""
with _env("10.0.0", "17.4.1"):
with pytest.raises(PreflightError, match="Ghostscript 10.0.0"): with pytest.raises(PreflightError, match="Ghostscript 10.0.0"):
check_preflight(pdfa_level="2") check_preflight(pdfa_level="2", skip_text=True)
def test_check_preflight_with_pdfa_passes_on_fixed_gs() -> None: def test_broken_gs_with_pdfa_without_skip_text_passes() -> None:
"""Mit pdfa_level + gefixtem GS → ok.""" """Ohne skip_text greift die ocrmypdf-Bedingung nicht — kein Abbruch."""
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \ with _env("10.0.0", "17.4.1"):
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version", check_preflight(pdfa_level="2", skip_text=False) # darf nicht werfen
return_value="10.02.1"):
check_preflight(pdfa_level="2") # darf nicht werfen
def test_healthy_gs_with_pdfa_and_skip_text_passes() -> None:
"""Nicht betroffene GS-Version → nie ein Abbruch."""
with _env("10.02.1", "17.4.1"):
check_preflight(pdfa_level="2", skip_text=True) # darf nicht werfen
# ---------------- Preflight: ocrmypdf 16.x (der Ausfall aus v0.6.0) ----------------
def test_broken_gs_skip_text_without_pdfa_fails_on_ocrmypdf_16() -> None:
"""DER Fall, der in v0.6.0 durchrutschte.
ocrmypdf 16.13.0 + Ghostscript 10.0.0 + skip_text=true + pdfa_level="":
"Preflight ok", Dienst active — und jede PDF landete in error/.
Jetzt muss der Dienst beim START abbrechen.
"""
with _env("10.0.0", "16.13.0"):
with pytest.raises(PreflightError) as exc_info:
check_preflight(pdfa_level="", skip_text=True)
msg = str(exc_info.value)
assert "10.0.0" in msg
assert "16.13.0" in msg
def test_broken_gs_without_skip_text_passes_on_ocrmypdf_16() -> None:
"""skip_text=false → auch 16.x prüft Ghostscript nicht."""
with _env("10.0.0", "16.13.0"):
check_preflight(pdfa_level="", skip_text=False) # darf nicht werfen
def test_healthy_gs_passes_on_ocrmypdf_16() -> None:
"""Nicht betroffene GS-Version → auch mit 16.x kein Abbruch."""
with _env("10.02.1", "16.13.0"):
check_preflight(pdfa_level="", skip_text=True) # darf nicht werfen
# ---------------- Meldungstext ----------------
def test_error_message_names_both_remedies() -> None:
"""Der Admin muss aus der Meldung heraus handeln können."""
with _env("10.0.0", "16.13.0"):
with pytest.raises(PreflightError) as exc_info:
check_preflight(pdfa_level="", skip_text=True)
msg = str(exc_info.value)
assert "backports" in msg, "Weg 1: Ghostscript aus bookworm-backports"
assert "skip_text = false" in msg, "Weg 2: skip_text abschalten"
def test_default_config_pdfa_level_is_empty() -> None: def test_default_config_pdfa_level_is_empty() -> None:
@@ -70,3 +155,47 @@ def test_default_config_pdfa_level_is_empty() -> None:
data = tomllib.load(f) data = tomllib.load(f)
assert data["ocr"]["pdfa_level"] == "", \ assert data["ocr"]["pdfa_level"] == "", \
"config.example.toml muss pdfa_level='' als sicheren Default haben" "config.example.toml muss pdfa_level='' als sicheren Default haben"
# ---------------- Der Dienst muss beim START abbrechen ----------------
def test_run_once_aborts_on_ocrmypdf_16_with_broken_gs(tmp_config) -> None:
"""Abbruch beim Start statt Totalausfall bei der ersten Datei."""
from pdf_ocr_hotfolder.service import HotfolderService
assert tmp_config.ocr.skip_text is True
assert tmp_config.ocr.pdfa_level == ""
service = HotfolderService(tmp_config)
try:
with _env("10.0.0", "16.13.0"):
with pytest.raises(PreflightError):
service.run_once()
finally:
service._executor.shutdown(wait=False)
def test_check_config_returns_2_on_ocrmypdf_16_with_broken_gs(
tmp_path, tmp_config, monkeypatch, capsys) -> None:
"""--check-config meldet den Zustand als Fehler (Exit 2) — auch mitten im Update."""
import sys
from pdf_ocr_hotfolder.__main__ import CHECK_ERROR, main
cfg_file = tmp_path / "cfg.toml"
cfg_file.write_text(f"""
[paths]
incoming = "{tmp_config.paths.incoming}"
outgoing = "{tmp_config.paths.outgoing}"
working = "{tmp_config.paths.working}"
error = "{tmp_config.paths.error}"
[ocr]
skip_text = true
pdfa_level = ""
""")
monkeypatch.setattr(sys, "argv",
["pdf-ocr-hotfolder", "--config", str(cfg_file),
"--check-config"])
with _env("10.0.0", "16.13.0"):
assert main() == CHECK_ERROR
assert "Ghostscript" in capsys.readouterr().err
+439 -2
View File
@@ -14,6 +14,7 @@
# Aufruf: # Aufruf:
# sudo ./update.sh # normales Update # sudo ./update.sh # normales Update
# sudo ./update.sh --rebuild-venv # venv-Neubau erzwingen (nach dist-upgrade) # sudo ./update.sh --rebuild-venv # venv-Neubau erzwingen (nach dist-upgrade)
# sudo ./update.sh --no-smoke-test # ohne Rauchtest durchlaufen
# #
set -Eeuo pipefail set -Eeuo pipefail
@@ -32,6 +33,10 @@ log_step() { echo -e "\n${BLUE}==>${NC} $*"; }
: "${TAR_ROOT:=/}" : "${TAR_ROOT:=/}"
: "${VERIFY_WAIT:=6}" : "${VERIFY_WAIT:=6}"
: "${BACKUP_KEEP:=5}" : "${BACKUP_KEEP:=5}"
# Max. Sekunden, die der Rauchtest auf das fertige OCR-PDF wartet. Eine
# Mini-PDF braucht ein paar Sekunden (Datei-Stabilisierung + OCR), unter Last
# auch mal laenger.
: "${SMOKE_TIMEOUT:=90}"
SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service" SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service"
UNIT_GLOB='pdf-ocr-hotfolder@*.service' UNIT_GLOB='pdf-ocr-hotfolder@*.service'
LXC_DROPIN_DIR="$SYSTEMD_DIR/${SERVICE_TEMPLATE}.d" LXC_DROPIN_DIR="$SYSTEMD_DIR/${SERVICE_TEMPLATE}.d"
@@ -44,6 +49,7 @@ LXC_SYNCED=0
BACKUP_FILE="" BACKUP_FILE=""
PRIMARY_USER="pdfocr" PRIMARY_USER="pdfocr"
TOUCHED=0 # 1, sobald auf der Platte etwas getauscht wurde TOUCHED=0 # 1, sobald auf der Platte etwas getauscht wurde
SMOKE_TEST=1 # per --no-smoke-test abschaltbar
UNITS_ALL=() # alle bekannten Instanz-Units UNITS_ALL=() # alle bekannten Instanz-Units
PREV_OK=() # liefen vorher sauber -> muessen nachher laufen PREV_OK=() # liefen vorher sauber -> muessen nachher laufen
@@ -53,6 +59,12 @@ CFG_WARN=() # Instanzen mit Config-Warnungen (Exit 1)
CFG_ERR=() # Instanzen mit Config-Fehlern (Exit 2) CFG_ERR=() # Instanzen mit Config-Fehlern (Exit 2)
STARTED_OK=() STARTED_OK=()
STARTED_FAIL=() STARTED_FAIL=()
DEP_UPGRADES=() # Paket: alt -> neu (hoeher)
DEP_DOWNGRADES=() # Paket: alt -> neu (niedriger) <- das Gefaehrliche
DEP_NEW=() # Paket war vorher nicht installiert
SMOKE_OK=() # Rauchtest bestanden
SMOKE_FAIL=() # Rauchtest fehlgeschlagen
SMOKE_SKIP=() # Rauchtest uebersprungen (mit Begruendung)
usage() { usage() {
cat <<EOF cat <<EOF
@@ -65,10 +77,25 @@ Optionen:
installieren. Das ist der Aufruf nach einem Debian- installieren. Das ist der Aufruf nach einem Debian-
Major-Upgrade (apt full-upgrade, z.B. 12 -> 13), wenn das Major-Upgrade (apt full-upgrade, z.B. 12 -> 13), wenn das
System-Python eine neue Version bekommen hat. System-Python eine neue Version bekommen hat.
--no-smoke-test Den Rauchtest nach dem Start auslassen.
-h, --help Diese Hilfe. -h, --help Diese Hilfe.
Ohne Option prueft das Skript selbst, ob die venv noch zum System-Python Ohne Option prueft das Skript selbst, ob die venv noch zum System-Python
passt, und baut sie bei Bedarf neu. passt, und baut sie bei Bedarf neu.
Rauchtest (Standard: an)
Nach dem Start schiebt das Skript pro Instanz eine winzige Test-PDF durch
die echte Pipeline und prueft, ob sie in outgoing/ ankommt. Erst das zeigt
einen Totalausfall, den systemd nicht sieht (Dienst active, aber jede PDF
landet in error/). Test-PDF und Ergebnis werden danach restlos wieder
entfernt, auch im Fehlerfall.
Uebersprungen wird der Test bei Instanzen mit aktivem Upload-Ziel
(Nextcloud, SFTP, Ordner-Ziel ausserhalb von outgoing) oder aktivem
E-Mail-Notify: dort wuerde die Testdatei nach aussen gehen.
Ein fehlgeschlagener Rauchtest setzt den Exit-Code, rollt aber NICHTS
zurueck. Wartezeit pro Instanz: ${SMOKE_TIMEOUT}s (SMOKE_TIMEOUT=...).
EOF EOF
} }
@@ -223,6 +250,97 @@ pip_install_requirements() {
return 1 return 1
} }
# ============================================================
# Versionen der Kernabhaengigkeiten
# ============================================================
#
# Ein Downgrade (z.B. ocrmypdf 17.4.1 -> 16.13.0, weil requirements.txt neu
# gepinnt wurde) verschwand bis v0.6.0 wortlos hinter "[INFO] Dependencies ok".
# Genau so ist der Totalausfall aus Issue #7 entstanden. Deshalb: vorher und
# nachher messen und jede Aenderung benennen.
# Paketnamen aus einer requirements.txt (normalisiert: klein, Bindestriche).
pinned_package_names() {
local req
for req in "$@"; do
[ -f "$req" ] || continue
sed -n 's/^[[:space:]]*\([A-Za-z0-9._-]\{1,\}\)[[:space:]]*[<>=!~].*/\1/p' "$req"
done | tr '[:upper:]_' '[:lower:]-' | sort -u
}
# "paket version" je Zeile — nur fuer die in $2.. gepinnten Pakete.
snapshot_pinned_versions() {
local venv="$1"; shift
local line name ver n
local -a names=()
mapfile -t names < <(pinned_package_names "$@")
[ "${#names[@]}" -gt 0 ] || return 0
[ -x "$venv/bin/pip" ] || return 0
while IFS= read -r line; do
case "$line" in (*==*) ;; (*) continue ;; esac
name="$(printf '%s' "${line%%==*}" | tr '[:upper:]_' '[:lower:]-')"
ver="${line#*==}"
for n in "${names[@]}"; do
[ "$n" = "$name" ] && printf '%s %s\n' "$name" "$ver"
done
done < <("$venv/bin/pip" list --format=freeze 2>/dev/null || true)
}
# 0, wenn $1 eine aeltere Version als $2 ist.
version_is_older() {
[ "$1" = "$2" ] && return 1
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ]
}
# Vergleicht zwei Snapshots und fuellt DEP_UPGRADES/DEP_DOWNGRADES/DEP_NEW.
diff_pinned_versions() {
local before="$1" after="$2"
local name new old
DEP_UPGRADES=(); DEP_DOWNGRADES=(); DEP_NEW=()
while read -r name new; do
[ -n "$name" ] || continue
old="$(awk -v p="$name" '$1==p {print $2; exit}' "$before" 2>/dev/null || true)"
if [ -z "$old" ]; then
DEP_NEW+=("$name $new")
elif [ "$old" = "$new" ]; then
continue
elif version_is_older "$new" "$old"; then
DEP_DOWNGRADES+=("$name: $old -> $new")
else
DEP_UPGRADES+=("$name: $old -> $new")
fi
done < "$after"
}
# Gibt die Aenderungen aus. Downgrades als WARN — die sind das Gefaehrliche.
report_dependency_changes() {
local entry
if [ "${#DEP_DOWNGRADES[@]}" -eq 0 ] && [ "${#DEP_UPGRADES[@]}" -eq 0 ] \
&& [ "${#DEP_NEW[@]}" -eq 0 ]; then
log_info "Kernabhaengigkeiten unveraendert."
return 0
fi
for entry in "${DEP_DOWNGRADES[@]:-}"; do
[ -n "$entry" ] || continue
log_warn "DOWNGRADE: $entry"
done
if [ "${#DEP_DOWNGRADES[@]}" -gt 0 ]; then
log_warn " Ein Downgrade kommt aus einem gesenkten Pin in requirements.txt."
log_warn " Pruefen, ob das beabsichtigt ist — die aeltere Version kann auf"
log_warn " diesem System unbrauchbar sein, ohne dass systemd es merkt."
fi
for entry in "${DEP_UPGRADES[@]:-}"; do
[ -n "$entry" ] || continue
log_info "Upgrade: $entry"
done
for entry in "${DEP_NEW[@]:-}"; do
[ -n "$entry" ] || continue
log_info "Neu: $entry"
done
}
# Baut die venv neu: alte wegsichern, neue bauen, Requirements installieren, # Baut die venv neu: alte wegsichern, neue bauen, Requirements installieren,
# und erst bei Erfolg die alte entfernen. Scheitert etwas, wird die alte # und erst bei Erfolg die alte entfernen. Scheitert etwas, wird die alte
# zurueckgerollt und hart abgebrochen. # zurueckgerollt und hart abgebrochen.
@@ -534,6 +652,266 @@ check_all_configs() {
done done
} }
# ============================================================
# Rauchtest
# ============================================================
#
# systemd sagt "active", --check-config sagt "ok" — und trotzdem landet jede
# PDF in error/ (Issue #7). Das sieht man nur, wenn man wirklich eine Datei
# durch die Pipeline schiebt. Deshalb: eine Mini-PDF nach incoming/, warten,
# bis sie in outgoing/ ankommt, danach restlos aufraeumen.
#
# Die Test-PDF steckt als base64 hier drin — kein Pillow, kein gs, kein
# convert, damit der Test keine Abhaengigkeit mitbringt, die auf dem Zielsystem
# fehlen kann. 694 Bytes, eine Seite, eine Textzeile.
smoke_pdf_base64() {
cat <<'SMOKEPDF'
JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4K
ZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4K
ZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAg
MCA1OTUgODQyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA0IDAgUiA+PiA+PiAvQ29udGVu
dHMgNSAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1R5cGUx
IC9CYXNlRm9udCAvSGVsdmV0aWNhID4+CmVuZG9iago1IDAgb2JqCjw8IC9MZW5ndGggMTQ0ID4+
CnN0cmVhbQpCVCAvRjEgMjQgVGYgNzIgNzAwIFRkIChQREYgT0NSIEhPVEZPTERFUiBTTU9LRVRF
U1QpIFRqIEVUCkJUIC9GMSAxMSBUZiA3MiA2NzAgVGQgKEF1dG9tYXRpc2NoIGVyemV1Z3Qgdm9u
IHVwZGF0ZS5zaCAtIGJpdHRlIGlnbm9yaWVyZW4uKSBUaiBFVAplbmRzdHJlYW0KZW5kb2JqCnhy
ZWYKMCA2CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDAxNSAwMDAwMCBuIAowMDAwMDAwMDY0
IDAwMDAwIG4gCjAwMDAwMDAxMjEgMDAwMDAgbiAKMDAwMDAwMDI0NyAwMDAwMCBuIAowMDAwMDAw
MzE3IDAwMDAwIG4gCnRyYWlsZXIKPDwgL1NpemUgNiAvUm9vdCAxIDAgUiA+PgpzdGFydHhyZWYK
NTExCiUlRU9GCg==
SMOKEPDF
}
# Liest die fuer den Rauchtest noetigen Werte aus einer Instanz-Config.
# Ausgabe: "SCHLUESSEL<TAB>Wert" je Zeile.
smoke_read_config() {
local py="$1" cfg="$2"
"$py" - "$cfg" <<'SMOKECFG'
import sys
import tomllib
with open(sys.argv[1], "rb") as fh:
data = tomllib.load(fh)
def sec(*keys):
cur = data
for key in keys:
cur = cur.get(key, {}) if isinstance(cur, dict) else {}
return cur if isinstance(cur, dict) else {}
paths = sec("paths")
out = sec("output")
folder = sec("upload", "folder")
nextcloud = sec("upload", "nextcloud")
sftp = sec("upload", "sftp")
mail = sec("notify", "email")
# Alles, was die Testdatei nach aussen tragen wuerde. Defaults wie in config.py.
blockers = []
if nextcloud.get("enabled", False):
blockers.append("upload.nextcloud")
if sftp.get("enabled", False):
blockers.append("upload.sftp")
if mail.get("enabled", False):
blockers.append("notify.email")
# Ordner-Upload ohne target schreibt nach outgoing/ (harmlos); mit target
# geht die Datei irgendwo anders hin - oft eine Kundenfreigabe.
if folder.get("enabled", True) and str(folder.get("target", "")).strip():
blockers.append("upload.folder (target gesetzt)")
mode = str(out.get("original_on_success", "delete"))
archive = str(out.get("archive_dir", "")) if mode == "archive" else ""
for key, value in (
("INCOMING", paths.get("incoming", "")),
("OUTGOING", paths.get("outgoing", "")),
("WORKING", paths.get("working", "")),
("ERROR", paths.get("error", "")),
("ARCHIVE", archive),
("NAME_MODE", out.get("name_mode", "prefix")),
("NAME_TAG", out.get("name_tag", "OCR_")),
("BLOCKERS", ", ".join(blockers)),
):
print(f"{key}\t{value}")
SMOKECFG
}
# Bildet build_output_name() aus processor.py nach.
smoke_output_name() {
local src="$1" mode="$2" tag="$3"
if [ "$mode" = "none" ] || [ -z "$tag" ]; then
printf '%s' "$src"; return 0
fi
case "$mode" in
prefix) printf '%s%s' "$tag" "$src" ;;
suffix) printf '%s%s%s' "${src%.*}" "$tag" ".${src##*.}" ;;
*) printf '%s' "$src" ;;
esac
}
# Raeumt alle Spuren des Rauchtests weg — in JEDEM Ausgang. Nach dieser
# Funktion darf in keinem der Verzeichnisse etwas vom Test liegen bleiben.
smoke_cleanup() {
local in_dir="$1" out_dir="$2" work_dir="$3" err_dir="$4" arch_dir="$5"
local src_name="$6" out_name="$7"
local f
for f in "$in_dir/$src_name" \
"$work_dir/$src_name" \
"$work_dir/__ocr_$out_name" \
"$out_dir/$out_name" \
"$err_dir/$src_name" \
"$err_dir/$out_name" \
"${arch_dir:+$arch_dir/$src_name}"; do
[ -n "$f" ] || continue
[ -e "$f" ] && rm -f "$f" 2>/dev/null
done
return 0
}
# Schiebt eine Test-PDF durch eine laufende Instanz.
# 0 = bestanden, 1 = durchgefallen, 2 = uebersprungen
smoke_test_instance() {
local name="$1"
local py="$INSTALL_DIR/venv/bin/python"
local cfg="$CONFIG_DIR/$name.toml"
local key value
local INCOMING="" OUTGOING="" WORKING="" ERROR="" ARCHIVE=""
local NAME_MODE="prefix" NAME_TAG="OCR_" BLOCKERS=""
[ -f "$cfg" ] || { SMOKE_SKIP+=("$name (keine Config)"); return 2; }
[ -x "$py" ] || { SMOKE_SKIP+=("$name (kein venv-Python)"); return 2; }
local cfg_out
if ! cfg_out="$(smoke_read_config "$py" "$cfg" 2>&1)"; then
log_warn " ⚠ $name — Config liess sich fuer den Rauchtest nicht lesen:"
printf '%s\n' "$cfg_out" | tail -n 5
SMOKE_SKIP+=("$name (Config nicht lesbar)")
return 2
fi
while IFS="$(printf '\t')" read -r key value; do
case "$key" in
INCOMING) INCOMING="$value" ;;
OUTGOING) OUTGOING="$value" ;;
WORKING) WORKING="$value" ;;
ERROR) ERROR="$value" ;;
ARCHIVE) ARCHIVE="$value" ;;
NAME_MODE) NAME_MODE="$value" ;;
NAME_TAG) NAME_TAG="$value" ;;
BLOCKERS) BLOCKERS="$value" ;;
esac
done <<< "$cfg_out"
if [ -n "$BLOCKERS" ]; then
log_info " ⏭ $name — Rauchtest uebersprungen: aktive Ziele ($BLOCKERS)."
log_info " Eine Testdatei wuerde sonst beim Empfaenger landen."
SMOKE_SKIP+=("$name (aktive Ziele: $BLOCKERS)")
return 2
fi
if [ -z "$INCOMING" ] || [ -z "$OUTGOING" ]; then
SMOKE_SKIP+=("$name ([paths] unvollstaendig)")
return 2
fi
if [ ! -d "$INCOMING" ]; then
log_warn " ⚠ $name — $INCOMING existiert nicht; Rauchtest uebersprungen."
SMOKE_SKIP+=("$name (incoming fehlt)")
return 2
fi
# Eindeutiger Name, der mit keiner Kundendatei kollidieren kann.
local src_name out_name
src_name="__smoketest_update_$(date +%Y%m%d-%H%M%S)_$$.pdf"
out_name="$(smoke_output_name "$src_name" "$NAME_MODE" "$NAME_TAG")"
# Als Instanz-User anlegen, damit Rechte und Eigentuemer denen einer
# echten Scan-Datei entsprechen.
local unit_user
unit_user="$(systemctl show -p User --value "pdf-ocr-hotfolder@${name}.service" 2>/dev/null || true)"
[ -n "$unit_user" ] || unit_user="$PRIMARY_USER"
local tmp_pdf
tmp_pdf="$(mktemp)"
if ! smoke_pdf_base64 | base64 -d > "$tmp_pdf" 2>/dev/null; then
rm -f "$tmp_pdf"
log_warn " ⚠ $name — Test-PDF liess sich nicht erzeugen (base64 fehlt?)."
SMOKE_SKIP+=("$name (base64 fehlt)")
return 2
fi
# Erst fertig schreiben, dann hineinbewegen: der Watcher soll keine
# halb geschriebene Datei sehen.
local staged="$INCOMING/.$src_name.part"
cp "$tmp_pdf" "$staged" 2>/dev/null || {
rm -f "$tmp_pdf" "$staged"
log_warn " ⚠ $name — nach $INCOMING liess sich nicht schreiben."
SMOKE_SKIP+=("$name (incoming nicht beschreibbar)")
return 2
}
rm -f "$tmp_pdf"
chown "$unit_user":"$unit_user" "$staged" 2>/dev/null || true
chmod 644 "$staged" 2>/dev/null || true
if ! mv "$staged" "$INCOMING/$src_name" 2>/dev/null; then
rm -f "$staged"
log_warn " ⚠ $name — Testdatei liess sich nicht in $INCOMING einstellen."
SMOKE_SKIP+=("$name (incoming nicht beschreibbar)")
return 2
fi
# Warten: fertiges PDF in outgoing/ oder Abbruch in error/.
local waited=0 result=1
while [ "$waited" -lt "$SMOKE_TIMEOUT" ]; do
if [ -f "$OUTGOING/$out_name" ]; then result=0; break; fi
if [ -n "$ERROR" ] && { [ -f "$ERROR/$src_name" ] || [ -f "$ERROR/$out_name" ]; }; then
result=1; break
fi
sleep 1
waited=$((waited + 1))
done
smoke_cleanup "$INCOMING" "$OUTGOING" "$WORKING" "$ERROR" "$ARCHIVE" \
"$src_name" "$out_name"
if [ "$result" -eq 0 ]; then
log_info " ✅ $name — Test-PDF kam nach ${waited}s in outgoing/ an"
SMOKE_OK+=("$name")
return 0
fi
if [ "$waited" -ge "$SMOKE_TIMEOUT" ]; then
log_error " ❌ $name — Test-PDF war nach ${SMOKE_TIMEOUT}s nicht in $OUTGOING"
else
log_error " ❌ $name — Test-PDF landete in $ERROR statt in $OUTGOING"
fi
log_error " Der Dienst laeuft, verarbeitet aber nichts. Ursache im Journal:"
log_error " journalctl -u pdf-ocr-hotfolder@${name}.service -n 80 --no-pager"
SMOKE_FAIL+=("$name")
return 1
}
# Faehrt den Rauchtest fuer alle Instanzen, die nach dem Update laufen.
run_smoke_tests() {
SMOKE_OK=(); SMOKE_FAIL=(); SMOKE_SKIP=()
if [ "$SMOKE_TEST" -ne 1 ]; then
log_step "Rauchtest"
log_warn "Uebersprungen (--no-smoke-test). Ein Totalausfall bei der"
log_warn "Verarbeitung faellt damit erst der ersten echten Datei auf."
return 0
fi
if [ "${#STARTED_OK[@]}" -eq 0 ]; then
log_step "Rauchtest"
log_info "Keine laufende Instanz — nichts zu testen."
return 0
fi
log_step "Rauchtest (Test-PDF durch die echte Pipeline)"
local unit name
for unit in "${STARTED_OK[@]}"; do
name="${unit#pdf-ocr-hotfolder@}"
name="${name%.service}"
smoke_test_instance "$name" || true
done
}
# ============================================================ # ============================================================
# Backup # Backup
# ============================================================ # ============================================================
@@ -672,6 +1050,7 @@ fi
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case "$1" in case "$1" in
--rebuild-venv) REBUILD_VENV=1 ;; --rebuild-venv) REBUILD_VENV=1 ;;
--no-smoke-test) SMOKE_TEST=0 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) log_error "Unbekannte Option: $1"; echo; usage; exit 1 ;; *) log_error "Unbekannte Option: $1"; echo; usage; exit 1 ;;
esac esac
@@ -754,6 +1133,14 @@ stop_instances
create_backup create_backup
# Versionen der gepinnten Pakete festhalten, SOLANGE die alte
# requirements.txt noch liegt — sonst verschwindet ein Downgrade
# (z.B. ocrmypdf 17.4.1 -> 16.13.0) wortlos hinter "Dependencies ok ✓".
DEP_BEFORE="$(mktemp)"
DEP_AFTER="$(mktemp)"
snapshot_pinned_versions "$INSTALL_DIR/venv" \
"$INSTALL_DIR/requirements.txt" "$REPO_DIR/requirements.txt" > "$DEP_BEFORE" || true
log_step "Code aktualisieren" log_step "Code aktualisieren"
TOUCHED=1 TOUCHED=1
rm -rf "$INSTALL_DIR/pdf_ocr_hotfolder" rm -rf "$INSTALL_DIR/pdf_ocr_hotfolder"
@@ -772,6 +1159,12 @@ else
log_info "Dependencies ok ✓" log_info "Dependencies ok ✓"
fi fi
snapshot_pinned_versions "$INSTALL_DIR/venv" \
"$INSTALL_DIR/requirements.txt" "$REPO_DIR/requirements.txt" > "$DEP_AFTER" || true
diff_pinned_versions "$DEP_BEFORE" "$DEP_AFTER"
report_dependency_changes
rm -f "$DEP_BEFORE" "$DEP_AFTER"
install_units install_units
log_step "Berechtigungen setzen" log_step "Berechtigungen setzen"
@@ -785,10 +1178,16 @@ check_all_configs
log_step "Instanzen starten" log_step "Instanzen starten"
start_instances start_instances
# Ab hier wird nichts mehr getauscht. Der Rauchtest laeuft bewusst OHNE
# ERR-Trap: ein durchgefallener Test setzt den Exit-Code, loest aber keinen
# Abbruch und kein Zurueckrollen aus.
trap - ERR INT TERM
run_smoke_tests
# ============================================================ # ============================================================
# Zusammenfassung: Ist gegen Soll # Zusammenfassung: Ist gegen Soll
# ============================================================ # ============================================================
trap - ERR INT TERM
RC=0 RC=0
echo echo
@@ -844,13 +1243,51 @@ if [ "${#CFG_WARN[@]}" -gt 0 ]; then
log_warn "Config-Warnungen (Exit 1) bei: ${CFG_WARN[*]}" log_warn "Config-Warnungen (Exit 1) bei: ${CFG_WARN[*]}"
log_warn " Kein Abbruchgrund, aber bitte nachsehen:" log_warn " Kein Abbruchgrund, aber bitte nachsehen:"
for name in "${CFG_WARN[@]}"; do for name in "${CFG_WARN[@]}"; do
log_warn " $INSTALL_DIR/venv/bin/python -m pdf_ocr_hotfolder --check-config --config $CONFIG_DIR/$name.toml" # Das Paket ist nicht pip-installiert, sondern liegt in $INSTALL_DIR —
# ohne das cd findet Python das Modul nicht.
log_warn " cd $INSTALL_DIR && ./venv/bin/python -m pdf_ocr_hotfolder --check-config --config $CONFIG_DIR/$name.toml"
done done
fi fi
if [ "$APT_WARN" -eq 1 ]; then if [ "$APT_WARN" -eq 1 ]; then
log_warn "System-Pakete konnten nicht vollstaendig abgeglichen werden (siehe oben)." log_warn "System-Pakete konnten nicht vollstaendig abgeglichen werden (siehe oben)."
fi fi
# Versionsspruenge der Kernabhaengigkeiten hier noch einmal — im Log weiter
# oben gehen sie zwischen pip-Ausgaben unter.
if [ "${#DEP_DOWNGRADES[@]}" -gt 0 ]; then
log_warn "Abhaengigkeiten DOWNGEGRADED:"
for entry in "${DEP_DOWNGRADES[@]}"; do
log_warn " $entry"
done
log_warn " Das kommt aus requirements.txt. War es nicht beabsichtigt: Pin"
log_warn " korrigieren und update.sh --rebuild-venv erneut fahren."
fi
if [ "${#DEP_UPGRADES[@]}" -gt 0 ]; then
log_info "Abhaengigkeiten angehoben:"
for entry in "${DEP_UPGRADES[@]}"; do
log_info " $entry"
done
fi
if [ "${#DEP_NEW[@]}" -gt 0 ]; then
log_info "Abhaengigkeiten neu dazugekommen: ${DEP_NEW[*]}"
fi
if [ "$SMOKE_TEST" -ne 1 ]; then
log_warn "Rauchtest: uebersprungen (--no-smoke-test)"
else
[ "${#SMOKE_OK[@]}" -gt 0 ] && log_info "Rauchtest bestanden: ${SMOKE_OK[*]}"
[ "${#SMOKE_SKIP[@]}" -gt 0 ] && log_info "Rauchtest uebersprungen: ${SMOKE_SKIP[*]}"
if [ "${#SMOKE_FAIL[@]}" -gt 0 ]; then
log_error "RAUCHTEST FEHLGESCHLAGEN: ${SMOKE_FAIL[*]}"
log_error " Diese Instanzen laufen, verarbeiten aber keine PDFs."
log_error " Es wurde NICHT zurueckgerollt. Journal ansehen:"
for name in "${SMOKE_FAIL[@]}"; do
log_error " journalctl -u pdf-ocr-hotfolder@${name}.service -n 80 --no-pager"
done
RC=1
fi
fi
echo echo
if [ "$RC" -eq 0 ]; then if [ "$RC" -eq 0 ]; then
log_info "Update auf $NEW_VERSION abgeschlossen ✓" log_info "Update auf $NEW_VERSION abgeschlossen ✓"