# 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, `.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-` 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@.service`) und zum Config-Dateinamen (`/etc/pdf-ocr-hotfolder/.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/]: ``` 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@.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-`, 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 [/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:`, 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@.service`. ### Test ```bash cp irgendein-scan.pdf /var/lib/pdf-ocr-hotfolder//incoming/ journalctl -u pdf-ocr-hotfolder@ -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@.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** — enthält Regressionen, die PDFs mit vorhandenem Text beschädigen. ocrmypdf verweigert deshalb den Dienst, statt ein kaputtes Ergebnis zu liefern. ### Wann genau ocrmypdf abbricht Die Bedingung steht in `ocrmypdf/builtin_plugins/ghostscript.py` (`check_options()`) und hängt an **zwei** Dingen — an der Config *und* an der ocrmypdf-Version: | ocrmypdf | Die Prüfung greift bei | Heißt für uns | |----------|------------------------|---------------| | **≤ 16.x** | `skip_text` oder `redo_ocr` — **unabhängig vom `output_type`** | Auch ohne PDF/A scheitert **jede** Datei, denn `skip_text = true` ist unser Default | | **≥ 17.0** | dasselbe, aber nur innerhalb von `if options.output_type.startswith('pdfa')` | Ohne PDF/A wird Ghostscript gar nicht angefasst — unkritisch | > ⚠️ **`pdfa_level = ""` allein ist damit kein Schutz.** Die Entwarnung gilt nur > zusammen mit **ocrmypdf ≥ 17**. Das war der Fehler in 0.6.0: der Pin stand auf > `ocrmypdf==16.13.0`, und auf Debian 12 landete daraufhin jede PDF in `error/` — > bei grünem `systemctl status` und „Preflight ok". > `requirements.txt` pinnt deshalb 17.x. ### Was das Tool dagegen tut - `pdfa_level = ""` ist der Default (kein PDF/A-Output). - `requirements.txt` pinnt **ocrmypdf 17.x**. Ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar; `update.sh` weist Versionssprünge der gepinnten Pakete deshalb ausdrücklich aus. - Der Preflight bricht beim **Dienststart** mit **Exit 2** ab, wenn die Ghostscript-Version betroffen ist **und** die Kombination aus `skip_text`, `pdfa_level` und installierter ocrmypdf-Version tatsächlich zum Abbruch führen würde. Der Dienst startet dann gar nicht erst, statt jede Datei einzeln scheitern zu lassen. - `--check-config` meldet denselben Zustand als **Fehler (Exit 2)** und zeigt ocrmypdf- und Ghostscript-Version an (siehe [UPDATE.md](UPDATE.md#config-prüfung-per---check-config)). - Der [Rauchtest](UPDATE.md#rauchtest) in `update.sh` schiebt nach dem Update eine Test-PDF durch die echte Pipeline — er hätte den Ausfall sofort gezeigt. ### Abhilfe **Weg 1 — Ghostscript anheben** (empfohlen). Der Installer erkennt betroffene Versionen und bietet auf Debian 12 bookworm-backports an. Manuell: ```bash echo 'deb http://deb.debian.org/debian bookworm-backports main' | \ 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. **Weg 2 — `skip_text = false` setzen.** Dann wird vorhandener Text neu erkannt statt übersprungen, und die Bedingung greift nicht mehr. Das kostet Laufzeit bei PDFs, die bereits eine Textebene haben. --- ## Instanz manuell löschen Der Installer legt Instanzen an, löscht aber keine. Von Hand: ```bash sudo systemctl disable --now pdf-ocr-hotfolder@ sudo rm /etc/pdf-ocr-hotfolder/.toml sudo rm -rf /etc/systemd/system/pdf-ocr-hotfolder@.service.d sudo systemctl daemon-reload # Datenverzeichnis /var/lib/pdf-ocr-hotfolder/ 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/.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). Achtung: leer allein schützt nur zusammen mit ocrmypdf ≥ 17 | | `deskew` | `true` | schiefe Scans begradigen | | `clean` | `false` | Hintergrund säubern (unpaper) | | `max_workers` | `2` | wie viele PDFs **parallel** verarbeitet werden | | `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/ ``` ### 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/.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.