Files
pdf-ocr-hotfolder/docs/UPDATE.md
T
techadmin 3e24aa2ecd feat: Wiederaufnahme aus working/, --check-config, feldtauglicher Updater (v0.6.0)
Datenverlust behoben:
- Nach hartem Stopp blieb das Original in working/ liegen und wurde nie
  wieder angefasst (_scan_existing sah nur incoming/). Es wird jetzt beim
  Start an Ort und Stelle wieder aufgegriffen, mit Kollisionsschutz gegen
  gleichnamige neue Scans; angefangene __ocr_-Fragmente werden geloescht.
- TimeoutStopSec 30 -> 300, damit laufendes OCR zu Ende laufen darf.

Config-Drift sichtbar gemacht:
- Neues --check-config (Exit 0 sauber / 1 Warnungen / 2 Fehler), das
  update.sh vor dem Neustart ueber alle Instanz-Configs laufen laesst.
- Warnungen fuer [ocr].timeout >= 900 (seit 0.4.0 pro SEITE) und gesetztes
  pdfa_level, beim Dienststart wie im Check.
- Unbekannte Config-Keys werden nicht mehr still verworfen, sondern genannt.

Updater feldtauglich:
- venv-Health-Check erkennt toten Symlink UND Versions-Drift gegen das
  System-Python; --rebuild-venv als ausdruecklicher Weg nach einem Debian-
  Major-Upgrade. Neubau ist ganz-oder-gar-nicht mit Rollback.
- apt-Pakete werden auch beim Update synchronisiert (Quelle: install.sh).
- Instanz-Erfassung inkl. activating/failed, Verifikation prueft is-failed
  und NRestarts statt sleep 1 + is-active.
- Backup enthaelt Configs, Unit, Drop-ins und pip-freeze.txt, liegt auf
  0600 und rotiert auf 5; schlaegt es fehl, bricht das Update vorher ab.
- ERR-Trap faehrt die vorher laufenden Instanzen wieder hoch.
- lxc-compat.conf wird beim Update nachgezogen.
- requirements.txt gepinnt (ocrmypdf 16.13.0, geprueft fuer Python 3.11+3.13).

Doku in Installation / Update / OS-Upgrade aufgeteilt (docs/).
135 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 22:04:04 +02:00

13 KiB
Raw Permalink Blame History

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)

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
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 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:

  1. is-active muss active sein
  2. is-failed darf nicht failed melden
  3. NRestarts darf 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 --upgrade Pakete angehoben, holt das Rollback den alten Stand der Pakete nicht zurück. Dafür ist pip-freeze.txt aus 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 -x legt nur an und überschreibt; es löscht nichts. Eine mit dem neuen Stand hinzugekommene Datei im Code-Verzeichnis überlebt das Rollback. Sauberer ist deshalb rm -rf /opt/pdf-ocr-hotfolder/pdf_ocr_hotfolder vor dem Entpacken.
  • Die Datenverzeichnisse sind nicht im Backup — gewollt. Ein Rollback verändert keine PDFs, weder in incoming/ noch in error/.
  • 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:

  1. sagt das Skript laut, bei welchem Exit-Code und in welcher Zeile es aufhörte,
  2. sagt es, ob auf der Platte schon getauscht wurde oder ob der alte Stand unverändert ist,
  3. startet es die vorher laufenden Instanzen wieder und meldet jede einzeln,
  4. 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.


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:

/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 und PDF/A-Level, fährt den Preflight (tesseract, gs, Ghostscript-Version bei gesetztem pdfa_level) 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 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.

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.