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>
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
# Installation
|
||||
|
||||
Erstinstallation und Anlage von Hotfolder-Instanzen mit `install.sh`.
|
||||
|
||||
Verwandte Dokumente: [README](../README.md) · [Update](UPDATE.md) · [Debian-Major-Upgrade](OS-UPGRADE.md)
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
| Punkt | Anforderung |
|
||||
|-------|-------------|
|
||||
| Betriebssystem | Debian 12 (bookworm) oder Debian 13 — systemd wird vorausgesetzt |
|
||||
| Python | 3.11+ (wegen `tomllib` aus der stdlib); kommt aus der Distribution |
|
||||
| Rechte | `root` (`sudo ./install.sh`) |
|
||||
| Netz | apt-Zugriff für die System-Pakete, PyPI-Zugriff für die venv |
|
||||
| Repo | muss dauerhaft liegen bleiben — `update.sh` kopiert daraus (s. [UPDATE.md](UPDATE.md)) |
|
||||
|
||||
Die System-Pakete installiert der Installer selbst. Die Liste steht als
|
||||
einzige Quelle in `install.sh` (Funktion `pdf_ocr_apt_packages()`, zwischen den
|
||||
Marken `# --- BEGIN apt-packages` / `# --- END apt-packages`) und wird von
|
||||
`update.sh` von dort ausgelesen:
|
||||
|
||||
```
|
||||
python3 python3-venv python3-pip
|
||||
tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng
|
||||
ghostscript qpdf unpaper pngquant icc-profiles-free
|
||||
ca-certificates curl
|
||||
```
|
||||
|
||||
Weitere Tesseract-Sprachpakete installiert der Installer bei Bedarf pro Instanz
|
||||
nach (siehe [OCR-Sprachen](#3-ocr-sprachen)).
|
||||
|
||||
Die Python-Abhängigkeiten stehen **fest gepinnt** in `requirements.txt`
|
||||
(ocrmypdf, watchdog, requests, paramiko). Warum das so ist und wie man die Pins
|
||||
anhebt: [OS-UPGRADE.md](OS-UPGRADE.md#pins-in-requirementstxt).
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
|
||||
cd pdf-ocr-hotfolder
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
`install.sh` ist **Installer und Instanz-Manager in einem** und idempotent —
|
||||
jeder weitere Aufruf überspringt, was schon steht.
|
||||
|
||||
### Basis-Install vs. Instanz-Anlage
|
||||
|
||||
Der Installer unterscheidet zwei Ebenen:
|
||||
|
||||
| Ebene | Wann | Was passiert |
|
||||
|-------|------|--------------|
|
||||
| **Basis-Install** | einmalig; erkannt an `venv` + Template-Unit | System-Pakete, Ghostscript-Check, Container-Erkennung, Default-User `pdfocr`, Code nach `/opt/pdf-ocr-hotfolder/`, venv, systemd-Template-Unit |
|
||||
| **Instanz-Anlage** | bei jedem Lauf, beliebig oft | Abfragen pro Instanz, Datenverzeichnisse, `<instanz>.toml`, optionales User-Drop-in, `enable --now` |
|
||||
|
||||
Ist die Basis-Installation vorhanden, aber die venv passt nicht mehr zum
|
||||
System-Python (typisch nach einem Distributions-Upgrade), läuft der Basis-Install
|
||||
**zur Reparatur erneut**: die alte venv wird nach `venv.old-<timestamp>`
|
||||
weggesichert und neu gebaut. Für den geplanten Weg über ein Debian-Major-Upgrade
|
||||
ist aber `update.sh --rebuild-venv` gedacht, siehe [OS-UPGRADE.md](OS-UPGRADE.md).
|
||||
|
||||
Beim Erstlauf ist mindestens **eine** Instanz Pflicht. Danach fragt der
|
||||
Installer in der Schleife `Weitere Instanz anlegen? [j/N]:`.
|
||||
|
||||
---
|
||||
|
||||
## Die Abfragen pro Instanz
|
||||
|
||||
`create_instance()` stellt fünf Fragen. Alle Antworten gelten **nur für diese
|
||||
Instanz** — nichts davon ist global.
|
||||
|
||||
### 1. Instanz-Name
|
||||
|
||||
```
|
||||
Instanz-Name (nur a-z, 0-9, -):
|
||||
```
|
||||
|
||||
Muster `^[a-z0-9][a-z0-9-]*$`. Der Name wird zum Unit-Suffix
|
||||
(`pdf-ocr-hotfolder@<name>.service`) und zum Config-Dateinamen
|
||||
(`/etc/pdf-ocr-hotfolder/<name>.toml`). Existiert die Config schon, bricht die
|
||||
Anlage ab — ein versehentliches Überschreiben gibt es nicht.
|
||||
|
||||
### 2. Basis-Pfad für die Daten
|
||||
|
||||
```
|
||||
Basis-Pfad für Daten [/var/lib/pdf-ocr-hotfolder/<name>]:
|
||||
```
|
||||
|
||||
Darunter entstehen `incoming/`, `outgoing/`, `working/`, `error/` und werden auf
|
||||
den Service-User gechownt.
|
||||
|
||||
### 3. Service-User
|
||||
|
||||
```
|
||||
Service-User [pdfocr]:
|
||||
```
|
||||
|
||||
- **Existiert der User** (lokal oder als **AD-User via SSSD/Winbind**), wird er
|
||||
übernommen; die primäre Gruppe ermittelt der Installer per `id -gn`.
|
||||
- **Existiert er nicht**, bietet der Installer an, ihn lokal als System-User
|
||||
anzulegen. Wird das abgelehnt, bricht die Instanz-Anlage ab — der User muss
|
||||
dann erst über AD/SSSD bereitstehen.
|
||||
- Ist der gewählte User **nicht** `pdfocr`, legt der Installer das Drop-in
|
||||
`/etc/systemd/system/pdf-ocr-hotfolder@<instanz>.service.d/user.conf` mit
|
||||
`User=`/`Group=` an.
|
||||
|
||||
Bei AD-Usern mit lokaler UID werden die Datei-Berechtigungen über die UID
|
||||
gesetzt — das läuft transparent.
|
||||
|
||||
### 4. OCR-Sprachen
|
||||
|
||||
```
|
||||
Tesseract-Sprachen [deu+eng]:
|
||||
```
|
||||
|
||||
Die Sprachen werden bewusst **je Instanz** abgefragt: ein Hotfolder
|
||||
`buchhaltung` sieht nur deutsche Belege, ein Hotfolder `export`
|
||||
internationale Korrespondenz.
|
||||
|
||||
```toml
|
||||
# /etc/pdf-ocr-hotfolder/buchhaltung.toml
|
||||
languages = "deu"
|
||||
|
||||
# /etc/pdf-ocr-hotfolder/export.toml
|
||||
languages = "deu+eng+fra"
|
||||
```
|
||||
|
||||
**Die Liste so eng wie möglich halten.** Jede zusätzliche Sprache kostet
|
||||
Laufzeit *und* Erkennungsqualität: Tesseract wägt mehr Modelle gegeneinander ab
|
||||
und verwechselt dabei Wörter, die in einer Sprache eindeutig wären.
|
||||
`deu+eng+fra` auf reinen Deutsch-Scans ist kein Sicherheitsnetz, sondern ein
|
||||
Rückschritt.
|
||||
|
||||
Was der Installer damit macht:
|
||||
|
||||
1. **Format prüfen** — Sprachcodes mit `+` verbunden
|
||||
(`^[a-z]{3}(_[A-Za-z]+)?(\+…)*$`), also `deu`, `deu+eng`, `chi_sim+eng`.
|
||||
Bei Unsinn wird erneut gefragt, nicht abgebrochen.
|
||||
2. **Jeden Code gegen `tesseract --list-langs` prüfen.** Fehlt eine
|
||||
Sprachdatei, bietet er das passende apt-Paket an: `tesseract-ocr-<code>`,
|
||||
Unterstrich wird zum Bindestrich (`chi_sim` → `tesseract-ocr-chi-sim`).
|
||||
3. **Lehnt man ab oder scheitert die Installation**, warnt er, dass OCR mit
|
||||
dieser Sprache **bei jeder Datei** scheitern würde, und fragt die Sprachen
|
||||
erneut ab — die fehlende Sprache kann man dann einfach weglassen.
|
||||
4. Ist `tesseract` gar nicht aufrufbar, wird die Prüfung übersprungen und die
|
||||
Eingabe unverändert übernommen.
|
||||
|
||||
### 5. Original archivieren?
|
||||
|
||||
```
|
||||
Original nach erfolgreichem OCR archivieren? [j/N]:
|
||||
```
|
||||
|
||||
- **Nein** (Default) → `original_on_success = "delete"`, das Original wird nach
|
||||
erfolgreichem OCR gelöscht.
|
||||
- **Ja** → `original_on_success = "archive"`, danach:
|
||||
|
||||
```
|
||||
Archiv-Verzeichnis [<basis>/archive]:
|
||||
```
|
||||
|
||||
Der Pfad muss **absolut** sein und darf **nicht** `incoming/`, `outgoing/`,
|
||||
`working/` oder `error/` der Instanz sein — im Eingang würde das Original sonst
|
||||
endlos neu aufgegriffen, in den übrigen kollidiert es mit der Verarbeitung. Das
|
||||
Verzeichnis wird angelegt und auf den Service-User gechownt; innerhalb des
|
||||
Basis-Pfads erledigt das bestehende `chown -R` das mit, ein Archiv **außerhalb**
|
||||
bekommt ein eigenes.
|
||||
|
||||
### Was danach passiert
|
||||
|
||||
Die Instanz-Config entsteht per `sed` aus `config.example.toml`. Substituiert
|
||||
werden die vier `[paths]`-Zeilen sowie `[ocr].languages`,
|
||||
`[output].original_on_success` und `[output].archive_dir`. Anschließend liest
|
||||
der Installer diese drei Keys aus der erzeugten Datei zurück und vergleicht sie
|
||||
mit der Eingabe; erst wenn das passt, nennt die Zusammenfassung Sprachen und
|
||||
Archiv-Verzeichnis. Sonst gibt es eine Warnung mit der Bitte, die Config von
|
||||
Hand nachzuziehen.
|
||||
|
||||
Die Config bekommt `chmod 640` und `chown root:<service-gruppe>`, das
|
||||
Verzeichnis `/etc/pdf-ocr-hotfolder` selbst `750 root:pdfocr` — in den
|
||||
Instanz-Configs stehen **Klartext-Passwörter** für SMTP, Nextcloud und SFTP.
|
||||
|
||||
Zum Schluss: `daemon-reload` und `systemctl enable --now
|
||||
pdf-ocr-hotfolder@<instanz>.service`.
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
cp irgendein-scan.pdf /var/lib/pdf-ocr-hotfolder/<instanz>/incoming/
|
||||
journalctl -u pdf-ocr-hotfolder@<instanz> -f
|
||||
```
|
||||
|
||||
Nach wenigen Sekunden liegt das OCR-PDF im `outgoing/` der Instanz.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Instanz-Betrieb
|
||||
|
||||
Das Tool arbeitet komplett instanzbasiert über die systemd-Template-Unit
|
||||
`pdf-ocr-hotfolder@<name>.service`. Jede Instanz hat eigene Config, eigene
|
||||
Datenverzeichnisse, eigene Unit, optional eigenen Service-User — und eigene
|
||||
OCR-Sprachen und Original-Behandlung.
|
||||
|
||||
```bash
|
||||
sudo ./install.sh # legt z.B. kunde-a, kunde-b, buchhaltung an
|
||||
|
||||
systemctl status 'pdf-ocr-hotfolder@*'
|
||||
journalctl -u pdf-ocr-hotfolder@kunde-a -f
|
||||
```
|
||||
|
||||
Der Code unter `/opt/pdf-ocr-hotfolder/` (inkl. venv) ist für **alle** Instanzen
|
||||
gemeinsam. Ein Update trifft damit immer alle Instanzen auf einmal — siehe
|
||||
[UPDATE.md](UPDATE.md).
|
||||
|
||||
Das vollständige Verzeichnis-Layout steht im
|
||||
[README](../README.md#verzeichnisse).
|
||||
|
||||
---
|
||||
|
||||
## LXC/Container: Error 226/NAMESPACE
|
||||
|
||||
In LXC-Containern schlagen die systemd-Hardening-Optionen der Unit
|
||||
(`PrivateTmp`, `ProtectSystem`, `ProtectKernelTunables`, …) fehl; systemd
|
||||
quittiert das mit `Error 226/NAMESPACE`.
|
||||
|
||||
Der Installer erkennt Container über `systemd-detect-virt --container` und
|
||||
bietet das Drop-in automatisch an. Manuell:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/pdf-ocr-hotfolder@.service.d/
|
||||
sudo cp /opt/pdf-ocr-hotfolder/systemd/lxc-compat.conf \
|
||||
/etc/systemd/system/pdf-ocr-hotfolder@.service.d/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart 'pdf-ocr-hotfolder@*'
|
||||
```
|
||||
|
||||
Das Drop-in setzt alle betroffenen Hardening-Schalter auf `false`. Es liegt auf
|
||||
Template-Ebene (`pdf-ocr-hotfolder@.service.d/`) und gilt damit für alle
|
||||
Instanzen. Ist es installiert, zieht `update.sh` es bei jedem Update aus dem
|
||||
Repo nach, damit ein neu ergänzter Hardening-Schalter nicht alle
|
||||
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.
|
||||
|
||||
Deshalb:
|
||||
|
||||
- `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
|
||||
[UPDATE.md](UPDATE.md#config-prüfung-per---check-config)).
|
||||
|
||||
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' | \
|
||||
sudo tee /etc/apt/sources.list.d/bookworm-backports.list
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Instanz manuell löschen
|
||||
|
||||
Der Installer legt Instanzen an, löscht aber keine. Von Hand:
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now pdf-ocr-hotfolder@<name>
|
||||
sudo rm /etc/pdf-ocr-hotfolder/<name>.toml
|
||||
sudo rm -rf /etc/systemd/system/pdf-ocr-hotfolder@<name>.service.d
|
||||
sudo systemctl daemon-reload
|
||||
# Datenverzeichnis /var/lib/pdf-ocr-hotfolder/<name> manuell aufräumen
|
||||
```
|
||||
|
||||
Das Datenverzeichnis bleibt **bewusst** liegen: dort können noch unverarbeitete
|
||||
PDFs in `incoming/`, Fehlerfälle in `error/` oder Originale im Archiv liegen.
|
||||
Erst hineinsehen, dann löschen.
|
||||
|
||||
Solange die Config unter `/etc/pdf-ocr-hotfolder/` liegt, zählt `update.sh` die
|
||||
Instanz weiter mit — auch wenn sie gestoppt ist.
|
||||
|
||||
---
|
||||
|
||||
## Konfigurationsreferenz
|
||||
|
||||
Vollständiges, kommentiertes Beispiel: [`config.example.toml`](../config.example.toml).
|
||||
Jede Instanz hat ihre eigene Kopie unter `/etc/pdf-ocr-hotfolder/<instanz>.toml`.
|
||||
|
||||
Unbekannte Keys werden beim Laden ignoriert, aber **gemeldet** — beim
|
||||
Dienststart im Log und von `--check-config`. Ein Tippfehler wie
|
||||
`[ocr].langauges` fällt damit auf.
|
||||
|
||||
### `[paths]` — Pflicht
|
||||
|
||||
| Key | Bedeutung |
|
||||
|-----|-----------|
|
||||
| `incoming` | Eingang, hier schreibt der Scanner hinein |
|
||||
| `outgoing` | Ausgang, fertige OCR-PDFs |
|
||||
| `working` | Arbeitsverzeichnis während der Verarbeitung |
|
||||
| `error` | fehlgeschlagene PDFs |
|
||||
|
||||
Fehlt die Sektion oder einer der vier Einträge, gibt es eine deutsche
|
||||
`ConfigError`-Meldung mit Datei- und Key-Nennung und **Exit 2** — der Dienst
|
||||
startet nicht.
|
||||
|
||||
### `[ocr]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
|-----|---------|-----------|
|
||||
| `languages` | `"deu+eng"` | Tesseract-Sprachen; der Installer fragt sie pro Instanz ab |
|
||||
| `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) |
|
||||
| `deskew` | `true` | schiefe Scans begradigen |
|
||||
| `clean` | `false` | Hintergrund säubern (unpaper) |
|
||||
| `max_workers` | `2` | wie viele PDFs **parallel** verarbeitet werden |
|
||||
| `timeout` | `300` | max. Sekunden, die Tesseract **pro Seite** laufen darf; `0` = kein eigenes Limit |
|
||||
|
||||
**`timeout` ist ein Seiten-Timeout, kein Gesamt-Timeout.** Der Wert geht als
|
||||
`tesseract_timeout` an ocrmypdf; ein Dokument-Timeout kennt ocrmypdf nicht. Wer
|
||||
noch den alten Default `1800` aus einer Config vor 0.4.0 stehen hat, gibt
|
||||
Tesseract 30 Minuten **je Seite** — Richtwert ist 300. Läuft eine Seite in den
|
||||
Timeout, landet sie ohne Textebene im Ergebnis, die übrigen Seiten laufen
|
||||
weiter. Ein durchgereichtes `0` würde ocrmypdf dazu bringen, OCR **still zu
|
||||
überspringen**, deshalb wird `0` (oder negativ) gar nicht erst übergeben und der
|
||||
ocrmypdf-Default greift. Siehe auch
|
||||
[Config-Drift](UPDATE.md#config-drift-nach-einem-update).
|
||||
|
||||
### `[output]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
|-----|---------|-----------|
|
||||
| `name_mode` | `"prefix"` | `prefix` → `OCR_scan.pdf`, `suffix` → `scan_OCR.pdf` (vor der Extension), `none` → unverändert |
|
||||
| `name_tag` | `"OCR_"` | verbatim eingefügter String; leer wirkt wie `none` |
|
||||
| `original_on_success` | `"delete"` | `delete` oder `archive` — Installer fragt das ab |
|
||||
| `archive_dir` | `""` | absoluter Pfad, **Pflicht** bei `archive`; Namenskollision → Zeitstempel-Suffix |
|
||||
|
||||
Ein Tippfehler in `name_mode` oder `original_on_success` führt beim Start zum
|
||||
Abbruch mit **Exit 2**, nicht erst bei der ersten Datei.
|
||||
|
||||
### `[verapdf]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
|-----|---------|-----------|
|
||||
| `enabled` | `false` | PDF/A-Validierung per veraPDF-CLI |
|
||||
| `binary` | `/opt/verapdf/verapdf` | Pfad zum veraPDF-Binary |
|
||||
| `flavour` | `"1b"` | PDF/A-Flavour |
|
||||
|
||||
veraPDF startet eine JVM und ist entsprechend teuer — nur einschalten, wenn die
|
||||
Validierung wirklich gebraucht wird. Bei FAIL wandert das OCR-Ergebnis nach
|
||||
`error/`; das Original folgt `original_on_success` (bei `archive` bleibt es also
|
||||
erhalten).
|
||||
|
||||
### `[upload.folder]` / `[upload.nextcloud]` / `[upload.sftp]`
|
||||
|
||||
Beliebig viele Ziele gleichzeitig aktivierbar. Sind alle aus, bleibt das fertige
|
||||
PDF einfach in `outgoing/` liegen.
|
||||
|
||||
| Sektion | Keys |
|
||||
|---------|------|
|
||||
| `[upload.folder]` | `enabled`, `target` — leer heißt `[paths].outgoing`, dann No-op |
|
||||
| `[upload.nextcloud]` | `enabled`, `url`, `username`, `password`, `remote_path`, `verify_ssl` |
|
||||
| `[upload.sftp]` | `enabled`, `host`, `port`, `username`, `key_file`, `password`, `remote_path` |
|
||||
|
||||
Schlägt mindestens ein Ziel fehl, zählt das als Fehler und die Mail geht als
|
||||
FEHLER raus — das PDF bleibt aber **bewusst in `outgoing/`** liegen, das OCR
|
||||
selbst war ja erfolgreich.
|
||||
|
||||
### `[notify.email]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
|-----|---------|-----------|
|
||||
| `enabled` | `false` | E-Mail-Benachrichtigung an/aus |
|
||||
| `smtp_host`, `smtp_port`, `smtp_user`, `smtp_password`, `use_starttls` | — | SMTP-Zugang |
|
||||
| `from_addr` | — | Absender |
|
||||
| `to_addrs` | `[]` | Empfängerliste |
|
||||
| `on` | `"errors"` | `always` \| `errors` \| `never` |
|
||||
|
||||
### `[logging]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
|-----|---------|-----------|
|
||||
| `level` | `"INFO"` | `DEBUG` \| `INFO` \| `WARNING` \| `ERROR` |
|
||||
|
||||
Der Dienst schreibt **kein eigenes Logfile** — alles geht nach stdout und damit
|
||||
ins journal.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tesseract findet die Sprache nicht
|
||||
|
||||
```bash
|
||||
sudo apt install tesseract-ocr-deu tesseract-ocr-eng
|
||||
```
|
||||
|
||||
Danach `[ocr].languages` prüfen. Der Installer nimmt einem das beim Anlegen
|
||||
einer Instanz ab, ein nachträglich in die Config geschriebener Sprachcode wird
|
||||
aber nicht geprüft — `--check-config` zeigt die eingestellten Sprachen an.
|
||||
|
||||
### "PriorOcrFoundError"
|
||||
|
||||
ocrmypdf erkennt bereits vorhandenen OCR-Text. `skip_text = true` in der Config
|
||||
setzen (Default).
|
||||
|
||||
### Berechtigungsprobleme bei AD-User
|
||||
|
||||
Der Service-User braucht **rw** auf alle vier Verzeichnisse der Instanz (und auf
|
||||
das Archiv, falls konfiguriert):
|
||||
|
||||
```bash
|
||||
sudo chown -R DOMAIN\\scanuser:DOMAIN\\scangroup /var/lib/pdf-ocr-hotfolder/<instanz>
|
||||
```
|
||||
|
||||
### veraPDF-Validierung schlägt immer fehl
|
||||
|
||||
`[verapdf].binary` prüfen. Wenn die Validierung nicht zwingend gebraucht wird:
|
||||
`enabled = false`.
|
||||
|
||||
### Dienst startet nicht (Exit 2)
|
||||
|
||||
Exit 2 heißt immer: Config oder Preflight. Die Ursache steht im journal und
|
||||
ausführlicher in:
|
||||
|
||||
```bash
|
||||
sudo /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \
|
||||
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
|
||||
```
|
||||
|
||||
### Dienst startet nicht (203/EXEC)
|
||||
|
||||
Der Interpreter der venv ist weg — fast immer nach einem Distributions-Upgrade.
|
||||
Siehe [OS-UPGRADE.md](OS-UPGRADE.md).
|
||||
|
||||
---
|
||||
|
||||
## Manueller Lauf (One-Shot)
|
||||
|
||||
Bestehende PDFs einer Instanz einmalig verarbeiten und beenden — greift auch
|
||||
Dateien auf, die in `working/` liegen geblieben sind:
|
||||
|
||||
```bash
|
||||
sudo -u pdfocr /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \
|
||||
--config /etc/pdf-ocr-hotfolder/kunde-a.toml --once
|
||||
```
|
||||
|
||||
Exit-Code: `0` = alles verarbeitet (auch "nichts da"), `1` = mindestens eine
|
||||
Datei fehlgeschlagen, `2` = Config- oder Preflight-Fehler.
|
||||
@@ -0,0 +1,219 @@
|
||||
# Debian-Major-Upgrade
|
||||
|
||||
Wie der Hotfolder ein Distributions-Upgrade übersteht (12 → 13, später 13 → 14).
|
||||
|
||||
Verwandte Dokumente: [README](../README.md) · [Installation](INSTALLATION.md) · [Update](UPDATE.md)
|
||||
|
||||
---
|
||||
|
||||
## Warum das ein eigener Ablauf ist
|
||||
|
||||
Die venv unter `/opt/pdf-ocr-hotfolder/venv/` hängt an der **Python-Version der
|
||||
Distribution**. Ein `apt full-upgrade` von Debian 12 auf 13 tauscht Python 3.11
|
||||
gegen 3.13 aus. Danach zeigt `venv/bin/python` auf einen Interpreter, den es so
|
||||
nicht mehr gibt — systemd quittiert den Start jeder Instanz mit `203/EXEC`, und
|
||||
auch wenn der Interpreter noch existiert, passen die installierten Pakete nicht
|
||||
mehr zum System-Python.
|
||||
|
||||
**Die venv muss nach dem Sprung neu gebaut werden.** Ein normales
|
||||
`sudo ./update.sh` genügt dafür nicht sicher genug — es gibt den ausdrücklichen
|
||||
Schalter `--rebuild-venv`.
|
||||
|
||||
---
|
||||
|
||||
## Der Ablauf
|
||||
|
||||
### 1. Vorher updaten
|
||||
|
||||
```bash
|
||||
cd /pfad/zum/repo
|
||||
git pull
|
||||
sudo ./update.sh
|
||||
```
|
||||
|
||||
Das bringt die Installation auf den aktuellen Stand und erzeugt vor allem ein
|
||||
**frisches Backup** inklusive `pip-freeze.txt` — die Liste der Paketversionen,
|
||||
die auf dem alten System liefen. Inhalt und Ort des Backups:
|
||||
[UPDATE.md](UPDATE.md#backup).
|
||||
|
||||
### 2. Instanzen stoppen
|
||||
|
||||
```bash
|
||||
sudo systemctl stop 'pdf-ocr-hotfolder@*'
|
||||
```
|
||||
|
||||
Während des Upgrades darf kein OCR laufen: Ghostscript, Tesseract und die
|
||||
Python-Pakete werden mitten im Betrieb ausgetauscht.
|
||||
|
||||
> **Geduld.** Die Unit hat `TimeoutStopSec=300`, damit ein laufendes OCR sauber
|
||||
> zu Ende kommt. **Ein Stop kann pro Instanz bis zu 5 Minuten dauern** — bei
|
||||
> mehreren Instanzen entsprechend länger. Nicht mit `kill -9` nachhelfen; ein
|
||||
> harter Stopp lässt das Original in `working/` liegen (der Dienst nimmt es beim
|
||||
> nächsten Start zwar wieder auf, aber der Durchlauf ist verloren).
|
||||
|
||||
Prüfen, dass wirklich alles steht:
|
||||
|
||||
```bash
|
||||
systemctl status 'pdf-ocr-hotfolder@*'
|
||||
```
|
||||
|
||||
### 3. Distribution upgraden
|
||||
|
||||
Der übliche Debian-Weg — Sources auf das neue Release umstellen, dann:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt full-upgrade
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
Die Instanzen sind `enabled` und starten nach dem Reboot mit; mit der alten venv
|
||||
scheitern sie (`203/EXEC`). Das ist erwartet und wird im nächsten Schritt
|
||||
behoben.
|
||||
|
||||
### 4. venv neu bauen
|
||||
|
||||
```bash
|
||||
cd /pfad/zum/repo
|
||||
git pull
|
||||
sudo ./update.sh --rebuild-venv
|
||||
```
|
||||
|
||||
`--rebuild-venv` erzwingt den Neubau. Der Rest des Updates läuft wie gewohnt
|
||||
(siehe [UPDATE.md](UPDATE.md#was-das-skript-tut--in-dieser-reihenfolge)) — die
|
||||
System-Pakete werden dabei ebenfalls abgeglichen, auch `python3-venv` für das
|
||||
neue Python.
|
||||
|
||||
Der Neubau ist **ganz oder gar nicht**:
|
||||
|
||||
1. Die alte venv wird nach `venv.old-<timestamp>` verschoben.
|
||||
2. `python3 -m venv` baut neu.
|
||||
3. Die Requirements werden installiert.
|
||||
4. **Erst bei Erfolg** wird die alte venv gelöscht.
|
||||
|
||||
### 5. Wenn ein Pin nicht mehr passt
|
||||
|
||||
Scheitert `pip install` an einer gepinnten Version, bricht das Skript ab und
|
||||
sagt genau, was los ist:
|
||||
|
||||
- die letzten 25 Zeilen der pip-Ausgabe,
|
||||
- das **gescheiterte Paket** ("Gescheitertes Paket: …"),
|
||||
- die Diagnose: "eine in requirements.txt fest gepinnte Version gibt es für
|
||||
Python \<version\> nicht (mehr)",
|
||||
- den nächsten Schritt: **requirements.txt anheben** und
|
||||
`update.sh --rebuild-venv` erneut laufen lassen.
|
||||
|
||||
**Die alte venv ist dann zurückgerollt** — es liegt keine halb gefüllte venv
|
||||
herum. Sie hängt zwar weiterhin am alten Interpreter und die Instanzen laufen
|
||||
damit nicht (das sagt das Skript auch), aber der Zustand ist eindeutig.
|
||||
|
||||
Also:
|
||||
|
||||
```bash
|
||||
# im Repo, auf einer Testmaschine
|
||||
vim requirements.txt # Version des genannten Pakets anheben
|
||||
pytest # Suite muss grün bleiben
|
||||
git commit -am 'requirements: <paket> auf <version> anheben'
|
||||
git push
|
||||
|
||||
# auf dem Zielsystem
|
||||
git pull
|
||||
sudo ./update.sh --rebuild-venv
|
||||
```
|
||||
|
||||
### 6. `--rebuild-venv` vergessen?
|
||||
|
||||
Halb so wild: `update.sh` prüft die venv auch ohne den Schalter und baut sie bei
|
||||
Versions-Drift von selbst neu. Geprüft wird
|
||||
|
||||
- ob das Verzeichnis und `venv/bin/python` überhaupt existieren,
|
||||
- ob der Interpreter der venv noch **läuft** (toter Symlink nach dem Upgrade),
|
||||
- ob seine `major.minor` zum System-`python3` passt,
|
||||
- ob `pyvenv.cfg` dieselbe Version nennt wie der Interpreter.
|
||||
|
||||
Stimmt eines davon nicht, nennt das Skript den Grund und baut neu.
|
||||
`--rebuild-venv` ist also nicht der einzige, aber der **ausdrückliche** Weg —
|
||||
und der, den man nach einem Distributions-Upgrade nimmt, statt sich auf die
|
||||
Erkennung zu verlassen.
|
||||
|
||||
---
|
||||
|
||||
## Danach prüfen
|
||||
|
||||
**Laufen alle Instanzen?**
|
||||
|
||||
```bash
|
||||
systemctl status 'pdf-ocr-hotfolder@*'
|
||||
```
|
||||
|
||||
`update.sh` hat das schon verifiziert (Wartezeit, `is-failed`, Crash-Loop) und
|
||||
in der Zusammenfassung Soll gegen Ist gestellt — ein Exit 0 heißt, dass jede
|
||||
Instanz, die vorher lief, auch wieder läuft.
|
||||
|
||||
**Sind die Configs sauber?**
|
||||
|
||||
```bash
|
||||
for f in /etc/pdf-ocr-hotfolder/*.toml; do
|
||||
sudo /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \
|
||||
--check-config --config "$f"
|
||||
done
|
||||
```
|
||||
|
||||
Exit 0/1/2 und was bei Warnungen zu tun ist:
|
||||
[UPDATE.md](UPDATE.md#config-prüfung-per---check-config).
|
||||
|
||||
**Läuft eine echte PDF durch?**
|
||||
|
||||
```bash
|
||||
cp test.pdf /var/lib/pdf-ocr-hotfolder/<instanz>/incoming/
|
||||
journalctl -u pdf-ocr-hotfolder@<instanz> -f
|
||||
```
|
||||
|
||||
Im `outgoing/` muss das OCR-PDF liegen, im Journal steht `OCR done`. Das ist der
|
||||
einzige Test, der die neue venv **und** die neuen System-Binaries (Tesseract,
|
||||
Ghostscript) wirklich anfasst — `systemctl status` sagt darüber nichts.
|
||||
|
||||
**Ghostscript-Version auf dem neuen Release ansehen:**
|
||||
|
||||
```bash
|
||||
gs --version
|
||||
```
|
||||
|
||||
Auf Debian 13 ist der Ghostscript-Bug aus Debian 12 kein Thema mehr; ein
|
||||
bookworm-backports-Eintrag unter `/etc/apt/sources.list.d/` gehört nach dem
|
||||
Upgrade entfernt. Hintergrund:
|
||||
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
|
||||
|
||||
---
|
||||
|
||||
## Pins in `requirements.txt`
|
||||
|
||||
Die Python-Abhängigkeiten sind **bewusst fest gepinnt**:
|
||||
|
||||
```
|
||||
ocrmypdf==16.13.0
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**Beim Anheben:**
|
||||
|
||||
1. **Testmaschine benutzen** — nie direkt auf dem produktiven Hotfolder.
|
||||
2. 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.
|
||||
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
|
||||
Vorgang mit eigenem Test, nicht in ein OS-Upgrade.
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
# 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)
|
||||
```
|
||||
|
||||
`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](#instanz-erfassung) |
|
||||
| 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](#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 |
|
||||
| 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 |
|
||||
|
||||
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) | 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:
|
||||
|
||||
```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@<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:
|
||||
|
||||
```bash
|
||||
/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](#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 |
|
||||
|
||||
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` 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).
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user