465ff8873f
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>
994 lines
43 KiB
Markdown
994 lines
43 KiB
Markdown
# 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 |
|
||
| Arbeitsspeicher | **mindestens 2 GB** für den produktiven Betrieb — siehe [Systemanforderungen](#systemanforderungen) |
|
||
| Dateisystem | **ext4, xfs oder zfs**; `incoming/` **lokal**, kein CIFS/NFS — siehe [Dateisystem](#dateisystem-ext4-xfs-oder-zfs) |
|
||
| Rechte | `root` — als root direkt `./install.sh`, sonst `sudo ./install.sh` (s. [root oder sudo](#root-oder-sudo)) |
|
||
| `git` | zum Klonen des Repos — **nicht** vorinstalliert, s. [Pakete, die fehlen können](#pakete-die-auf-einem-frischen-system-fehlen-können) |
|
||
| 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)) |
|
||
|
||
### Pakete, die auf einem frischen System fehlen können
|
||
|
||
Auf dem **Proxmox-Debian-Standard-Template** (12 **und** 13) fehlen zwei Dinge,
|
||
die jede Anleitung stillschweigend voraussetzt — am frisch angelegten Container
|
||
verifiziert:
|
||
|
||
| Fehlt | Folge | Abhilfe |
|
||
|-------|-------|---------|
|
||
| **`git`** | `git clone …` schlägt mit `git: command not found` fehl — und ohne Clone gibt es kein Repo, aus dem `install.sh` läuft | `apt install git` |
|
||
| **`sudo`** | der überall dokumentierte Aufruf `sudo ./install.sh` schlägt mit `sudo: command not found` fehl | entweder `apt install sudo`, oder **einfach als `root` ohne `sudo` arbeiten** |
|
||
|
||
```bash
|
||
apt update
|
||
apt install -y git # zwingend
|
||
apt install -y sudo # nur, wenn nicht als root gearbeitet wird
|
||
```
|
||
|
||
### root oder sudo
|
||
|
||
Installer und Updater brauchen **root-Rechte** — *wie* man dahin kommt, ist
|
||
ihnen gleich. Beide Wege sind gleichwertig:
|
||
|
||
```bash
|
||
# (a) man ist bereits root — im frischen Container der Normalfall
|
||
./install.sh
|
||
|
||
# (b) man arbeitet als normaler Benutzer und hat sudo
|
||
sudo ./install.sh
|
||
```
|
||
|
||
In dieser Doku steht durchgehend die Variante mit `sudo`, weil die meisten
|
||
Systeme es haben. **Wer als `root` arbeitet, lässt das `sudo` bei jedem Befehl
|
||
einfach weg** — das gilt für alle Kommandos in diesem und den übrigen
|
||
Dokumenten. Ein fehlendes `sudo` ist kein Grund, es nachzuinstallieren.
|
||
|
||
Die System-Pakete installiert der Installer selbst. Die Liste steht als
|
||
einzige Quelle in `lib/common.sh` (Funktion `pdf_ocr_apt_packages()`, zwischen
|
||
den Marken `# --- BEGIN apt-packages` / `# --- END apt-packages`); `install.sh`
|
||
sourct die Datei, und `update.sh` schneidet den Block zusätzlich noch einmal
|
||
aus der **Repo**-Fassung heraus, damit beim Update die neue Liste gilt und
|
||
nicht die eventuell ältere Kopie unter `/opt/pdf-ocr-hotfolder/lib/`:
|
||
|
||
```
|
||
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](#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).
|
||
|
||
---
|
||
|
||
## Systemanforderungen
|
||
|
||
CPU und Platte sind unkritisch — **der Arbeitsspeicher ist es nicht.** OCR
|
||
rastert jede Seite in voller Auflösung ins RAM; der Spitzenbedarf hängt an der
|
||
Seitengröße, nicht an der Dateigröße der PDF.
|
||
|
||
**Empfehlung: mindestens 2 GB RAM.** Für Mehr-Instanz-Betrieb oder
|
||
`max_workers > 2` entsprechend mehr.
|
||
|
||
### Dateisystem: ext4, xfs oder zfs
|
||
|
||
Der Dienst wird **ausschließlich auf ext4, xfs oder zfs** betrieben. Andere
|
||
Dateisysteme sind nicht vorgesehen und werden nicht getestet.
|
||
|
||
**`incoming/` gehört auf ein lokales, inotify-fähiges Dateisystem — kein
|
||
CIFS/NFS-Mount.** Der Hotfolder hängt vollständig an inotify (`watchdog`
|
||
meldet `created`, `moved`, `closed`). inotify ist ein Mechanismus des *lokalen*
|
||
Kernels: er sieht nur Änderungen, die dieser Kernel selbst ausführt. Schreibt
|
||
ein anderer Rechner über SMB oder NFS in ein gemountetes Verzeichnis, geht das
|
||
am lokalen VFS vorbei und **es entsteht gar kein Event** — nicht verzögert,
|
||
nicht unzuverlässig, sondern grundsätzlich keines.
|
||
|
||
Die Folge ist heimtückisch, weil nichts kaputt aussieht: Der Dienst startet,
|
||
meldet `active (running)`, arbeitet den Bestand beim Start-Scan sauber ab — und
|
||
bemerkt danach **keine einzige neue Datei mehr**. Es gibt keinen Fehler, keine
|
||
Meldung, keine Mail. Erst wenn jemand die Instanz neu startet, wird der
|
||
inzwischen angesammelte Stapel auf einmal verarbeitet.
|
||
|
||
Richtiger Aufbau: der Scanner schreibt über SMB/NFS auf **den Rechner, auf dem
|
||
der Dienst läuft**, und `incoming/` liegt dort auf der lokalen Platte. Der
|
||
Netz-Export zeigt auf dieses lokale Verzeichnis, nicht umgekehrt. Für die
|
||
**Ausgabe** gilt die Einschränkung nicht — `outgoing/` und
|
||
`[upload.folder].target` dürfen auf einem Netz-Share liegen, dorthin wird nur
|
||
geschrieben.
|
||
|
||
### Was 2 GB tatsächlich tragen
|
||
|
||
Gemessen auf einem LXC-Container mit **2 GB RAM**, Debian 13 — eine
|
||
**A4-Seite in 300 dpi** mit `deskew = true`, `oversample = 300`, `jobs = 4`:
|
||
|
||
| Messwert | Ergebnis |
|
||
|----------|----------|
|
||
| Laufzeit | **19 s** |
|
||
| `MemoryPeak` des Dienstes (`systemctl show -p MemoryPeak`) | **380 MB** |
|
||
| `memory.peak` des ganzen Containers | **503 MB** |
|
||
| `oom_kill` in `/sys/fs/cgroup/memory.events` | **0** |
|
||
|
||
**Dieselbe Seite auf derselben Maschine mit 512 MB war genau der OOM-Kill
|
||
unten.** Der Spitzenbedarf liegt also bei rund einem halben Gigabyte für
|
||
*eine* Seite bei *einem* Worker — 2 GB lassen damit Luft für den
|
||
Default `max_workers = 2`, für das Betriebssystem und für einen zweiten
|
||
Hotfolder, sind aber keine üppige Reserve.
|
||
|
||
### Warum 512 MB nachweislich nicht reichen
|
||
|
||
Gemessen auf einem LXC-Container mit **512 MB RAM + 512 MB Swap**, Debian 13,
|
||
Ghostscript 10.05.1:
|
||
|
||
| Vorgang | Ergebnis |
|
||
|---------|----------|
|
||
| eine einzelne **A4-Seite in 300 dpi**, `deskew = true` | cgroup-Limit gerissen, Dienst vom **OOM-Killer** beendet |
|
||
| dieselbe Verarbeitung mit einer kleineren Seite (850 × 1100 px) | läuft sauber durch, ca. 19 s |
|
||
|
||
Beleg aus dem `dmesg` des LXC-Hosts:
|
||
|
||
```
|
||
oom-kill:constraint=CONSTRAINT_MEMCG, oom_memcg=/lxc/201, task=python
|
||
total-vm:1168764kB, anon-rss:489676kB
|
||
```
|
||
|
||
Eine Seite, ein Worker — und schon knapp 500 MB anonymer Speicher. 512 MB sind
|
||
damit für 300-dpi-Scans keine knappe, sondern eine unzureichende Dimensionierung.
|
||
|
||
### RAM ↔ `max_workers` × `jobs` × Auflösung
|
||
|
||
Der Spitzenbedarf multipliziert sich über drei Config-Werte aus
|
||
[`[ocr]`](#ocr):
|
||
|
||
| Key | Default | Wirkung auf den Speicher |
|
||
|-----|---------|--------------------------|
|
||
| `max_workers` | `2` | **so viele PDFs gleichzeitig** — jede mit eigenem Seitenpuffer. Der direkte Multiplikator |
|
||
| `jobs` | `4` | Threads **innerhalb** einer PDF; mehrere Seiten gleichzeitig im Speicher |
|
||
| `oversample` | `300` | Auflösung gerasterter Seiten — der Bedarf wächst quadratisch mit der dpi |
|
||
|
||
Der Default `max_workers = 2` erlaubt also, dass **zwei** solcher Seiten
|
||
parallel verarbeitet werden. Wer knapp dimensioniert, zieht zuerst
|
||
`max_workers` auf `1` herunter, danach `jobs`. `oversample` unter 300 zu
|
||
drücken, spart zwar Speicher, kostet aber Erkennungsqualität — das ist der
|
||
letzte Hebel, nicht der erste.
|
||
|
||
### Wie sich ein OOM-Kill äußert
|
||
|
||
Von außen sieht ein OOM-Kill wie ein Anwendungsfehler aus — er ist keiner:
|
||
|
||
- Der Dienst ist **weg** bzw. wurde von systemd neu gestartet
|
||
(`Restart=on-failure`); `systemctl show -p NRestarts` steigt.
|
||
- Im journal bricht die Verarbeitung **mitten in der Datei** ab, ohne
|
||
Python-Traceback und ohne `ERROR`-Zeile aus dem Tool.
|
||
- Die betroffene PDF bleibt in `working/` liegen.
|
||
|
||
### Wie man ihn nachweist
|
||
|
||
**Im Container:**
|
||
|
||
```bash
|
||
cat /sys/fs/cgroup/memory.events
|
||
# oom_kill 1 <- alles über 0 ist ein Treffer
|
||
```
|
||
|
||
**Auf dem LXC-Host** (im Container zeigt `dmesg` diese Zeilen nicht):
|
||
|
||
```bash
|
||
dmesg -T | grep -i oom-kill
|
||
```
|
||
|
||
Diese Prüfung gehört an den **Anfang** der Fehlersuche, wenn Dateien
|
||
unerklärlich in `working/` liegen bleiben: ohne sie sucht man den Fehler in
|
||
ocrmypdf, Tesseract oder der Config, wo keiner ist.
|
||
|
||
### Datenverlust ist abgefangen, der OOM bleibt
|
||
|
||
Seit **v0.6.0** greift der Dienst beim nächsten Start auf, was in `working/`
|
||
liegen geblieben ist (siehe [UPDATE.md](UPDATE.md#wiederaufnahme-aus-working)).
|
||
Eine vom OOM-Killer unterbrochene Datei geht also nicht verloren. Behoben ist
|
||
damit aber nur die Folge: bei unveränderter Dimensionierung läuft dieselbe Datei
|
||
nach dem Neustart erneut in denselben OOM — bis `max_workers` sinkt oder das
|
||
System mehr RAM bekommt.
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
apt update && apt install -y git # fehlt im Proxmox-Standard-Template
|
||
git clone https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder.git
|
||
cd pdf-ocr-hotfolder
|
||
sudo ./install.sh # als root: ./install.sh
|
||
```
|
||
|
||
#### Welche Clone-URL?
|
||
|
||
| Variante | URL | Wann |
|
||
|----------|-----|------|
|
||
| **HTTPS** (Normalfall) | `https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder.git` | braucht **keine** Credentials und funktioniert auf einem frischen Server sofort — der Weg, der im Installations-Test benutzt wurde |
|
||
| **SSH** | `gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git` | nur sinnvoll, wenn auf dem Zielsystem ein **Deploy-Key** hinterlegt ist (z.B. weil auch gepusht werden soll) |
|
||
|
||
> ⚠️ **Der SSH-User bei `gitea.sonith.de` heißt `gitea`, nicht `git`.**
|
||
> `git@gitea.sonith.de:…` ist der Default anderer Git-Hoster und hier **falsch**
|
||
> — der Clone scheitert dann mit `Permission denied (publickey)`.
|
||
|
||
`install.sh` ist **Installer und Instanz-Manager in einem** und idempotent —
|
||
jeder weitere Aufruf überspringt, was schon steht.
|
||
|
||
### So sieht ein Erstlauf aus
|
||
|
||
Verifiziert auf frischen Debian-12- und Debian-13-Containern. Die Reihenfolge
|
||
der Abfragen:
|
||
|
||
1. **LXC-Drop-in** — nur im Container: „systemd-Hardening-Drop-in für LXC
|
||
installieren?" → **ja**, sonst scheitert der Start mit `226/NAMESPACE`
|
||
([warum](#lxccontainer-error-226namespace)).
|
||
2. **Ghostscript-Hinweis** — nur auf Debian 12 mit betroffener Version. Kein
|
||
Abbruch: mit dem Default `pdfa_level = ""` ist die Installation
|
||
unproblematisch ([Hintergrund](#ghostscript-bug-auf-debian-12)).
|
||
3. **journald-Warnung** — nur, wenn `systemd-journald` nicht läuft. Typisch für
|
||
Debian 13 in LXC auf Proxmox. Hier **abbrechen**, journald reparieren und neu
|
||
anfangen — sonst hat der Dienst kein Log
|
||
([Abhilfe](#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials)).
|
||
4. Dann die **fünf Fragen pro Instanz**: Instanz-Name → Basis-Pfad →
|
||
Service-User → OCR-Sprachen → Original-Behandlung (Archiv/löschen/liegen
|
||
lassen). Im Detail: [Die Abfragen pro Instanz](#die-abfragen-pro-instanz).
|
||
5. Zum Schluss `Weitere Instanz anlegen? [j/N]:` — hier entsteht die zweite
|
||
Instanz, ohne dass irgendetwas am Basis-Install noch einmal angefasst wird.
|
||
|
||
Danach läuft `pdf-ocr-hotfolder@<instanz>.service`. Gegenprobe: eine PDF nach
|
||
`incoming/` kopieren und `journalctl -u pdf-ocr-hotfolder@<instanz> -f`
|
||
mitlesen.
|
||
|
||
### 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 (inkl. [journald-Prüfung](#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials)), Default-User `pdfocr`, Code **und `lib/`** 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, in der Reihenfolge der folgenden
|
||
Abschnitte: Instanz-Name, Basis-Pfad, Service-User, OCR-Sprachen,
|
||
Original-Behandlung. Alle Antworten gelten **nur für diese Instanz** — nichts
|
||
davon ist global.
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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** —
|
||
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
|
||
|
||
> 🚫 **Es gibt auf Debian 12 kein neueres Ghostscript.** `bookworm-backports`
|
||
> führt **kein** Ghostscript-Paket — am Paketindex geprüft
|
||
> (`bookworm-backports/main/binary-amd64`: 2606 Pakete, `ghostscript` nicht
|
||
> darunter, `systemd`/`golang-go`/`linux-image-amd64` schon). Ältere Fassungen
|
||
> dieser Anleitung und frühere Versionen des Installers haben genau das
|
||
> empfohlen — der Rat konnte nie funktionieren.
|
||
>
|
||
> **Planungsaussage:** Wer auf Debian 12 **PDF/A in der schnellen Betriebsart**
|
||
> (`skip_text = true`) braucht, hat dort **keinen Weg** — weder über Backports
|
||
> noch sonst. Es bleibt: PDF/A aufgeben, `skip_text` aufgeben (Weg 2, kostet
|
||
> Laufzeit) oder Debian 13. **Das gehört vor die Installation**, nicht in den
|
||
> Moment, in dem der Preflight abbricht.
|
||
|
||
**Weg 1 — `pdfa_level = ""` lassen** (Default, empfohlen). Ohne PDF/A-Ausgabe
|
||
fasst ocrmypdf ≥ 17 Ghostscript gar nicht an; die betroffene Version ist dann
|
||
völlig unproblematisch. Der Preis ist PDF/A — das Ergebnis ist eine normale
|
||
durchsuchbare PDF. Für die übliche Anwendung (Scan wird durchsuchbar) reicht
|
||
das.
|
||
|
||
**Weg 2 — `skip_text = false` setzen.** Dann wird vorhandener Text neu erkannt
|
||
statt übersprungen, und die Bedingung greift nicht mehr — PDF/A ist damit auch
|
||
auf Debian 12 möglich. Das kostet Laufzeit bei PDFs, die bereits eine Textebene
|
||
haben, und OCRt sie ein zweites Mal.
|
||
|
||
**Weg 3 — Distribution mit neuerem Ghostscript.** **Debian 13 liefert
|
||
Ghostscript 10.05.1** und ist vom Bug nicht betroffen; dort ist PDF/A zusammen
|
||
mit `skip_text = true` ohne Einschränkung nutzbar. Wer PDF/A verbindlich
|
||
braucht, installiert von vornherein auf Debian 13.
|
||
|
||
| Ich brauche … | Debian 12 | Debian 13 |
|
||
|---------------|-----------|-----------|
|
||
| durchsuchbare PDF, kein PDF/A | ✅ Default (`pdfa_level = ""`) | ✅ |
|
||
| PDF/A **und** `skip_text = true` | ❌ kein Weg | ✅ |
|
||
| PDF/A mit `skip_text = false` | ✅ (langsamer) | ✅ |
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
**Alle vier Pfade müssen absolut sein.** Ein relativer Pfad wird gegen das
|
||
Arbeitsverzeichnis des *Prozesses* aufgelöst, bei der Unit also gegen
|
||
`WorkingDirectory=/opt/pdf-ocr-hotfolder` — **nicht** gegen das Verzeichnis, in
|
||
dem die Config liegt. `incoming = "in"` legte damit still
|
||
`/opt/pdf-ocr-hotfolder/in` an: der Scanner schreibt woanders hin als der
|
||
Dienst schaut, und niemand sieht einen Fehler. Seit 0.7.0 ist das ein
|
||
Config-Fehler mit **Exit 2**. Dieselbe Regel gilt für
|
||
[`[output].archive_dir`](#output) und
|
||
[`[upload.folder].target`](#uploadfolder--uploadnextcloud--uploadsftp);
|
||
`install.sh` erzeugt ohnehin nur absolute Pfade.
|
||
|
||
`incoming` muss außerdem auf einem **lokalen** Dateisystem liegen — siehe
|
||
[Dateisystem](#dateisystem-ext4-xfs-oder-zfs).
|
||
|
||
### `[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 (relativ wird abgelehnt), **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.
|
||
|
||
**Kollisionen überschreiben nichts.** Liegt im Ziel bereits eine Datei
|
||
desselben Namens, wird die neue mit Zeitstempel danebengelegt
|
||
(`scan.pdf` → `scan_20260923-081500.pdf`; bei zwei Dateien innerhalb derselben
|
||
Sekunde zusätzlich mit Zähler), und es gibt eine Warnung im Journal. Das gilt
|
||
seit 0.7.0 einheitlich für **`outgoing/`, das Archiv, `error/` und den
|
||
Ordner-Upload** — vorher ersetzte der zweite Durchlauf das Ergebnis des ersten
|
||
kommentarlos. Die E-Mail-Benachrichtigung und die Upload-Ziele nennen den
|
||
tatsächlich geschriebenen Namen.
|
||
|
||
**Scheitert das Entsorgen des Originals** (Platte voll, Verzeichnis
|
||
read-only), gilt der Durchlauf trotzdem als Erfolg: das fertige PDF liegt
|
||
bereits in `outgoing/` und wird normal ausgeliefert. Es gibt aber eine
|
||
`ERROR`-Zeile im Journal, und die Mail geht als **„OK mit Warnung"** raus —
|
||
auch bei `[notify.email].on = "errors"`. Das Original bleibt dann in
|
||
`working/` liegen und wird beim nächsten Start **erneut** durch das OCR
|
||
geschickt; es gehört von Hand aufgeräumt und die Ursache behoben.
|
||
|
||
### `[verapdf]`
|
||
|
||
| Key | Default | Bedeutung |
|
||
|-----|---------|-----------|
|
||
| `enabled` | `false` | PDF/A-Validierung per veraPDF-CLI |
|
||
| `binary` | `/opt/verapdf/verapdf` | Pfad zum veraPDF-Binary, oder ein nackter Name, der im `PATH` gesucht wird |
|
||
| `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).
|
||
|
||
**Mit `enabled = true` wird `binary` im Preflight geprüft** (seit 0.7.0). Zeigt
|
||
der Pfad nicht auf ein vorhandenes, ausführbares Programm, startet der Dienst
|
||
gar nicht erst (**Exit 2**), und `--check-config` meldet denselben Fehler.
|
||
`--check-config` zeigt Binary und Flavour außerdem in der Übersicht an.
|
||
|
||
> ⚠️ **Warum das eine harte Sperre ist.** Bis 0.6.3 war ein Tippfehler in
|
||
> `binary` der gefährlichste Fehler des ganzen Dienstes: `run_verapdf()` fand
|
||
> das Programm für **jede** Datei nicht, wertete das als „nicht konform",
|
||
> schob das OCR-Ergebnis nach `error/` — und entsorgte das Original laut
|
||
> `original_on_success`, beim Default `delete` also die Vorlage. Scan für Scan
|
||
> verschwanden so die Originale, während die Unit als `active (running)`
|
||
> dastand.
|
||
|
||
**Störung ist kein FAIL.** Lässt sich veraPDF im laufenden Betrieb nicht mehr
|
||
befragen — Programm verschwunden, JVM startet nicht, Timeout (300 s pro Datei),
|
||
oder die Ausgabe enthält weder `PASS` noch `FAIL` —, ist das **kein Urteil über
|
||
die PDF**. In diesem Fall wandern **Original und OCR-Ergebnis** nach `error/`,
|
||
und das Original wird **weder gelöscht noch archiviert**, unabhängig von
|
||
`original_on_success`. Im Journal steht die Ursache samt Hinweis auf
|
||
`--check-config`.
|
||
|
||
### `[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; sonst **absoluter** Pfad (relativ wird abgelehnt, Exit 2) |
|
||
| `[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.
|
||
|
||
Liegt im `target` von `[upload.folder]` schon eine gleichnamige Datei, wird sie
|
||
seit 0.7.0 **nicht mehr ersetzt**, sondern die Kopie mit Zeitstempel
|
||
danebengelegt (samt Warnung) — dieselbe Regel wie in [`[output]`](#output).
|
||
|
||
### `[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.
|
||
|
||
---
|
||
|
||
## Exit-Codes
|
||
|
||
Der Dienst und die CLI benutzen vier Codes. `systemctl status` und
|
||
`journalctl` zeigen sie als `status=<n>`.
|
||
|
||
| Exit | Bedeutung | Startet systemd neu? | Was zu tun ist |
|
||
|------|-----------|----------------------|----------------|
|
||
| **0** | regulärer Stopp (SIGTERM/SIGINT); bei `--once`: alles verarbeitet, auch „nichts da"; bei `--check-config`: Config sauber | — | nichts |
|
||
| **1** | nur im Einmal-Betrieb: mindestens eine PDF ist fehlgeschlagen. Bei `--check-config`: Config nutzbar, aber mit **Warnungen** | — | `error/` ansehen bzw. Warnungen nachziehen |
|
||
| **2** | **Config- oder Preflight-Fehler** — kaputtes/unlesbares TOML, fehlender Pflicht-Key, relativer Pfad, ungültige `[output]`-Werte, fehlendes `tesseract`/`gs`, betroffene Ghostscript-Version, nicht aufrufbares veraPDF | **nein** — `RestartPreventExitStatus=2` in der Unit | Config korrigieren, mit `--check-config` gegenprüfen, dann `systemctl start` |
|
||
| **3** | **Der Verzeichnis-Watch ist gestorben** — es würden keine neuen Dateien mehr erkannt | **ja**, und genau darum geht es | meist nichts; häuft es sich, `fs.inotify.max_user_watches` und den Mount von `incoming/` prüfen |
|
||
|
||
**Zu Exit 2:** Ein Neustart heilt einen Config-Fehler nicht. Ohne
|
||
`RestartPreventExitStatus=2` startete `Restart=on-failure` die Instanz endlos
|
||
im 5-Sekunden-Takt neu (das Start-Rate-Limit greift bei `RestartSec=5` nie).
|
||
Seit 0.7.0 bleibt sie stattdessen sichtbar `failed` stehen — das ist gewollt
|
||
und soll beim Nachsehen auffallen.
|
||
|
||
**Zu Exit 3:** Stirbt der watchdog-Observer im Betrieb (erschöpftes
|
||
`fs.inotify.max_user_watches`, ersetztes oder neu gemountetes Verzeichnis),
|
||
blieb die Unit früher `active (running)` und verarbeitete stumm nichts mehr —
|
||
für einen Hotfolder der schlechteste denkbare Zustand. Der Dienst prüft den
|
||
Observer jetzt sekündlich mit, loggt eine `ERROR`-Zeile mit den möglichen
|
||
Ursachen und beendet sich mit 3, damit systemd ihn neu startet und der Watch
|
||
neu aufgesetzt wird. Ein einzelnes Vorkommnis ist damit selbstheilend; ein
|
||
steigendes `systemctl show -p NRestarts` ist der Hinweis, dass man nachsehen
|
||
sollte.
|
||
|
||
---
|
||
|
||
## 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: Dienst startet nicht / Dateien landen in `error/`
|
||
|
||
Seit 0.7.0 prüft der Preflight `[verapdf].binary`. Startet die Instanz mit
|
||
**Exit 2** nicht mehr, nachdem vorher „alles lief", ist das die gute Nachricht:
|
||
der Pfad war schon vorher falsch, nur hat es bisher niemand gemerkt. Vorher
|
||
wurde jede PDF als ungültig gewertet und das Original laut
|
||
`original_on_success` entsorgt.
|
||
|
||
```bash
|
||
ls -l /opt/verapdf/verapdf # vorhanden? ausführbar (chmod +x)?
|
||
```
|
||
|
||
Korrigieren — oder, wenn die Validierung nicht zwingend gebraucht wird,
|
||
`[verapdf].enabled = false` setzen. Landen **Original und OCR-Ergebnis
|
||
gemeinsam** in `error/`, war veraPDF im laufenden Betrieb nicht mehr
|
||
ansprechbar; das Original ist dann unangetastet, siehe
|
||
[`[verapdf]`](#verapdf).
|
||
|
||
### Dienst startet nicht (Exit 2)
|
||
|
||
Exit 2 heißt immer: Config oder Preflight — siehe [Exit-Codes](#exit-codes).
|
||
Die Instanz bleibt bewusst `failed` stehen und wird **nicht** neu gestartet.
|
||
Die Ursache steht im journal und ausführlicher in:
|
||
|
||
```bash
|
||
cd /opt/pdf-ocr-hotfolder && sudo ./venv/bin/python -m pdf_ocr_hotfolder \
|
||
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
|
||
```
|
||
|
||
Typische Fälle: kaputtes TOML (die Meldung nennt Zeile und Spalte, sofern der
|
||
Interpreter sie liefert), ein relativer Pfad in `[paths]`,
|
||
`[output].archive_dir` oder `[upload.folder].target`, ein leeres `archive_dir`
|
||
bei `original_on_success = "archive"`, ein nicht aufrufbares veraPDF oder eine
|
||
betroffene Ghostscript-Version.
|
||
|
||
### Dienst läuft, verarbeitet aber nichts mehr
|
||
|
||
`systemctl status` sagt `active (running)`, in `incoming/` stapeln sich die
|
||
PDFs, im Journal passiert nichts. Drei Ursachen, in dieser Reihenfolge prüfen:
|
||
|
||
1. **`incoming/` liegt auf einem CIFS/NFS-Mount.** Dann liefert inotify
|
||
grundsätzlich keine Events — der Dienst verarbeitet nur noch beim Start.
|
||
`findmnt -T /var/lib/pdf-ocr-hotfolder/<instanz>/incoming` zeigt den Typ;
|
||
Hintergrund und richtiger Aufbau unter
|
||
[Dateisystem](#dateisystem-ext4-xfs-oder-zfs).
|
||
2. **Der Verzeichnis-Watch ist gestorben.** Seit 0.7.0 fällt das auf: der
|
||
Dienst beendet sich mit [Exit 3](#exit-codes) und systemd startet ihn neu.
|
||
Im Journal steht die `ERROR`-Zeile, `systemctl show -p NRestarts` steigt.
|
||
Häuft sich das, ist meist das inotify-Limit erschöpft:
|
||
```bash
|
||
cat /proc/sys/fs/inotify/max_user_watches
|
||
```
|
||
3. **Es sind gar keine PDFs.** Dateien ohne `.pdf`-Endung werden ignoriert.
|
||
Beim Start-Scan meldet der Dienst sie seit 0.7.0 als Sammelzeile mit Anzahl
|
||
und bis zu drei Beispielnamen:
|
||
```bash
|
||
journalctl -u pdf-ocr-hotfolder@<instanz> | grep 'ohne .pdf-Endung'
|
||
```
|
||
|
||
### Dienst startet nicht (203/EXEC)
|
||
|
||
Der Interpreter der venv ist weg — fast immer nach einem Distributions-Upgrade.
|
||
Siehe [OS-UPGRADE.md](OS-UPGRADE.md).
|
||
|
||
### Keine Logs: "No journal files were found"
|
||
|
||
**Symptom:** `journalctl -u pdf-ocr-hotfolder@<instanz>` bleibt leer oder meldet
|
||
`No journal files were found.` — auch dann, wenn der Dienst nachweislich läuft
|
||
und Dateien verarbeitet.
|
||
|
||
**Erste Prüfung:**
|
||
|
||
```bash
|
||
systemctl status systemd-journald
|
||
```
|
||
|
||
Ist `systemd-journald.service` selbst `failed` (beobachtet mit
|
||
`status=243/CREDENTIALS`), gibt es schlicht kein journal, in das geschrieben
|
||
werden könnte.
|
||
|
||
Läuft journald dagegen `active (running)` und das Journal der Instanz ist
|
||
trotzdem leer (`-- No entries --`), ist meist journald **erst nach dem Dienst**
|
||
wieder hochgekommen: die Startmeldungen hatten in der Zwischenzeit kein Ziel und
|
||
sind weg. `sudo systemctl restart 'pdf-ocr-hotfolder@*'` schreibt sie neu —
|
||
siehe die Warnung im
|
||
[nächsten Abschnitt](#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials).
|
||
|
||
**Einordnung.** Der Dienst loggt seit v0.4.1 **ausschließlich** nach journald —
|
||
es gibt bewusst kein eigenes Logfile und kein Logverzeichnis. Ein kaputtes
|
||
journald ist damit ein blinder Fleck: jede Fehlersuche läuft ins Leere, und der
|
||
Ausfall sieht aus wie ein stummer Dienst. **Deshalb vor jeder Fehlersuche
|
||
zuerst journald prüfen**, nicht erst, wenn nichts anderes mehr passt.
|
||
|
||
Die häufigste Ursache ist **kein** Schaden an dieser einen Maschine, sondern
|
||
systematisch: **Debian 13 in einer LXC auf Proxmox** — siehe den nächsten
|
||
Abschnitt. Auf Debian 12 tritt sie nicht auf.
|
||
|
||
`install.sh` warnt beim Erstinstall in einem Container von sich aus, wenn
|
||
`systemd-journald` nicht läuft, nennt den Drop-in-Befehl und fragt, ob
|
||
fortgefahren werden soll.
|
||
|
||
**Notbehelf, solange journald nicht zu retten ist:** die Instanz einmal im
|
||
Vordergrund laufen lassen — dann geht die Ausgabe direkt ins Terminal, am
|
||
journal vorbei.
|
||
|
||
```bash
|
||
sudo systemctl stop pdf-ocr-hotfolder@<instanz>
|
||
cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./venv/bin/python -m pdf_ocr_hotfolder \
|
||
--config /etc/pdf-ocr-hotfolder/<instanz>.toml
|
||
```
|
||
|
||
Das `cd` ist zwingend: das Paket wird nicht pip-installiert, sondern nach
|
||
`/opt/pdf-ocr-hotfolder` kopiert und nur über das Arbeitsverzeichnis gefunden
|
||
(ohne `cd` gibt es `No module named pdf_ocr_hotfolder`, v0.6.2). Beenden mit
|
||
`Strg+C`, danach `sudo systemctl start pdf-ocr-hotfolder@<instanz>`. Wer nur den
|
||
Bestand abarbeiten und dann aussteigen will, hängt `--once` an (siehe
|
||
[Manueller Lauf](#manueller-lauf-one-shot)).
|
||
|
||
### Debian 13 in LXC auf Proxmox: journald scheitert (243/CREDENTIALS)
|
||
|
||
Verifizierter Befund. Er betrifft **jede** Debian-13-LXC auf Proxmox 8.4, nicht
|
||
nur eine einzelne Maschine — und er trifft nicht nur dieses Tool, sondern
|
||
alles, was auf dem Container-journal aufsetzt.
|
||
|
||
**Symptom im Container:**
|
||
|
||
```bash
|
||
systemctl status systemd-journald
|
||
# ● systemd-journald.service - Journal Service
|
||
# Active: failed
|
||
# Process: ... (code=exited, status=243/CREDENTIALS)
|
||
# Main PID exited, status=243/CREDENTIALS
|
||
|
||
journalctl -u pdf-ocr-hotfolder@<instanz>
|
||
# No journal files were found.
|
||
```
|
||
|
||
**Ursache.** Ab systemd 255 (Debian 13 liefert **257**) setzt die
|
||
journald-Unit `ImportCredential=journal.*`. Zum Einlesen dieser Credentials
|
||
startet systemd den Hilfsprozess `(sd-mkdcreds)`, und der **mountet** dafür.
|
||
Genau diesen Mount verbietet das AppArmor-Profil des Proxmox-Hosts. Im Log des
|
||
**Hosts** steht dazu:
|
||
|
||
```
|
||
apparmor="DENIED" operation="mount" profile="lxc-<id>_</var/lib/lxc>" name="/dev/" comm="(sd-mkdcreds)"
|
||
```
|
||
|
||
Debian 12 hat systemd 252, kennt `ImportCredential` in dieser Unit nicht und
|
||
ist deshalb **nicht** betroffen. Das Upgrade 12 → 13 ist damit der Auslöser,
|
||
nicht der Container an sich.
|
||
|
||
**Dieselbe Ursache legt weitere Units lahm** — beobachtet bei
|
||
`systemd-logind`, `systemd-networkd`, `console-getty` und
|
||
`systemd-tmpfiles-setup`. Wer nur journald repariert, hat die übrigen noch vor
|
||
sich; ein Blick auf `systemctl --failed` lohnt sich.
|
||
|
||
**Abhilfe im Container** (reboot-fest verifiziert) — `ImportCredential` wird
|
||
per Drop-in auf leer gesetzt und damit abgeschaltet:
|
||
|
||
```bash
|
||
sudo mkdir -p /etc/systemd/system/systemd-journald.service.d
|
||
printf '[Service]\nImportCredential=\n' | \
|
||
sudo tee /etc/systemd/system/systemd-journald.service.d/no-credentials.conf
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl restart systemd-journald
|
||
sudo journalctl --flush
|
||
```
|
||
|
||
Danach `systemctl status systemd-journald` gegenprüfen — muss `active
|
||
(running)` sein. Für die anderen betroffenen Units gilt dasselbe Muster mit
|
||
deren Unit-Namen.
|
||
|
||
> ⚠️ **Jetzt die Instanzen einmal neu starten — sonst steht man vor einem
|
||
> leeren Journal.** Wurde der Dienst gestartet, **während** journald tot war,
|
||
> sind seine Startmeldungen unwiederbringlich weg: sie hatten kein Ziel. Nach
|
||
> der Reparatur meldet `journalctl -u pdf-ocr-hotfolder@<instanz>` dann
|
||
> `-- No entries --` — und das sieht exakt so aus wie ein gescheiterter
|
||
> Reparaturversuch, obwohl journald längst wieder läuft. Der Neustart schreibt
|
||
> die Startmeldungen neu ins frische Journal:
|
||
>
|
||
> ```bash
|
||
> sudo systemctl restart 'pdf-ocr-hotfolder@*'
|
||
> journalctl -u pdf-ocr-hotfolder@<instanz> -n 20
|
||
> ```
|
||
>
|
||
> Erst wenn hier die Startmeldungen stehen, ist die Reparatur belegt. (`restart`
|
||
> kann das Glob, weil die Units geladen sind — `start` nicht, siehe
|
||
> [UPDATE.md](UPDATE.md#rollback).)
|
||
|
||
> **Der saubere Weg liegt host-seitig.** Das Drop-in kuriert das Symptom im
|
||
> Container. Richtig behoben wird es auf dem Proxmox-Host: Update von
|
||
> `pve-container`/`lxc-pve` auf eine Fassung mit passenden AppArmor-Regeln —
|
||
> oder, als grobes Mittel, `lxc.apparmor.profile: unconfined` in der
|
||
> Container-Config, was die AppArmor-Isolation dieses Containers allerdings
|
||
> komplett aufgibt.
|
||
|
||
### Dienst bricht mitten in der Verarbeitung weg
|
||
|
||
Datei bleibt in `working/`, kein Traceback, `NRestarts` steigt: das ist fast
|
||
immer der OOM-Killer, kein Anwendungsfehler. Nachweis und Dimensionierung unter
|
||
[Systemanforderungen](#wie-sich-ein-oom-kill-äußert).
|
||
|
||
---
|
||
|
||
## Manueller Lauf (One-Shot)
|
||
|
||
Bestehende PDFs einer Instanz einmalig verarbeiten und beenden — greift auch
|
||
Dateien auf, die in `working/` liegen geblieben sind:
|
||
|
||
```bash
|
||
cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./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. Exit 3 gibt es hier
|
||
nicht — der gehört zum Dauerbetrieb, siehe [Exit-Codes](#exit-codes).
|