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:
2026-09-22 22:04:04 +02:00
parent 2062476252
commit 3e24aa2ecd
20 changed files with 3095 additions and 347 deletions
+67 -190
View File
@@ -2,40 +2,43 @@
Verwandelt eingehende gescannte PDFs automatisch in **durchsuchbare PDFs** (PDF/A optional) per OCR. Hauptanwendung: Kunden-Scanner schiebt PDF in einen Ordner — Sekunden später liegt die OCR-Version im Ausgang oder wird in Nextcloud / per SFTP weitergeleitet.
## Dokumentation
| Dokument | Inhalt |
|----------|--------|
| **[docs/INSTALLATION.md](docs/INSTALLATION.md)** | Erstinstallation, Instanzen anlegen, LXC, Ghostscript, **Konfigurationsreferenz**, Troubleshooting |
| **[docs/UPDATE.md](docs/UPDATE.md)** | Update mit `update.sh`: Ablauf, Backup, Rollback, `--check-config`, Config-Drift |
| **[docs/OS-UPGRADE.md](docs/OS-UPGRADE.md)** | Debian-Major-Upgrade (12 → 13): venv neu bauen, Pins anheben |
Weiter: [CHANGELOG.md](CHANGELOG.md) · [AI_AGENT_BRIEFING.md](AI_AGENT_BRIEFING.md) · [config.example.toml](config.example.toml)
## Features
- 🔍 **OCR via ocrmypdf + Tesseract** (Library-Call, kein Subprozess-Overhead)
- 📂 **Hotfolder via watchdog** — reagiert auf `created`, `moved`, `closed` Events
- 🧠 **Stabilitäts-Erkennung**: wartet bis Scanner fertig geschrieben hat
- 🔁 **Parallelverarbeitung** mehrerer PDFs (ThreadPool, konfigurierbar)
- ♻️ **Wiederaufnahme aus `working/`** nach einem harten Stopp — keine Datei bleibt liegen
- ✅ **PDF/A-Output** (1, 2 oder 3) optional
- 🛡️ **veraPDF-Validierung** optional
- ☁️ **Upload-Ziele**: lokaler Ordner, Nextcloud (WebDAV via Python), SFTP
- 📧 **E-Mail-Notify** (immer / nur Fehler / nie)
- 🔐 **Service-User-Support** für lokale **und AD-User mit lokaler UID** (SSSD/Winbind)
- ⚙️ Saubere systemd-Integration mit auto-Restart
- ⚙️ Saubere systemd-Integration mit auto-Restart, **Multi-Instanz** über eine Template-Unit
- 🩺 **`--check-config`** prüft eine Instanz-Config ohne etwas zu verarbeiten
## Schnellstart
```bash
git clone https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder.git
git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
cd pdf-ocr-hotfolder
sudo ./install.sh
```
Der Installer:
1. Installiert einmalig Code + venv + systemd-Template-Unit
2. Fragt **pro Instanz** ab:
- Instanz-Name
- Basis-Pfad für die Daten
- Service-User
- **OCR-Sprachen** (Tesseract, Default `deu+eng`) — fehlende Sprachpakete
(`tesseract-ocr-<code>`) werden erkannt und auf Wunsch nachinstalliert
- **Original nach erfolgreichem OCR archivieren?** (Default nein = löschen;
bei ja zusätzlich der Archiv-Pfad, vorgeschlagen `<basis>/archive`)
3. Legt so viele Hotfolder-Instanzen an, wie du willst (`Weitere Instanz anlegen? [j/N]`)
Bei jedem erneuten Aufruf erkennt der Installer bestehende Instanzen und fragt nur nach neuen.
Der Installer legt einmalig Code, venv und die systemd-Template-Unit an und
fragt danach **pro Instanz** Name, Basis-Pfad, Service-User, OCR-Sprachen und
die Original-Behandlung ab. Bei jedem erneuten Aufruf erkennt er bestehende
Instanzen und fragt nur nach neuen.
Test:
@@ -46,126 +49,56 @@ journalctl -u pdf-ocr-hotfolder@<instanz> -f
Nach wenigen Sekunden liegt das OCR-PDF im `outgoing/`-Ordner der Instanz.
## Multi-Instanz-Betrieb
Alle Details zu den Abfragen, zum Multi-Instanz-Betrieb und zu den Fallstricken
(LXC, Ghostscript): **[docs/INSTALLATION.md](docs/INSTALLATION.md)**.
Das Tool arbeitet komplett **instanzbasiert** über eine systemd Template-Unit `pdf-ocr-hotfolder@<name>.service`. Jede Instanz hat:
- eigene Config-Datei: `/etc/pdf-ocr-hotfolder/<name>.toml`
- eigene Datenverzeichnisse: `/var/lib/pdf-ocr-hotfolder/<name>/{incoming,working,outgoing,error}/`
- eigene systemd-Unit: `pdf-ocr-hotfolder@<name>.service`
- optional eigenen Service-User (via Drop-in `/etc/systemd/system/pdf-ocr-hotfolder@<name>.service.d/user.conf`)
- **eigene OCR-Sprachen und eigene Original-Behandlung** (löschen oder archivieren)
### Sprachen pro Instanz
Die Tesseract-Sprachen werden bewusst **je Instanz** abgefragt, nicht global:
Hotfolder haben unterschiedliche Post. Ein Buchhaltungs-Hotfolder sieht nur
deutsche Belege, ein Export-Hotfolder 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 muss mehr Modelle gegeneinander
abwägen und verwechselt dabei Wörter, die in der einen Sprache eindeutig wären.
`deu+eng+fra` auf reinen Deutsch-Scans ist also kein Sicherheitsnetz, sondern
ein Rückschritt.
Beispiel für 3 Hotfolder:
```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
```
Manuell eine weitere Instanz anlegen geht auch — einfach `install.sh` erneut starten, er fragt wieder nach.
Update: `git pull && sudo ./update.sh` — siehe **[docs/UPDATE.md](docs/UPDATE.md)**.
Nach einem Debian-Major-Upgrade: **[docs/OS-UPGRADE.md](docs/OS-UPGRADE.md)**.
## Verzeichnisse
| Pfad | Zweck |
|------|-------|
| `/opt/pdf-ocr-hotfolder/` | Code + venv (für alle Instanzen gemeinsam) |
| `/etc/pdf-ocr-hotfolder/<instanz>.toml` | Config pro Instanz |
| `/etc/pdf-ocr-hotfolder/<instanz>.toml` | Config pro Instanz (640, root:\<service-gruppe\>) |
| `/etc/systemd/system/pdf-ocr-hotfolder@.service` | systemd Template-Unit |
| `/var/lib/pdf-ocr-hotfolder/<instanz>/incoming` | Eingang (Scanner schreibt hier rein) |
| `/var/lib/pdf-ocr-hotfolder/<instanz>/working` | Arbeitsverzeichnis während OCR |
| `/var/lib/pdf-ocr-hotfolder/<instanz>/outgoing` | Ausgang (fertige PDFs) |
| `/var/lib/pdf-ocr-hotfolder/<instanz>/error` | Fehlgeschlagene PDFs |
| `/var/backups/pdf-ocr-hotfolder/` | Update-Backups |
| `/var/backups/pdf-ocr-hotfolder/` | Update-Backups (0600, letzte 5) |
## Konfiguration
Ein eigenes Logverzeichnis gibt es nicht — der Dienst loggt nach stdout und
damit ins journal.
Vollständiges Beispiel: [`config.example.toml`](config.example.toml). Wichtigste Sektionen:
## Konfiguration im Überblick
Der Installer fragt `[ocr].languages`, `[output].original_on_success` und
`[output].archive_dir` pro Instanz ab und schreibt sie direkt in die
Instanz-Config — die Werte unten sind nur die Beispiel-Defaults.
Jede Instanz hat ihre eigene TOML unter `/etc/pdf-ocr-hotfolder/<instanz>.toml`.
Vollständiges, kommentiertes Beispiel: [`config.example.toml`](config.example.toml).
Key-für-Key-Referenz: [docs/INSTALLATION.md](docs/INSTALLATION.md#konfigurationsreferenz).
### `[ocr]`
```toml
languages = "deu+eng" # Tesseract-Sprachen (Installer fragt pro Instanz)
jobs = 4 # Threads pro PDF
skip_text = true # bereits OCR-haltige Seiten überspringen
pdfa_level = "" # "1", "2", "3" oder "" für reines PDF (Default "" wegen Ghostscript-Bug, s.u.)
deskew = true
max_workers = 2 # parallele PDFs
timeout = 300 # max. Sekunden pro SEITE (Tesseract), 0 = ocrmypdf-Default
| Sektion | Zweck |
|---------|-------|
| `[paths]` | `incoming`, `outgoing`, `working`, `error` — **Pflicht** |
| `[ocr]` | Sprachen, `jobs`, `skip_text`, `pdfa_level`, `deskew`, `max_workers`, `timeout` (Sekunden **pro Seite**) |
| `[output]` | Dateibenennung (`name_mode`/`name_tag`) und Original-Behandlung (`delete`/`archive`) |
| `[verapdf]` | optionale PDF/A-Validierung per CLI |
| `[upload.folder]` / `[upload.nextcloud]` / `[upload.sftp]` | Upload-Ziele, beliebig viele gleichzeitig |
| `[notify.email]` | SMTP-Benachrichtigung: `always` \| `errors` \| `never` |
| `[logging]` | `level` = DEBUG/INFO/WARNING/ERROR |
Die Instanz-Configs enthalten **Klartext-Passwörter** (SMTP, Nextcloud, SFTP) —
deshalb `640 root:<service-gruppe>` und beim Debuggen nicht in Tickets kopieren.
Config prüfen, ohne etwas zu verarbeiten:
```bash
sudo /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
```
### `[output]`
```toml
# Dateiname im outgoing/:
# "prefix" → OCR_scan.pdf
# "suffix" → scan_OCR.pdf (vor der Extension)
# "none" → scan.pdf (unverändert)
name_mode = "prefix"
name_tag = "OCR_"
# Nach erfolgreichem OCR mit dem Original:
# "delete" → löschen
# "archive" → in archive_dir verschieben
# Beides fragt der Installer beim Anlegen der Instanz ab:
original_on_success = "delete"
archive_dir = "" # absoluter Pfad, Pflicht bei "archive"
```
### `[upload.nextcloud]`
```toml
enabled = true
url = "https://cloud.example.com"
username = "scanuser"
password = "app-password"
remote_path = "Scans/Inbox"
```
### `[upload.sftp]`
```toml
enabled = true
host = "sftp.example.com"
username = "scanuser"
key_file = "/etc/pdf-ocr-hotfolder/sftp_key"
remote_path = "/uploads"
```
### `[notify.email]`
```toml
enabled = true
smtp_host = "smtp.example.com"
smtp_port = 587
smtp_user = "alerts@example.com"
smtp_password = "secret"
from_addr = "PDF OCR <alerts@example.com>"
to_addrs = ["admin@example.com"]
on = "errors" # always | errors | never
```
Exit 0 = sauber, 1 = Warnungen, 2 = Fehler. Details:
[docs/UPDATE.md](docs/UPDATE.md#config-prüfung-per---check-config).
## Service-Verwaltung
@@ -178,80 +111,11 @@ journalctl -u pdf-ocr-hotfolder@kunde-a -f
# Alle Instanzen
sudo systemctl status 'pdf-ocr-hotfolder@*'
sudo systemctl restart 'pdf-ocr-hotfolder@*'
journalctl -u 'pdf-ocr-hotfolder@*' --since today
```
### Logs
Der Dienst schreibt **kein eigenes Logfile** — alles geht nach stdout und damit
ins journal:
```bash
journalctl -u pdf-ocr-hotfolder@<instanz> -f # eine Instanz mitlesen
journalctl -u 'pdf-ocr-hotfolder@*' --since today # alle Instanzen, heute
```
## Update
```bash
cd /pfad/zum/repo
git pull
sudo ./update.sh
```
`update.sh`:
1. Stoppt alle laufenden Instanzen
2. Sichert den alten Code nach `/var/backups/pdf-ocr-hotfolder/`
3. Aktualisiert Code + venv + systemd-Template-Unit in `/opt/pdf-ocr-hotfolder/`
4. Startet alle zuvor laufenden Instanzen neu
Config-Dateien unter `/etc/pdf-ocr-hotfolder/` werden **nie** überschrieben.
Das Repo muss bestehen bleiben — `update.sh` kopiert daraus.
## Manueller Lauf (One-Shot)
Bestehende PDFs einer Instanz einmalig verarbeiten und beenden:
```bash
sudo -u pdfocr /opt/pdf-ocr-hotfolder/venv/bin/python -m pdf_ocr_hotfolder \
--config /etc/pdf-ocr-hotfolder/kunde-a.toml --once
```
## Troubleshooting
### Tesseract findet die Sprache nicht
```bash
sudo apt install tesseract-ocr-deu tesseract-ocr-eng
```
### "PriorOcrFoundError"
ocrmypdf erkennt bereits vorhandenen OCR-Text. `skip_text = true` in der Config setzen.
### Berechtigungsprobleme bei AD-User
Service-User braucht **rw** auf alle vier Verzeichnisse unter `/var/lib/pdf-ocr-hotfolder/`. Bei AD-User mit lokaler UID:
```bash
sudo chown -R DOMAIN\\scanuser:DOMAIN\\scangroup /var/lib/pdf-ocr-hotfolder
```
### LXC/Container: Error 226/NAMESPACE
In LXC-Containern schlagen systemd-Hardening-Optionen fehl. Der Installer erkennt Container automatisch und bietet ein Drop-in 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@*'
```
### Ghostscript PDF/A-Bug auf Debian 12
GS 10.00.0–10.02.0 (Debian 12 Default) zerstört OCR bei `pdfa_level` + `skip_text=true`. Der Installer bietet automatisch 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
```
### veraPDF-Validierung schlägt immer fehl
veraPDF binary prüfen (`[verapdf].binary`). Wenn nicht zwingend gebraucht: `enabled = false`.
Ein laufendes OCR darf beim Stoppen zu Ende laufen (`TimeoutStopSec=300`) — ein
`stop` kann deshalb pro Instanz bis zu 5 Minuten dauern.
## Architektur
@@ -275,11 +139,24 @@ veraPDF binary prüfen (`[verapdf].binary`). Wenn nicht zwingend gebraucht: `ena
└────────────┘ └────────────┘ └────────────┘
```
Beim Start wird `working/` zuerst durchsucht: was ein harter Stopp dort liegen
ließ, wird wiederaufgenommen; unvollständige OCR-Fragmente (`__ocr_*`) werden
gelöscht.
## Tests
```bash
pytest # 135 Tests
```
`ocrmypdf` muss dafür nicht installiert sein — der Import ist lazy und wird in
den Tests gemockt.
## Lizenz
MIT — © Sonith UG
---
**Version:** 0.5.0
**Version:** 0.6.0
**Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder