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>
This commit is contained in:
+11
-7
@@ -1,8 +1,8 @@
|
||||
# AI Agent Briefing — PDF OCR Hotfolder
|
||||
|
||||
**Zuletzt aktualisiert:** 2026-09-22
|
||||
**Version:** 0.6.0
|
||||
**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.
|
||||
**Version:** 0.6.1
|
||||
**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:
|
||||
> [README.md](README.md) (Einstieg, Layout, Config-Überblick) ·
|
||||
@@ -26,7 +26,7 @@ pdf-ocr-hotfolder/
|
||||
│ ├── service.py # HotfolderService (watchdog + ThreadPool), Preflight, Resume, Zähler
|
||||
│ ├── processor.py # ocrmypdf-Call, veraPDF, Ausgabename, Original-Entsorgung
|
||||
│ └── 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
|
||||
│ ├── test_check_config.py # --check-config, Exit 0/1/2
|
||||
│ ├── test_config_errors.py
|
||||
@@ -230,7 +230,7 @@ wenn der installierte Code das Flag noch nicht kennt.
|
||||
## 🔄 Verarbeitungs-Flow
|
||||
|
||||
**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`
|
||||
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/`
|
||||
@@ -289,7 +289,11 @@ Der Service läuft in allen Fällen weiter (kein `exit 1` wie im alten Bash-Tool
|
||||
|
||||
## ⚠️ 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.
|
||||
- **`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`).
|
||||
@@ -312,14 +316,14 @@ python -m pdf_ocr_hotfolder --config /tmp/config.toml
|
||||
|
||||
Tests (aus dem Repo-Root, `pytest.ini` setzt `testpaths = tests`):
|
||||
```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.
|
||||
|
||||
## 📋 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] Config-Prüfung ohne Verarbeitung (`--check-config`) + Auswertung im Updater
|
||||
- [x] Updater übersteht Debian-Major-Upgrades (venv-Rebuild, Pins, Rollback)
|
||||
|
||||
+108
@@ -1,5 +1,113 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
@@ -146,7 +146,7 @@ gelöscht.
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pytest # 135 Tests
|
||||
pytest # 152 Tests
|
||||
```
|
||||
|
||||
`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.1
|
||||
**Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
|
||||
|
||||
+11
-3
@@ -21,9 +21,17 @@ skip_text = true
|
||||
# Auflösung für gerasterte Seiten
|
||||
oversample = 300
|
||||
# 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,
|
||||
# der mit pdfa_level + skip_text=true ocrmypdf komplett blockiert.
|
||||
# Sicherer Default ist "" — nur auf "1"/"2"/"3" setzen, wenn gs >= 10.02.1 installiert ist.
|
||||
# ACHTUNG: Ghostscript 10.0.0 bis 10.02.0 (Debian 12 default!) haben einen Bug;
|
||||
# ocrmypdf lehnt damit die Kombination pdfa_level + skip_text=true komplett ab.
|
||||
# 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 = ""
|
||||
# Schiefe Scans automatisch begradigen
|
||||
deskew = true
|
||||
|
||||
+42
-10
@@ -249,19 +249,47 @@ Container-Instanzen reißt.
|
||||
## Ghostscript-Bug auf Debian 12
|
||||
|
||||
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`:
|
||||
ocrmypdf blockiert komplett.
|
||||
enthält Regressionen, die PDFs mit vorhandenem Text beschädigen. ocrmypdf
|
||||
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).
|
||||
- Der Preflight beim Dienststart bricht mit **Exit 2** ab, wenn `pdfa_level`
|
||||
gesetzt **und** die installierte Ghostscript-Version betroffen ist.
|
||||
- `--check-config` meldet ein gesetztes `pdfa_level` als Warnung (siehe
|
||||
Die Bedingung steht in `ocrmypdf/builtin_plugins/ghostscript.py`
|
||||
(`check_options()`) und hängt an **zwei** Dingen — an der Config *und* an der
|
||||
ocrmypdf-Version:
|
||||
|
||||
| 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)).
|
||||
- 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
|
||||
bookworm-backports an. Manuell:
|
||||
### Abhilfe
|
||||
|
||||
**Weg 1 — Ghostscript anheben** (empfohlen). Der Installer erkennt betroffene
|
||||
Versionen und bietet auf Debian 12 bookworm-backports an. Manuell:
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
@@ -325,7 +357,7 @@ startet nicht.
|
||||
| `jobs` | `4` | Threads, die ocrmypdf innerhalb **einer** PDF nutzt |
|
||||
| `skip_text` | `true` | Seiten, die schon Text haben, nicht neu OCRen |
|
||||
| `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 |
|
||||
| `clean` | `false` | Hintergrund säubern (unpaper) |
|
||||
| `max_workers` | `2` | wie viele PDFs **parallel** verarbeitet werden |
|
||||
|
||||
+26
-9
@@ -191,28 +191,45 @@ Upgrade entfernt. Hintergrund:
|
||||
Die Python-Abhängigkeiten sind **bewusst fest gepinnt**:
|
||||
|
||||
```
|
||||
ocrmypdf==16.13.0
|
||||
ocrmypdf==17.4.1
|
||||
watchdog==6.0.0
|
||||
requests==2.33.1
|
||||
paramiko==4.0.0
|
||||
```
|
||||
|
||||
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
|
||||
Instanzen auf einmal, und zwar im Moment des Updates, nicht zu einem Zeitpunkt,
|
||||
den man sich ausgesucht hat.
|
||||
Major-Version ziehen — der nächste Sprung wäre ocrmypdf **17 auf 18**, und der
|
||||
reißt sonst alle Instanzen auf einmal, und zwar im Moment des Updates, nicht zu
|
||||
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)
|
||||
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:**
|
||||
|
||||
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.
|
||||
3. `pytest` muss grün bleiben (135 Tests).
|
||||
4. Eine echte PDF durchschieben — die Test-Suite mockt ocrmypdf, ein Major-Sprung
|
||||
fällt dort also nicht auf.
|
||||
4. `pytest` muss grün bleiben (152 Tests).
|
||||
5. Eine echte PDF durchschieben — die Test-Suite mockt ocrmypdf, ein Major-Sprung
|
||||
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.
|
||||
|
||||
Der ocrmypdf-Sprung 16 → 17 ist ein **Major-Sprung** und gehört in einen eigenen
|
||||
|
||||
+109
-9
@@ -19,8 +19,9 @@ sudo ./update.sh
|
||||
```
|
||||
|
||||
```
|
||||
sudo ./update.sh --help # Optionen anzeigen
|
||||
sudo ./update.sh --rebuild-venv # venv zwingend neu bauen (nach dist-upgrade)
|
||||
sudo ./update.sh --help # Optionen anzeigen
|
||||
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
|
||||
@@ -37,12 +38,13 @@ muss also liegen bleiben**, das Tool kopiert daraus.
|
||||
| 4 | **Instanzen stoppen** | nur die, die vorher liefen oder kaputt waren |
|
||||
| 5 | **Backup** | [Inhalt und Ort](#backup) |
|
||||
| 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` |
|
||||
| 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) |
|
||||
| 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
|
||||
nur bei der Installation. Bereits installierte Tesseract-Sprachpakete bleiben
|
||||
@@ -187,6 +189,95 @@ 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`
|
||||
|
||||
Nach dem Code-Update und vor dem Start prüft `update.sh` jede Instanz-Config mit
|
||||
@@ -199,8 +290,11 @@ dem neuen Code:
|
||||
|
||||
`--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
|
||||
fehlt), Sprachen, Seiten-Timeout und PDF/A-Level, fährt den Preflight
|
||||
(`tesseract`, `gs`, Ghostscript-Version bei gesetztem `pdfa_level`) und
|
||||
fehlt), Sprachen, Seiten-Timeout, PDF/A-Level, `skip_text` sowie die
|
||||
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.
|
||||
|
||||
| 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
|
||||
`--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
|
||||
mit Exit 2 ab; ab Ghostscript 10.02.1 ist PDF/A unproblematisch. Hintergrund:
|
||||
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
|
||||
Kombination mit `skip_text` von ocrmypdf abgelehnt wird. Der Preflight bricht in
|
||||
dem Fall mit Exit 2 ab; ab Ghostscript 10.02.1 ist PDF/A unproblematisch.
|
||||
|
||||
> **`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
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen."""
|
||||
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.6.1"
|
||||
|
||||
@@ -14,6 +14,8 @@ from .service import (
|
||||
PreflightError,
|
||||
check_output_config,
|
||||
check_preflight,
|
||||
detect_ghostscript_version,
|
||||
detect_ocrmypdf_version,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -70,11 +72,15 @@ def check_config(cfg_path: Path) -> int:
|
||||
print(f" OCR-Sprachen = {cfg.ocr.languages}")
|
||||
print(f" Seiten-Timeout= {cfg.ocr.timeout} s")
|
||||
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] = []
|
||||
try:
|
||||
check_preflight(cfg.ocr.pdfa_level)
|
||||
print(" Preflight ok (tesseract, gs vorhanden).")
|
||||
check_preflight(cfg.ocr.pdfa_level, cfg.ocr.skip_text)
|
||||
print(" Preflight ok (tesseract, gs vorhanden, Ghostscript-Version "
|
||||
"passt zu ocrmypdf + [ocr]-Einstellungen).")
|
||||
except PreflightError as e:
|
||||
errors.append(str(e))
|
||||
try:
|
||||
|
||||
@@ -25,8 +25,13 @@ class OcrConfig:
|
||||
jobs: int = 4
|
||||
skip_text: bool = True
|
||||
oversample: int = 300
|
||||
# Default bewusst leer: pdfa_level + skip_text zerschießt OCR mit
|
||||
# Ghostscript 10.0.0-10.02.0 (Debian-12-Default), siehe Issue #3
|
||||
# Default bewusst leer: mit Ghostscript 10.0.0-10.02.0 (Debian-12-Default)
|
||||
# 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 = ""
|
||||
deskew: bool = True
|
||||
clean: bool = False
|
||||
@@ -251,9 +256,10 @@ def legacy_warnings(cfg: Config) -> list[str]:
|
||||
out.append(
|
||||
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 "
|
||||
"Bug, der zusammen mit skip_text das OCR blockiert (Issue #3). Der "
|
||||
"Preflight bricht ab, falls die installierte Ghostscript-Version "
|
||||
"betroffen ist; ab 10.02.1 ist alles in Ordnung."
|
||||
"Bug, wegen dem ocrmypdf die Kombination mit skip_text ablehnt "
|
||||
"(Issue #3). Der Preflight bricht ab, falls die installierte "
|
||||
"Ghostscript-Version betroffen ist; ab 10.02.1 ist alles in "
|
||||
"Ordnung."
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
+113
-15
@@ -35,11 +35,20 @@ class PreflightError(RuntimeError):
|
||||
# Pflicht-Binaries für ocrmypdf
|
||||
_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.
|
||||
_GS_BROKEN_MIN = (10, 0, 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:
|
||||
"""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:
|
||||
"""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.
|
||||
"""
|
||||
@@ -79,6 +88,36 @@ def detect_ghostscript_version() -> str | 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,
|
||||
name_mode: str = "prefix") -> None:
|
||||
"""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.
|
||||
|
||||
- Tesseract und Ghostscript müssen im PATH sein
|
||||
- Bei gesetztem pdfa_level wird die Ghostscript-Version gegen den
|
||||
bekannten 10.0.0–10.02.0 Bug geprüft
|
||||
- Die Ghostscript-Version wird gegen den bekannten 10.0.0–10.02.0 Bug
|
||||
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.
|
||||
"""
|
||||
@@ -117,15 +157,73 @@ def check_preflight(pdfa_level: str = "") -> None:
|
||||
+ ". 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:
|
||||
gs_version = detect_ghostscript_version()
|
||||
if is_ghostscript_broken(gs_version):
|
||||
raise PreflightError(
|
||||
f"Ghostscript {gs_version} ist mit pdfa_level='{pdfa_level}' nicht "
|
||||
"kompatibel (bekannter Bug in 10.0.0–10.02.0). "
|
||||
"Entweder ghostscript auf >=10.02.1 upgraden (z.B. via bookworm-backports) "
|
||||
"oder in der Config [ocr].pdfa_level = \"\" setzen."
|
||||
)
|
||||
ursache = (
|
||||
f"[ocr].pdfa_level = {pdfa_level!r} (PDF/A-Ausgabe) zusammen mit "
|
||||
"[ocr].skip_text = true"
|
||||
)
|
||||
else:
|
||||
ursache = (
|
||||
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:
|
||||
@@ -201,7 +299,7 @@ class HotfolderService:
|
||||
# ---- Lifecycle ----
|
||||
|
||||
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,
|
||||
self.cfg.output.archive_dir,
|
||||
self.cfg.output.name_mode)
|
||||
@@ -228,7 +326,7 @@ class HotfolderService:
|
||||
Returns:
|
||||
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,
|
||||
self.cfg.output.archive_dir,
|
||||
self.cfg.output.name_mode)
|
||||
|
||||
+17
-2
@@ -1,9 +1,24 @@
|
||||
# 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
|
||||
# gibt es fertige Wheels, es wird nichts kompiliert.
|
||||
# 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
|
||||
requests==2.33.1
|
||||
paramiko==4.0.0
|
||||
|
||||
@@ -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 contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -9,9 +19,21 @@ from pdf_ocr_hotfolder.service import (
|
||||
PreflightError,
|
||||
check_preflight,
|
||||
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", [
|
||||
# Betroffene Versionen
|
||||
("10.0.0", True),
|
||||
@@ -36,29 +58,92 @@ def test_is_ghostscript_broken(version, expected) -> None:
|
||||
assert is_ghostscript_broken(version) is expected
|
||||
|
||||
|
||||
def test_check_preflight_without_pdfa_passes_with_broken_gs() -> None:
|
||||
"""Ohne pdfa_level darf der betroffene GS verwendet werden."""
|
||||
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
|
||||
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
|
||||
return_value="10.0.0"):
|
||||
check_preflight(pdfa_level="") # darf nicht werfen
|
||||
@pytest.mark.parametrize("version,expected", [
|
||||
("16.13.0", True), # der Pin aus v0.6.0, der den Ausfall ausgeloest hat
|
||||
("16.0.0", True),
|
||||
("15.4.4", True),
|
||||
("17.0.0", False), # ab hier steckt die Pruefung hinter output_type
|
||||
("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:
|
||||
"""Mit pdfa_level + kaputtem GS → PreflightError mit hilfreicher Meldung."""
|
||||
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
|
||||
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
|
||||
return_value="10.0.0"):
|
||||
# ---------------- Preflight: ocrmypdf 17.x (unser Pin) ----------------
|
||||
|
||||
def test_broken_gs_skip_text_without_pdfa_passes_on_ocrmypdf_17() -> None:
|
||||
"""Der Debian-12-Standardfall: ohne PDF/A fasst ocrmypdf 17 gs nicht an.
|
||||
|
||||
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"):
|
||||
check_preflight(pdfa_level="2")
|
||||
check_preflight(pdfa_level="2", skip_text=True)
|
||||
|
||||
|
||||
def test_check_preflight_with_pdfa_passes_on_fixed_gs() -> None:
|
||||
"""Mit pdfa_level + gefixtem GS → ok."""
|
||||
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
|
||||
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
|
||||
return_value="10.02.1"):
|
||||
check_preflight(pdfa_level="2") # darf nicht werfen
|
||||
def test_broken_gs_with_pdfa_without_skip_text_passes() -> None:
|
||||
"""Ohne skip_text greift die ocrmypdf-Bedingung nicht — kein Abbruch."""
|
||||
with _env("10.0.0", "17.4.1"):
|
||||
check_preflight(pdfa_level="2", skip_text=False) # 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:
|
||||
@@ -70,3 +155,47 @@ def test_default_config_pdfa_level_is_empty() -> None:
|
||||
data = tomllib.load(f)
|
||||
assert data["ocr"]["pdfa_level"] == "", \
|
||||
"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
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# Aufruf:
|
||||
# sudo ./update.sh # normales Update
|
||||
# sudo ./update.sh --rebuild-venv # venv-Neubau erzwingen (nach dist-upgrade)
|
||||
# sudo ./update.sh --no-smoke-test # ohne Rauchtest durchlaufen
|
||||
#
|
||||
set -Eeuo pipefail
|
||||
|
||||
@@ -32,6 +33,10 @@ log_step() { echo -e "\n${BLUE}==>${NC} $*"; }
|
||||
: "${TAR_ROOT:=/}"
|
||||
: "${VERIFY_WAIT:=6}"
|
||||
: "${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"
|
||||
UNIT_GLOB='pdf-ocr-hotfolder@*.service'
|
||||
LXC_DROPIN_DIR="$SYSTEMD_DIR/${SERVICE_TEMPLATE}.d"
|
||||
@@ -44,6 +49,7 @@ LXC_SYNCED=0
|
||||
BACKUP_FILE=""
|
||||
PRIMARY_USER="pdfocr"
|
||||
TOUCHED=0 # 1, sobald auf der Platte etwas getauscht wurde
|
||||
SMOKE_TEST=1 # per --no-smoke-test abschaltbar
|
||||
|
||||
UNITS_ALL=() # alle bekannten Instanz-Units
|
||||
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)
|
||||
STARTED_OK=()
|
||||
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() {
|
||||
cat <<EOF
|
||||
@@ -65,10 +77,25 @@ Optionen:
|
||||
installieren. Das ist der Aufruf nach einem Debian-
|
||||
Major-Upgrade (apt full-upgrade, z.B. 12 -> 13), wenn das
|
||||
System-Python eine neue Version bekommen hat.
|
||||
--no-smoke-test Den Rauchtest nach dem Start auslassen.
|
||||
-h, --help Diese Hilfe.
|
||||
|
||||
Ohne Option prueft das Skript selbst, ob die venv noch zum System-Python
|
||||
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
|
||||
}
|
||||
|
||||
@@ -223,6 +250,97 @@ pip_install_requirements() {
|
||||
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,
|
||||
# und erst bei Erfolg die alte entfernen. Scheitert etwas, wird die alte
|
||||
# zurueckgerollt und hart abgebrochen.
|
||||
@@ -534,6 +652,266 @@ check_all_configs() {
|
||||
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
|
||||
# ============================================================
|
||||
@@ -672,6 +1050,7 @@ fi
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--rebuild-venv) REBUILD_VENV=1 ;;
|
||||
--no-smoke-test) SMOKE_TEST=0 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) log_error "Unbekannte Option: $1"; echo; usage; exit 1 ;;
|
||||
esac
|
||||
@@ -754,6 +1133,14 @@ stop_instances
|
||||
|
||||
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"
|
||||
TOUCHED=1
|
||||
rm -rf "$INSTALL_DIR/pdf_ocr_hotfolder"
|
||||
@@ -772,6 +1159,12 @@ else
|
||||
log_info "Dependencies ok ✓"
|
||||
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
|
||||
|
||||
log_step "Berechtigungen setzen"
|
||||
@@ -785,10 +1178,16 @@ check_all_configs
|
||||
log_step "Instanzen starten"
|
||||
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
|
||||
# ============================================================
|
||||
trap - ERR INT TERM
|
||||
|
||||
RC=0
|
||||
echo
|
||||
@@ -851,6 +1250,42 @@ if [ "$APT_WARN" -eq 1 ]; then
|
||||
log_warn "System-Pakete konnten nicht vollstaendig abgeglichen werden (siehe oben)."
|
||||
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
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
log_info "Update auf $NEW_VERSION abgeschlossen ✓"
|
||||
|
||||
Reference in New Issue
Block a user