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>
17 KiB
Update
Aktualisieren des OCR-Tools mit update.sh — Code, venv, System-Pakete und
systemd-Unit.
Verwandte Dokumente: README · Installation · Debian-Major-Upgrade
Für ein Debian-Major-Upgrade (12 → 13) gilt ein eigener Ablauf — die venv muss danach neu gebaut werden. Siehe OS-UPGRADE.md.
Der Ablauf
cd /pfad/zum/repo
git pull
sudo ./update.sh
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
es den gespeicherten Pfad aus /opt/pdf-ocr-hotfolder/.repo_path — das Repo
muss also liegen bleiben, das Tool kopiert daraus.
Was das Skript tut — in dieser Reihenfolge
| # | Schritt | Anmerkung |
|---|---|---|
| 1 | Instanzen erfassen | aktiv / kaputt / bewusst gestoppt, siehe unten |
| 2 | System-Pakete abgleichen | Liste wird aus install.sh extrahiert, apt-get install ist idempotent |
| 3 | venv prüfen | passt sie noch zum System-Python? Läuft vor dem Stoppen, damit man es früh sieht |
| 4 | Instanzen stoppen | nur die, die vorher liefen oder kaputt waren |
| 5 | Backup | Inhalt und Ort |
| 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. Versionssprünge der gepinnten Pakete werden benannt |
| 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 |
| 11 | Instanzen starten + verifizieren | mit Wartezeit und Crash-Loop-Erkennung |
| 12 | Rauchtest | eine Test-PDF durch die echte Pipeline, siehe unten |
| 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
unangetastet — es gibt kein purge und kein autoremove.
Die Schritte 1–3 verändern nichts auf der Platte. Erst ab Schritt 4 wird angefasst.
Was das Skript NICHT anfasst
| Bleibt unverändert | Warum |
|---|---|
/etc/pdf-ocr-hotfolder/*.toml |
Instanz-Configs werden nie überschrieben — weder neu geschrieben noch gemerged. Neue Config-Keys greifen über ihre Defaults, siehe Config-Drift |
/var/lib/pdf-ocr-hotfolder/… |
Datenverzeichnisse (incoming, working, outgoing, error, Archiv) — nichts wird verschoben oder gelöscht |
Instanz-Drop-ins (…@<instanz>.service.d/user.conf) |
Service-User pro Instanz bleibt |
| Nachinstallierte Tesseract-Sprachpakete | werden nicht entfernt |
| Bewusst gestoppte Instanzen | bleiben gestoppt |
Neue Config-Optionen muss man also selbst nachtragen, wenn man sie nutzen will.
config.example.toml liegt nach dem Update aktuell unter
/opt/pdf-ocr-hotfolder/config.example.toml und ist die Vorlage dafür.
Instanz-Erfassung
Eine Instanz gilt als bekannt, wenn sie irgendwo auftaucht: als geladene
Unit (systemctl list-units --all, also auch activating und failed), in
list-unit-files (enabled), oder als Config unter /etc/pdf-ocr-hotfolder/.
Damit fällt auch eine Instanz auf, die gerade in einem Crash-Loop hängt.
Aus dem Zustand vor dem Update ergeben sich drei Gruppen:
| Gruppe | Zustand vorher | Behandlung |
|---|---|---|
| lief sauber | active, nicht failed |
wird gestoppt und muss nachher wieder laufen — sonst ist das eine Regression und das Update meldet Exit 1 |
| war kaputt | failed, activating, reloading, deactivating |
wird mitgestartet; läuft sie danach, meldet das Skript "vorher kaputt, läuft jetzt". Läuft sie weiterhin nicht, Exit 1, aber ohne Regressions-Alarm |
| bewusst gestoppt | inactive und nicht failed |
bleibt gestoppt |
Verifikation nach dem Start
Ein systemctl start sagt bei Type=simple noch nichts. Deshalb prüft
verify_unit() nach einer Wartezeit (VERIFY_WAIT, Default 6 s) drei Dinge:
is-activemussactiveseinis-faileddarf nichtfailedmeldenNRestartsdarf nicht gestiegen sein — das entlarvt den Crash-Loop, der sich hinter einem sofortigen "active" versteckt
Vorher wird systemctl reset-failed gefahren, damit der alte Zustand die
Prüfung nicht verfälscht. Scheitert eine Instanz, nennt das Skript direkt den
passenden journalctl-Aufruf.
Die Zusammenfassung stellt am Ende Soll gegen Ist und liefert Exit 1, wenn eine Instanz fehlt, eine Config einen Fehler hat oder eine vorher kaputte Instanz immer noch kaputt ist.
Backup
Vor dem ersten Eingriff auf der Platte schreibt update.sh ein Archiv:
/var/backups/pdf-ocr-hotfolder/backup-YYYYmmdd-HHMMSS.tar.gz
| Enthalten | Nicht enthalten |
|---|---|
/opt/pdf-ocr-hotfolder/ (Code) |
die venv (venv/, venv.old-*) |
/etc/pdf-ocr-hotfolder/ (alle Instanz-Configs) |
die Datenverzeichnisse /var/lib/pdf-ocr-hotfolder/ |
Template-Unit pdf-ocr-hotfolder@.service |
__pycache__, *.pyc |
alle Drop-in-Verzeichnisse …@*.service.d |
|
pip-freeze.txt — pip freeze der alten venv plus Zeitstempel und Versionssprung |
pip-freeze.txt ist die Versicherung für den Fall, dass ein neuer Pin Ärger
macht: man sieht schwarz auf weiß, welche Paketversionen vorher liefen.
Rechte: Das Archiv enthält die Instanz-Configs und damit Klartext-Passwörter
(SMTP, Nextcloud, SFTP). Es wird deshalb mit umask 077 erzeugt und danach auf
0600 root:root gesetzt; das Verzeichnis selbst bekommt 700. Backups nicht in
Tickets anhängen und nicht in allgemein lesbare Pfade kopieren.
Rotation: Es werden die letzten 5 Archive behalten (BACKUP_KEEP),
ältere löscht das Skript nach dem Schreiben des neuen. Was entfernt wurde, steht
im Log.
Scheitert das Backup (typisch: volle Platte), bricht das Update ab, bevor etwas getauscht wurde.
Rollback
Das Backup-Archiv ist wurzelrelativ gepackt und lässt sich direkt zurückspielen:
sudo systemctl stop 'pdf-ocr-hotfolder@*'
sudo tar -xzf /var/backups/pdf-ocr-hotfolder/backup-YYYYmmdd-HHMMSS.tar.gz -C /
sudo systemctl daemon-reload
sudo systemctl start 'pdf-ocr-hotfolder@<instanz>'
Das Skript nennt diesen Befehl mit dem konkreten Archivnamen selbst — sowohl beim Abbruch als auch bei einer erkannten Regression.
Grenzen des Rollbacks
Ein Rollback ist ein Overlay, kein exaktes Zurücksetzen:
- Die venv ist nicht im Backup. Wurde sie beim Update neu gebaut oder
hat
pip install --upgradePakete angehoben, holt das Rollback den alten Stand der Pakete nicht zurück. Dafür istpip-freeze.txtaus dem Archiv da: die dort genannten Versionen lassen sich von Hand wiederherstellen (venv/bin/pip install -r …). - Dateien, die es vorher nicht gab, bleiben liegen.
tar -xlegt nur an und überschreibt; es löscht nichts. Eine mit dem neuen Stand hinzugekommene Datei im Code-Verzeichnis überlebt das Rollback. Sauberer ist deshalbrm -rf /opt/pdf-ocr-hotfolder/pdf_ocr_hotfoldervor dem Entpacken. - Die Datenverzeichnisse sind nicht im Backup — gewollt. Ein Rollback
verändert keine PDFs, weder in
incoming/noch inerror/. - System-Pakete werden nicht zurückgenommen. Ein per apt angehobenes Ghostscript oder ein neues Sprachpaket bleibt.
Der einfachere Weg zurück ist deshalb in den meisten Fällen: alten Stand im Repo
auschecken (git checkout v<version>) und sudo ./update.sh erneut fahren.
Der ERR-Trap
update.sh läuft mit set -Eeuo pipefail und hat ab dem Moment, in dem
Instanzen gestoppt werden, einen Trap auf ERR, INT und TERM. Bricht
irgendetwas ab — Fehler, Strg-C, kill —, dann:
- sagt das Skript laut, bei welchem Exit-Code und in welcher Zeile es aufhörte,
- sagt es, ob auf der Platte schon getauscht wurde oder ob der alte Stand unverändert ist,
- startet es die vorher laufenden Instanzen wieder und meldet jede einzeln,
- nennt es das Backup-Archiv und den Rollback-Befehl — oder sagt ausdrücklich, dass noch kein Backup geschrieben wurde.
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
gsund keinconvertauf dem Zielsystem. - Der Dateiname ist eindeutig (
__smoketest_update_<zeitstempel>_<pid>.pdf) und kann mit keiner Kundendatei kollidieren. - Wartezeit:
SMOKE_TIMEOUTSekunden (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.
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
dem neuen Code:
cd /opt/pdf-ocr-hotfolder && ./venv/bin/python -m pdf_ocr_hotfolder \
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
--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, 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 und
INSTALLATION.md) und
validiert die [output]-Sektion.
| Exit | Bedeutung | Was der Admin tun soll |
|---|---|---|
| 0 | Config sauber | nichts |
| 1 | Config nutzbar, aber mit Warnungen | Kein Abbruchgrund, der Dienst läuft. Die Warnungen aber nachziehen — sie nennen entweder einen Key, dessen Bedeutung sich geändert hat, oder einen Eintrag, der ins Leere läuft (s. Config-Drift) |
| 2 | Config unbrauchbar — der Dienst würde nicht starten | Sofort korrigieren. Die Instanz gilt als nicht erfolgreich aktualisiert, das Update endet mit Exit 1 |
Kennt der installierte Code --check-config noch nicht (Update von einem Stand
vor 0.6.0), erkennt update.sh das an der argparse-Meldung, überspringt die
Prüfung mit einer Warnung und läuft weiter.
Dieselben Warnungen schreibt der Dienst beim Start ins Journal — man sieht sie also auch ohne Update:
journalctl -u pdf-ocr-hotfolder@<instanz> | grep 'Config-Warnung'
Config-Drift nach einem Update
Instanz-Configs werden nie überschrieben. Das ist bequem, hat aber zwei Konsequenzen.
Neue Keys sind unkritisch. Fehlt ein Key, greift der Default aus der
Dataclass — genau der Wert, der auch in config.example.toml steht. Eine Config
von 0.2.x läuft unter 0.6.0 weiter, ohne dass etwas nachgetragen werden muss.
Wer die neue Option nutzen will, trägt sie nach; die aktuelle Vorlage liegt
nach jedem Update unter /opt/pdf-ocr-hotfolder/config.example.toml.
Zwei Fälle brauchen aber Handarbeit — beide meldet --check-config von selbst:
[ocr].timeout — Bedeutung geändert seit 0.4.0
Vor 0.4.0 war timeout ein Gesamt-Timeout pro PDF mit Default 1800 — und
wurde nirgends ausgewertet, war also wirkungslos. Seit 0.4.0 geht der Wert als
tesseract_timeout an ocrmypdf und ist damit das Limit pro Seite; ein
Dokument-Timeout kennt ocrmypdf nicht.
Wer den Altwert 1800 stehen hat, gibt Tesseract jetzt 30 Minuten je Seite.
Ab 900 meldet --check-config deshalb eine Warnung. Richtwert: 300.
[ocr]
timeout = 300 # Sekunden pro SEITE
0 heißt "kein eigenes Limit": der Wert wird dann gar nicht durchgereicht, weil
ocrmypdf tesseract_timeout=0 als "OCR komplett überspringen" interpretiert.
[ocr].pdfa_level — sollte leer sein
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 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 mitskip_text = truejede einzelne Datei. Die Entwarnung hängt also an der ocrmypdf-Version.requirements.txtpinnt darum 17.x, und der Preflight prüft beides zusammen. Hintergrund: INSTALLATION.md.
Unbekannte Keys
Einträge, die zu keiner Sektion und keinem Key gehören, werden beim Laden
ignoriert — aber gemeldet, mit Pfad ([ocr].langauges). Das deckt
Tippfehler und Optionen aus älteren Versionen ab. Die Meldung ist eine Warnung,
kein Fehler: der Dienst startet, der Eintrag tut nur nichts.
Nach dem Update prüfen
systemctl status 'pdf-ocr-hotfolder@*'
journalctl -u 'pdf-ocr-hotfolder@*' --since '5 min ago'
Und einmal eine Test-PDF durchschieben:
cp test.pdf /var/lib/pdf-ocr-hotfolder/<instanz>/incoming/
journalctl -u pdf-ocr-hotfolder@<instanz> -f
Im outgoing/ muss das OCR-PDF auftauchen.
Wiederaufnahme aus working/
Beim Start greift der Dienst nicht nur incoming/ auf, sondern zuerst
working/: Dateien, die ein harter Stopp dort liegen ließ, werden
wiederaufgenommen und das OCR läuft für sie neu. Unvollständige Zwischendateien
des abgebrochenen Laufs (Präfix __ocr_) werden dabei gelöscht.
Für das Update heißt das: ein systemctl stop mitten im OCR kostet den
angefangenen Durchlauf, aber keine Datei. Die Unit gibt einem laufenden OCR
TimeoutStopSec=300 Zeit, sauber fertig zu werden — ein Stop kann damit pro
Instanz bis zu 5 Minuten dauern. Bei mehreren Instanzen entsprechend länger;
update.sh stoppt sie nacheinander.