techadmin cd803a3dfe feat: stille Datenverlust-Pfade geschlossen, gemeinsame Shell-Lib (v0.7.0)
Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an
denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde.

Datenverlust:
- veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight
  geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" —
  Ergebnis nach error/, Original geloescht (Default delete). run_verapdf()
  trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung
  (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der
  Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das
  Original wird nicht entsorgt.
- Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload
  mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel
  daneben, mit Warnung; ProcessResult.output traegt den echten Pfad.

Robustheit:
- Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback.
- RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft
  nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen.
- Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu
  auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr.
- Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt
  still unter /opt zu landen.
- Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und
  Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail.
- Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet.
- Logging explizit nach stdout (die Doku versprach das schon).

Struktur:
- Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte
  venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung —
  die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte
  venv als gesund durchgewunken (nachgewiesen).
- install.sh warnt in Containern, wenn systemd-journald nicht laeuft.

Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify),
Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS,
AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte,
Exit-Code-Tabelle.

254 Tests gruen (vorher 152).

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

PDF OCR Hotfolder

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 Erstinstallation, Instanzen anlegen, LXC, Ghostscript, Konfigurationsreferenz, Troubleshooting
docs/UPDATE.md Update mit update.sh: Ablauf, Backup, Rollback, --check-config, Config-Drift
docs/OS-UPGRADE.md Debian-Major-Upgrade (12 → 13): venv neu bauen, Pins anheben

Weiter: CHANGELOG.md · AI_AGENT_BRIEFING.md · 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 — Binary wird im Preflight geprüft, eine Störung gilt nicht als FAIL
  • 🚫 Überschreibt nie eine gleichnamige Datei — in outgoing/, error/, Archiv und Ordner-Upload weicht sie mit Zeitstempel aus
  • ☁️ 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, Multi-Instanz über eine Template-Unit
  • 👁️ Toter Verzeichnis-Watch wird erkannt — der Dienst beendet sich (Exit 3), systemd setzt den Watch neu auf
  • 🩺 --check-config prüft eine Instanz-Config ohne etwas zu verarbeiten

Schnellstart

Voraussetzungen: Debian 12 oder 13, Python 3.11+, root — und mindestens 2 GB RAM (512 MB reichen für 300-dpi-Scans nachweislich nicht, siehe Systemanforderungen). Dateisystem ext4, xfs oder zfs; incoming/ muss lokal liegen — auf einem CIFS/NFS-Mount liefert inotify keine Events und der Hotfolder bemerkt neue Dateien nur noch beim Start (warum).

git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
cd pdf-ocr-hotfolder
sudo ./install.sh

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:

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/-Ordner der Instanz.

Alle Details zu den Abfragen, zum Multi-Instanz-Betrieb und zu den Fallstricken (LXC, Ghostscript): docs/INSTALLATION.md.

Update: git pull && sudo ./update.sh — siehe docs/UPDATE.md. Nach einem Debian-Major-Upgrade: docs/OS-UPGRADE.md.

Verzeichnisse

Pfad Zweck
/opt/pdf-ocr-hotfolder/ Code + venv (für alle Instanzen gemeinsam)
/opt/pdf-ocr-hotfolder/lib/common.sh gemeinsame Shell-Funktionen für install.sh/update.sh (mitkopiert)
/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 (0600, letzte 5)

Ein eigenes Logverzeichnis gibt es nicht — der Dienst loggt nach stdout und damit ins journal.

Konfiguration im Überblick

Jede Instanz hat ihre eigene TOML unter /etc/pdf-ocr-hotfolder/<instanz>.toml. Vollständiges, kommentiertes Beispiel: config.example.toml. Key-für-Key-Referenz: docs/INSTALLATION.md.

Sektion Zweck
[paths] incoming, outgoing, working, error — Pflicht, alle absolut
[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:

cd /opt/pdf-ocr-hotfolder && sudo ./venv/bin/python -m pdf_ocr_hotfolder \
    --check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml

Exit 0 = sauber, 1 = Warnungen, 2 = Fehler. Details: docs/UPDATE.md.

Exit-Codes des Dienstes

Exit Bedeutung Neustart durch systemd
0 regulärer Stopp —
1 nur bei --once: mindestens eine PDF fehlgeschlagen —
2 Config- oder Preflight-Fehler nein (RestartPreventExitStatus=2) — die Instanz bleibt sichtbar failed
3 Verzeichnis-Watch gestorben, es würden keine Dateien mehr erkannt ja, genau dafür

Vollständig: docs/INSTALLATION.md.

Service-Verwaltung

# Eine bestimmte Instanz
sudo systemctl status pdf-ocr-hotfolder@kunde-a
sudo systemctl restart pdf-ocr-hotfolder@kunde-a
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

Ein laufendes OCR darf beim Stoppen zu Ende laufen (TimeoutStopSec=300) — ein stop kann deshalb pro Instanz bis zu 5 Minuten dauern.

Architektur

   ┌──────────┐    watchdog     ┌──────────────┐    ocrmypdf    ┌──────────┐
   │ Scanner  │ ──────────────▶ │   incoming/  │ ─────────────▶ │ working/ │
   └──────────┘   PDF-Datei     └──────────────┘   (Library)    └────┬─────┘
                                                                     │
                                                              optional veraPDF
                                                                     │
                                                                     ▼
                                                              ┌──────────────┐
                                                              │  outgoing/   │
                                                              └──────┬───────┘
                                                                     │
                                              ┌──────────────────────┼──────────────────────┐
                                              ▼                      ▼                      ▼
                                       ┌────────────┐        ┌────────────┐        ┌────────────┐
                                       │ Nextcloud  │        │    SFTP    │        │  E-Mail    │
                                       │  (WebDAV)  │        │ (paramiko) │        │  Notify    │
                                       └────────────┘        └────────────┘        └────────────┘

Beim Start wird working/ zuerst durchsucht: was ein harter Stopp dort liegen ließ, wird wiederaufgenommen; unvollständige OCR-Fragmente (__ocr_*) werden gelöscht.

Tests

pytest          # 254 Tests

ocrmypdf muss dafür nicht installiert sein — der Import ist lazy und wird in den Tests gemockt.

Lizenz

MIT — © Sonith UG


Version: 0.7.0 Repo: https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder

S
Description
Hotfolder service that converts incoming scanned PDFs to searchable PDFs via OCR
Readme 781 KiB
Languages
Python 69.2%
Shell 30.8%