Files
techadmin 465ff8873f fix: Ghostscript-Angebot log nicht mehr, fehlende Voraussetzungen dokumentiert (v0.7.1)
Befunde aus dem ersten echten Erstinstallations-Test auf frischen Debian-12-
und Debian-13-Containern. Der Weg selbst hat getragen (Basis-Install,
Instanz-Anlage, zweite Instanz, Update mit Rauchtest, Rollback) — diese
Stellen haben gelogen oder gefehlt:

- Das Ghostscript-Backports-Angebot auf Debian 12 war ein garantierter
  Leerlauf, der "aktualisiert ✓" meldete: bookworm-backports enthaelt gar
  kein ghostscript (am Paketindex verifiziert). Die Routine sucht jetzt den
  echten Kandidaten, vergleicht vorher/nachher und raeumt eine nur zur Probe
  angelegte Quelle wieder weg.
- Derselbe untaugliche Rat stand in der Preflight-Meldung, der
  pdfa_level-Warnung, config.example.toml und vier Doku-Dateien — ueberall
  ersetzt durch die echten Optionen.
- Mehrzeilige Log-Hinweise waren durch "echo -e" zerrissen und nicht
  kopierbar; log_* nutzt jetzt printf mit %s.
- pip-freeze.txt landete beim Rollback als /pip-freeze.txt im
  Wurzelverzeichnis, liegt jetzt unter opt/pdf-ocr-hotfolder/.
- git und sudo fehlen auf dem Proxmox-Debian-Template; "sudo ./install.sh"
  scheitert dort. Beide Wege dokumentiert, git als Voraussetzung ergaenzt,
  HTTPS-Clone als Normalfall.
- Rollback: systemctl start kann kein Glob. journald-Reparatur: Instanzen
  danach neu starten, sonst bleibt das Journal leer.

254 Tests gruen.

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

486 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Update
Aktualisieren des OCR-Tools mit `update.sh` — Code, venv, System-Pakete und
systemd-Unit.
Verwandte Dokumente: [README](../README.md) · [Installation](INSTALLATION.md) · [Debian-Major-Upgrade](OS-UPGRADE.md)
> Für ein **Debian-Major-Upgrade** (12 → 13) gilt ein eigener Ablauf — die venv
> muss danach neu gebaut werden. Siehe [OS-UPGRADE.md](OS-UPGRADE.md).
---
## Der Ablauf
```bash
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
```
> **`sudo` oder direkt als `root`.** `update.sh` braucht root-Rechte, nicht
> `sudo`. Wer als `root` arbeitet — im Proxmox-Debian-Standard-Template der
> Normalfall, dort ist `sudo` gar nicht installiert —, lässt das `sudo` bei
> jedem Befehl dieser Seite einfach weg: `./update.sh`. Ebenso setzt `git pull`
> ein installiertes **`git`** voraus; siehe
> [INSTALLATION.md](INSTALLATION.md#pakete-die-auf-einem-frischen-system-fehlen-können).
`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.
`install.sh` und `update.sh` teilen sich seit 0.7.0 die Datei `lib/common.sh`
(Log-Funktionen, Root-Prüfung, Layout-Pfade, apt-Paketliste, venv-Prüfung).
`update.sh` sourct bevorzugt die Fassung **neben sich** im Repo und fällt auf
die installierte unter `/opt/pdf-ocr-hotfolder/lib/` zurück; fehlt sie
überall, bricht es sofort ab statt mitten im Lauf. Die apt-Paketliste schneidet
es zusätzlich noch einmal per `sed` aus der Repo-Fassung heraus — beim Update
soll die neue Liste gelten, nicht die eventuell ältere installierte Kopie.
## Was das Skript tut — in dieser Reihenfolge
| # | Schritt | Anmerkung |
|---|---------|-----------|
| 1 | **Instanzen erfassen** | aktiv / kaputt / bewusst gestoppt, siehe [unten](#instanz-erfassung) |
| 2 | **System-Pakete abgleichen** | Liste wird aus `lib/common.sh` des Repos 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](#backup) |
| 6 | **Code kopieren** | `pdf_ocr_hotfolder/`, **`lib/`**, `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](#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 | **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
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](#config-drift-nach-einem-update) |
| `/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 **inkl. `lib/`**) | 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` | |
| `opt/pdf-ocr-hotfolder/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. Sie
liegt im Archiv unter `opt/pdf-ocr-hotfolder/` und landet beim Entpacken nach
`/` folglich als `/opt/pdf-ocr-hotfolder/pip-freeze.txt` — also neben der
Installation statt im Wurzelverzeichnis. Ein Code-Tausch beim nächsten Update
löscht sie nicht (dort fliegen nur `pdf_ocr_hotfolder/` und `lib/`).
**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:
```bash
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@kunde-a
sudo systemctl start pdf-ocr-hotfolder@kunde-b # jede Instanz einzeln!
```
Das Skript nennt diesen Befehl mit dem konkreten Archivnamen selbst — sowohl
beim Abbruch als auch bei einer erkannten Regression.
> ⚠️ **`start` kann kein Glob.** `systemctl stop 'pdf-ocr-hotfolder@*'` trifft
> alle laufenden Instanzen, weil systemd dafür die **bereits geladenen** Units
> auflösen kann. Beim Starten gibt es nichts aufzulösen:
> `systemctl start 'pdf-ocr-hotfolder@*'` startet eine Instanz mit dem
> wörtlichen Namen `*` — und scheitert. **Bei mehreren Instanzen muss jede
> einzeln gestartet werden.** Welche es sind:
> `ls /etc/pdf-ocr-hotfolder/*.toml`. Am Stück:
>
> ```bash
> for f in /etc/pdf-ocr-hotfolder/*.toml; do
> n=$(basename "$f" .toml)
> sudo systemctl start "pdf-ocr-hotfolder@$n"
> done
> systemctl status 'pdf-ocr-hotfolder@*' --no-pager
> ```
### Was ein Rollback nachweislich zurückholt
Einmal real durchgespielt (Debian 12 **und** 13, Update auf den neuen Stand,
danach Rollback auf das Backup). Zurück kamen korrekt:
- **der Code** unter `/opt/pdf-ocr-hotfolder/` inklusive `lib/` — die alte
Version lief danach wieder,
- **alle Instanz-Configs** unter `/etc/pdf-ocr-hotfolder/` — **mit den
`640`-Rechten und dem `root:<service-gruppe>`-Eigentum**; die
Klartext-Passwörter bleiben also geschützt, `tar` stellt Modus und Eigentümer
mit her,
- die **Template-Unit** `pdf-ocr-hotfolder@.service` und
- die **Drop-ins** unter `…@*.service.d/` (LXC-Kompat, User-Drop-in).
Nach `daemon-reload` und dem Einzelstart liefen die Instanzen wieder mit dem
alten Stand. Was dabei **nicht** zurückkommt, steht im nächsten Abschnitt.
### Grenzen des Rollbacks
Ein Rollback ist ein **Overlay**, kein exaktes Zurücksetzen — beides im Test
bestätigt:
- **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 — nach dem Entpacken unter `/opt/pdf-ocr-hotfolder/pip-freeze.txt`: die
dort genannten Versionen lassen sich von Hand wiederherstellen
(`venv/bin/pip install -r …`).
Im Test lief nach dem Rollback die **alte** Code-Version in der **neuen**
venv — was hier gutging, weil sich die Pins nicht geändert hatten. Verlassen
darf man sich darauf nicht: nach einem Update mit Versionssprung gehört die
venv nach dem Rollback von Hand auf den alten Paketstand gebracht.
- **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 — im Test nachgestellt und
bestätigt. 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.
---
## 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
dem neuen Code:
```bash
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](#rauchtest) und
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12)) 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](#config-drift-nach-einem-update)) |
| **2** | Config **unbrauchbar** — der Dienst würde nicht starten | Sofort korrigieren. Die Instanz gilt als **nicht** erfolgreich aktualisiert, das Update endet mit Exit 1 |
`--check-config` selbst kennt nur 0/1/2. Der **Dienst** kennt seit 0.7.0
zusätzlich **Exit 3** (Verzeichnis-Watch gestorben, Neustart erwünscht) — und
die Unit startet bei **Exit 2** absichtlich **nicht** mehr neu
(`RestartPreventExitStatus=2`), die Instanz bleibt sichtbar `failed` stehen.
Für das Update heißt das: eine Instanz mit Config-Fehler verschwindet nicht
mehr in einem stillen 5-Sekunden-Crash-Loop, sondern fällt in der
Zusammenfassung auf. Die vollständige Tabelle:
[INSTALLATION.md](INSTALLATION.md#exit-codes).
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:
```bash
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.**
```toml
[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 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).
### Relative Pfade — Fehler seit 0.7.0
Bis 0.6.3 wurde ein relativer Pfad in `[paths]`, `[output].archive_dir` oder
`[upload.folder].target` klaglos angenommen und gegen das `WorkingDirectory`
der Unit aufgelöst, also unter `/opt/pdf-ocr-hotfolder/`. Seit 0.7.0 ist das
ein **Config-Fehler mit Exit 2**: `--check-config` meldet ihn beim Update, und
der Dienst startet nicht.
Das ist der einzige Fall, in dem ein Update von 0.6.x eine bisher „laufende"
Instanz stoppen kann. Er ist gewollt — eine solche Instanz schrieb an einer
Stelle, an der niemand sie gesucht hat. Abhilfe: den Pfad absolut eintragen
und, falls dort Dateien liegen, den Inhalt von `/opt/pdf-ocr-hotfolder/<pfad>`
vorher herüberholen.
### 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
```bash
systemctl status 'pdf-ocr-hotfolder@*'
journalctl -u 'pdf-ocr-hotfolder@*' --since '5 min ago'
```
Und einmal eine Test-PDF durchschieben:
```bash
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.