Files
pdf-ocr-hotfolder/AI_AGENT_BRIEFING.md
T
techadmin 04dc3c7b72 fix: gedrucktem --check-config-Befehl fehlte das cd ins Installationsverzeichnis (v0.6.2)
Das Paket ist nicht pip-installiert, sondern liegt unter /opt/pdf-ocr-hotfolder
und wird nur ueber das Arbeitsverzeichnis gefunden. Der Hinweis, den update.sh
in der Zusammenfassung ausgibt, lief deshalb so wie gedruckt nicht
("No module named pdf_ocr_hotfolder"). Gleiches galt fuer die Beispiele in
README.md, docs/INSTALLATION.md, docs/UPDATE.md und docs/OS-UPGRADE.md.

Gefunden beim Update-Test v0.3.1 -> v0.6.1 auf CT 200 (Debian 12).

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

24 KiB
Raw Blame History

AI Agent Briefing — PDF OCR Hotfolder

Zuletzt aktualisiert: 2026-09-22 Version: 0.6.2 Status: Multi-Instanz-Betrieb, Preflight-Checks, Fehlerzählung, Wiederaufnahme aus working/ und ein abgesicherter Updater (venv-Rebuild, Backup, Verifikation, Versionssprung-Meldung, Rauchtest). Test-Suite grün (152 pytest-Tests). Ein Produktiv-Einsatz ist im Repo (README/CHANGELOG) nicht dokumentiert — die bisherigen Fixes stammen aus Issues #1–#6 und aus Vorbereitungen auf Debian 13, nicht aus einem belegten Dauerbetrieb.

Betriebsabläufe stehen nicht hier, sondern in: README.md (Einstieg, Layout, Config-Überblick) · docs/INSTALLATION.md (Erstinstallation, Instanzen, Konfigurationsreferenz) · docs/UPDATE.md (Update, Backup, Rollback, --check-config) · docs/OS-UPGRADE.md (Debian-Major-Upgrade, venv-Rebuild, Pins). Dieses Briefing beschreibt wie der Code aufgebaut ist und warum — Schritt-für-Schritt-Anleitungen gehören in die drei Dokumente, nicht hierher.

🎯 Projektziel

Eingehende gescannte PDFs werden automatisch durch OCR (ocrmypdf + Tesseract) in durchsuchbare PDFs (optional PDF/A) umgewandelt und nach Wahl in einen Ordner / Nextcloud / per SFTP weitergegeben. Ersetzt das alte Bash-Tool pdf-tool (im Workspace).

📁 Projekt-Struktur

pdf-ocr-hotfolder/
├── pdf_ocr_hotfolder/
│   ├── __init__.py        # Versionsstring (__version__)
│   ├── __main__.py        # CLI (argparse: --config, --once, --check-config, --version)
│   ├── config.py          # TOML-Loader, Dataclasses, ConfigError, Warnungen
│   ├── service.py         # HotfolderService (watchdog + ThreadPool), Preflight, Resume, Zähler
│   ├── processor.py       # ocrmypdf-Call, veraPDF, Ausgabename, Original-Entsorgung
│   └── uploaders.py       # folder, nextcloud (WebDAV), sftp, E-Mail-Notify
├── tests/                 # pytest-Suite (152 Tests, ocrmypdf wird gemockt)
│   ├── conftest.py            # Fixtures tmp_config / dummy_pdf
│   ├── test_check_config.py   # --check-config, Exit 0/1/2
│   ├── test_config_errors.py
│   ├── test_config_warnings.py # Legacy- und Unbekannt-Warnungen
│   ├── test_error_counting.py
│   ├── test_ghostscript_version.py
│   ├── test_ocr_timeout.py
│   ├── test_once_exit_code.py
│   ├── test_output_naming.py
│   ├── test_preflight.py
│   ├── test_resume_working.py  # Wiederaufnahme + __ocr_-Fragmente
│   └── test_upload_folder.py
├── systemd/
│   ├── pdf-ocr-hotfolder@.service  # Template-Unit (Instanz = %i), TimeoutStopSec=300
│   └── lxc-compat.conf             # Drop-in-Vorlage: Hardening für LXC abschalten
├── docs/
│   ├── INSTALLATION.md    # Erstinstallation + Konfigurationsreferenz
│   ├── UPDATE.md          # update.sh, Backup/Rollback, --check-config, Config-Drift
│   └── OS-UPGRADE.md      # Debian 12 -> 13 -> 14, venv-Rebuild, Pins
├── pytest.ini             # testpaths = tests
├── config.example.toml
├── install.sh             # Interaktiver Installer + Instanz-Manager
├── update.sh              # Updater (--help, --rebuild-venv), ~860 Zeilen
├── requirements.txt       # feste Pins (ocrmypdf 16.x!)
├── VERSION
├── CHANGELOG.md
├── README.md
└── AI_AGENT_BRIEFING.md

🔧 Stack

Komponente Technologie
Sprache Python 3.11+ (für tomllib aus stdlib)
OCR ocrmypdf (als Library, nicht via Subprozess; Import ist lazy)
Engine Tesseract
Watcher watchdog
HTTP requests (Nextcloud WebDAV)
SFTP paramiko
Email smtplib (stdlib)
Tests pytest
Service systemd (Template-Unit)

Die vier Python-Deps sind in requirements.txt fest gepinnt (==), damit ein Update nicht ungefragt einen Major-Sprung einzieht (ocrmypdf 16 → 17 würde alle Instanzen auf einmal reißen). Geprüft gegen Python 3.11 (Debian 12) und 3.13 (Debian 13), Wheels für beide vorhanden. Anheben nur mit Testmaschine — docs/OS-UPGRADE.md.

🖥️ Installations-Layout (Multi-Instanz)

Pfad Inhalt
/opt/pdf-ocr-hotfolder/ Code + venv (für alle Instanzen gemeinsam)
/opt/pdf-ocr-hotfolder/.repo_path Pfad zum Repo, aus dem installiert wurde (nutzt update.sh)
/etc/pdf-ocr-hotfolder/<instanz>.toml Config pro Instanz (mode 640, root:)
/etc/systemd/system/pdf-ocr-hotfolder@.service Template-Unit
/etc/systemd/system/pdf-ocr-hotfolder@.service.d/lxc-compat.conf Drop-in für Container (optional)
/etc/systemd/system/pdf-ocr-hotfolder@<instanz>.service.d/user.conf Drop-in für abweichenden User (optional)
/var/lib/pdf-ocr-hotfolder/<instanz>/{incoming,working,outgoing,error}/ Daten pro Instanz
/var/backups/pdf-ocr-hotfolder/ Update-Backups (0600, letzte 5)

Ein eigenes Logverzeichnis gibt es nicht (seit 0.4.1 auch nicht mehr vom Installer angelegt): _setup_logging() nutzt logging.basicConfig() ohne FileHandler, alles geht nach stdout → journald.

journalctl -u pdf-ocr-hotfolder@<instanz> -f          # eine Instanz mitlesen
journalctl -u 'pdf-ocr-hotfolder@*' --since today     # alle Instanzen, heute

👤 Service-User

  • Basis-Install legt Default-User pdfocr an (als System-User, falls nicht schon vorhanden)
  • Beim Anlegen einer Instanz fragt der Installer nach dem Service-User (default pdfocr)
  • Wird ein abweichender User gewählt, wird ein systemd-Drop-in erstellt (pdf-ocr-hotfolder@<instanz>.service.d/user.conf) mit User=/Group= Override
  • Existierende User (lokal oder AD via SSSD/Winbind) werden übernommen, primäre Gruppe via id -gn ermittelt
  • Bei AD-Usern mit lokaler UID werden Datei-Berechtigungen über die UID gesetzt — transparent

🗂️ Instanz-Management (Kurzfassung)

install.sh ist gleichzeitig Installer und Instanz-Manager. Der komplette Ablauf inklusive aller fünf Abfragen steht in docs/INSTALLATION.md. Für die Arbeit am Code zählt:

  • Basis-Install wird an venv + Template-Unit erkannt und übersprungen — außer die venv passt nicht mehr zum System-Python, dann läuft er zur Reparatur erneut (venv_is_healthy() in install.sh, schlankere Variante der Prüfung in update.sh).
  • Abfragen pro Instanz: Name, Basis-Pfad, Service-User, OCR-Sprachen, Original archivieren? — LANGS/ORIG_MODE/ARCHIVE_DIR sind local in create_instance(), gelten also instanz-lokal und nicht global.
  • Sprachprüfung gegen tesseract --list-langs, fehlende Pakete werden als tesseract-ocr-<code> angeboten (Unterstrich → Bindestrich). Ablehnung führt nicht zum Abbruch, sondern zurück zur Sprach-Abfrage.
  • Das Archiv-Verzeichnis darf nicht incoming//outgoing//working//error/ sein — im Eingang würde das Original endlos neu aufgegriffen.
  • <instanz>.toml wird aus config.example.toml per sed erzeugt. Substituiert werden die vier [paths]-Zeilen sowie [ocr].languages, [output].original_on_success und [output].archive_dir. Die Ausdrücke sind am Zeilenanfang verankert (^key[[:space:]]*=), damit die deutschen Kommentarzeilen über den Keys nicht getroffen werden; Pfad-Variablen laufen vorher durch sed_escape_repl() (maskiert \, &, |). Nach dem sed-Lauf liest config_value() die drei Keys zurück und vergleicht sie mit der Eingabe.
  • Die apt-Paketliste steht als einzige Quelle in install.sh zwischen den Marken # --- BEGIN apt-packages / # --- END apt-packages in der Funktion pdf_ocr_apt_packages(). update.sh schneidet diesen Block per sed heraus und evaluiert ihn — Marken und Funktionsname dürfen sich nicht ändern, ohne update.sh anzupassen.
  • Instanz wird sofort enable --now gestartet. Löschen macht der Installer nicht, das steht als Handgriff in docs/INSTALLATION.md.

🔄 Update-Verhalten (Kurzfassung)

Vollständig: docs/UPDATE.md. Für die Arbeit am Skript wichtig:

  • update.sh hat --help und --rebuild-venv, läuft mit set -Eeuo pipefail und hat ab dem Stoppen der Instanzen einen ERR/INT/TERM-Trap: er sagt, ob auf der Platte schon getauscht wurde (TOUCHED), startet die vorher laufenden Instanzen wieder und nennt Backup + Rollback-Befehl.
  • Reihenfolge: Instanzen erfassen → apt-Sync → venv-Health → stoppen → Backup → Code → Deps/venv → Units → chown → --check-config → starten + verifizieren → Zusammenfassung (Soll gegen Ist, Exit 1 bei Regression/Config-Fehler).
  • Instanz-Erfassung deckt list-units --all (inkl. activating/failed), list-unit-files und die Configs unter /etc/pdf-ocr-hotfolder/ ab. Drei Gruppen: PREV_OK, PREV_BROKEN, PREV_STOPPED — bewusst gestoppte bleiben gestoppt.
  • Verifikation: verify_unit() wartet VERIFY_WAIT (6 s) und prüft is-active, is-failed und NRestarts — sonst würde ein Crash-Loop bei Type=simple als Erfolg durchgehen. Vorher reset-failed.
  • venv-Health (venv_is_healthy() in update.sh): Verzeichnis, ausführbarer Interpreter, Interpreter läuft überhaupt, major.minor == System-Python, pyvenv.cfg stimmt mit dem Interpreter überein. Bei Drift wird auch ohne --rebuild-venv neu gebaut.
  • rebuild_venv() ist ganz oder gar nicht: alte venv nach venv.old-<ts>, neu bauen, Requirements installieren, erst bei Erfolg die alte löschen; scheitert etwas, wird zurückgerollt und hart abgebrochen. pip_install_requirements() übersetzt pip-Fehler in eine Ansage mit dem gescheiterten Paketnamen und dem Hinweis "requirements.txt anheben".
  • apt-Sync läuft auch beim Update (sync_system_packages()), ist idempotent und fasst nachinstallierte Sprachpakete nicht an (kein purge/autoremove). Fehlschläge setzen nur APT_WARN, sie brechen nicht ab.
  • Backup (create_backup()): Code, /etc/pdf-ocr-hotfolder/, Template-Unit, alle Drop-ins und ein pip-freeze.txt der alten venv. Ohne venv und ohne Datenverzeichnisse. umask 077 + chmod 600 root:root, weil die Configs Klartext-Passwörter enthalten. Rotation: letzte BACKUP_KEEP = 5.
  • LXC-Drop-in wird beim Update aus dem Repo nachgezogen, falls es installiert ist — sonst würde ein neu ergänzter Hardening-Schalter in der Template-Unit alle Container-Instanzen reißen (Issue #4 redux).
  • Configs unter /etc/pdf-ocr-hotfolder/ werden nie überschrieben. Das Repo muss erhalten bleiben — update.sh kopiert daraus (.repo_path).
  • PDF_OCR_UPDATE_LIB_ONLY=1 source ./update.sh lädt nur die Funktionen, ohne irgendetwas zu tun — dafür sind INSTALL_DIR, CONFIG_DIR, SYSTEMD_DIR, BACKUP_DIR, TAR_ROOT, VERIFY_WAIT, BACKUP_KEEP überschreibbar.

⚙️ Konfiguration (Überblick)

Key-für-Key-Referenz: docs/INSTALLATION.md. Vollständiges Beispiel mit Kommentaren: config.example.toml.

Sektion Zweck
[paths] incoming, outgoing, working, error — Pflicht, fehlt einer → ConfigError + Exit 2
[ocr] languages, jobs, skip_text, oversample, pdfa_level, deskew, clean, max_workers, timeout (Sekunden pro Seite)
[output] name_mode (prefix/suffix/none), name_tag, original_on_success (delete/archive), archive_dir
[verapdf] enabled, binary, flavour — optionale PDF/A-Validierung per CLI
[upload.folder] enabled, target (leer = [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
[notify.email] enabled, SMTP-Daten, from_addr, to_addrs, on = always/errors/never
[logging] level = DEBUG/INFO/WARNING/ERROR

Unbekannte Keys werden beim Laden zwar ignoriert, aber nicht mehr still: _collect_unknown_keys() sammelt sie in Config.unknown_keys (Format [ocr].langauges, auch ganze unbekannte Sektionen und Upload-/Notify-Targets), unknown_key_warnings() macht Meldungen daraus. Ein Tippfehler fällt damit auf.

Warnungen statt Überraschungen

config.py kennt zwei Warnungsquellen, beide über config_warnings() gebündelt — der Text steht nur dort, weil ihn sowohl der Dienststart (_log_config_warnings() → log.warning) als auch --check-config ausgibt:

  • legacy_warnings(): [ocr].timeout >= LEGACY_TIMEOUT_THRESHOLD (900) deutet auf den alten Gesamt-Timeout-Wert 1800 hin (Richtwert RECOMMENDED_PAGE_TIMEOUT = 300); gesetztes pdfa_level weist auf den Ghostscript-Bug hin.
  • unknown_key_warnings(): siehe oben.

--check-config

python -m pdf_ocr_hotfolder --check-config --config <datei> lädt die Config, zeigt Pfade/Sprachen/Timeout/PDF/A, fährt check_preflight() und check_output_config() und gibt die Warnungen aus. Exit-Codes: CHECK_OK=0, CHECK_WARN=1, CHECK_ERROR=2. Hat Vorrang vor --once. update.sh wertet genau diese Codes aus und erkennt an der argparse-Meldung, wenn der installierte Code das Flag noch nicht kennt.

🔄 Verarbeitungs-Flow

Beim Start (run() wie run_once()), vor allem anderen:

  1. check_preflight(pdfa_level, skip_text) — tesseract und gs müssen im PATH sein; zusätzlich wird die Ghostscript-Version gegen den 10.0.0–10.02.0-Bug geprüft, und zwar unter genau der Bedingung, unter der ocrmypdf selbst abbricht (_gs_block_reason(): betroffene GS-Version und skip_text und (pdfa_level gesetzt oder ocrmypdf < 17))
  2. check_output_config() — validiert original_on_success, archive_dir (Pflicht bei archive) und name_mode
  3. Scheitert eines davon → PreflightError, CLI beendet sich mit Exit-Code 2 (ebenso bei kaputter/fehlender Config)
  4. ensure_dirs(), dann _scan_existing(): zuerst working/, danach incoming/

Wiederaufnahme aus working/ (_scan_working()): process_pdf() verschiebt das Original vor dem OCR nach working/. Wird der Dienst dort abgeschossen (SIGKILL nach TimeoutStopSec), blieb es früher liegen und wurde nie wieder angefasst — stiller Datenverlust. Jetzt:

  • Dateien mit dem Präfix OCR_TEMP_PREFIX (__ocr_) sind unvollständige Fragmente des abgebrochenen ocrmypdf-Laufs: als Eingabe unbrauchbar, als Ergebnis wertlos → werden gelöscht (mit log.warning).
  • Echte PDFs werden an Ort und Stelle wiederaufgenommen; process_pdf() erkennt das über _is_same_file() und verschiebt nicht erneut.
  • Liegt in incoming/ eine gleichnamige, andere Datei, bekommt die wiederaufgenommene per _free_resume_name() einen Zeitstempel angehängt — sonst würden beide dieselbe working- und outgoing-Datei beanspruchen.
  • Liegt in working/ bereits eine andere Datei desselben Namens, bricht process_pdf() für die neue ab und lässt sie in incoming/ liegen, statt den laufenden Vorgang stillschweigend zu überschreiben.

Pro Datei:

  1. watchdog triggert auf created/moved/closed in incoming/
  2. _wait_until_stable() wartet, bis die Datei nicht mehr wächst (max. ~60s)
  3. Move nach working/ (entfällt bei Wiederaufnahme)
  4. ocrmypdf.ocr() als Library-Call (kein Subprozess-Start pro PDF), Ziel ist working/__ocr_<zielname>
  5. Optional: veraPDF-Validierung (CLI-Subprozess) — bei FAIL geht das OCR-Ergebnis nach error/, das Original folgt original_on_success (wird also bei archive nicht gelöscht)
  6. Move nach outgoing/ unter dem laut [output] gebauten Namen (build_output_name())
  7. Original in working/ wird laut original_on_success gelöscht oder nach archive_dir archiviert (Kollision → Timestamp-Suffix)
  8. Aktive Upload-Targets ausführen (folder/nextcloud/sftp)
  9. E-Mail-Notify je nach [notify.email].on

Fehlerbehandlung:

Fehlerfall Zählt als Fehler Wo liegt die Datei danach
Stabilitäts-Check läuft in den Timeout ja bleibt in incoming/, wird beim nächsten Lauf erneut versucht
Datei verschwindet vor der Verarbeitung nein —
In working/ liegt schon eine andere Datei gleichen Namens ja bleibt in incoming/
OCR wirft (ocrmypdf) ja error/
veraPDF FAIL ja OCR-Ergebnis nach error/, Original laut original_on_success (delete → weg, archive → archive_dir; seit 0.4.1)
Beliebige Exception aus process_pdf() (z.B. shutil.move nach outgoing/) ja _rescue_to_error() sucht in incoming/ und working/ und verschiebt nach error/
Mindestens ein Upload-Ziel schlägt fehl ja PDF bleibt bewusst in outgoing/ (das OCR war ja erfolgreich), Fehler-Mail nennt die Ziele

Der Service läuft in allen Fällen weiter (kein exit 1 wie im alten Bash-Tool). Im --once-Modus liefert die CLI Exit-Code 1, sobald error_count > 0 ist, sonst 0.

🧠 Performance-Entscheidungen

  • ocrmypdf als Library statt subprocess: spart Python-Interpreter-Start pro PDF
  • ThreadPool mit max_workers (default 2) — selbst wenn selten >1 PDF gleichzeitig kommt, blockiert ein langsamer Scan keinen schnellen
  • --jobs an ocrmypdf: Tesseract parallelisiert Seiten innerhalb eines PDFs
  • skip_text=True: bereits OCR-haltige Seiten werden nicht neu verarbeitet
  • Stabilitäts-Check statt magic-file new (alte Bash-Krücke)
  • upload_folder() nutzt shutil.copyfile() statt read_bytes()/write_bytes() — große PDFs landen nicht komplett im RAM
  • veraPDF nur wenn enabled=true (JVM-Start ist teuer)

⚠️ Fallstricke

  • Ghostscript 10.0.0–10.02.0 zerschießt OCR. Das ist der Debian-12-Default. ocrmypdf verweigert damit die Arbeit — aber die Bedingung dafür hängt an der ocrmypdf-Version, und genau daran ist 0.6.0 gescheitert:

    • ocrmypdf ≤ 16.x: die Prüfung in builtin_plugins/ghostscript.py::check_options() läuft bedingungslos. skip_text = true allein reicht — output_type wird nicht geprüft. Auf Debian 12 scheitert damit jede Datei.
    • ocrmypdf ≥ 17.0: derselbe Block steckt in einem if options.output_type.startswith('pdfa'):. Ohne PDF/A wird Ghostscript nicht angefasst.

    pdfa_level = "" ist deshalb kein Schutz für sich genommen — es wirkt nur mit ocrmypdf ≥ 17. requirements.txt pinnt daher 17.x; ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar, bei grünem systemctl status. Der Preflight bildet die reale Bedingung ab (_gs_block_reason()) und bricht mit Exit 2 ab, --check-config meldet denselben Zustand als Fehler. redo_ocr ist bewusst nicht in der Bedingung: die Config kennt keinen solchen Key. Abhilfe: Ghostscript ≥ 10.02.1 aus bookworm-backports (der Installer bietet das an) oder skip_text = false.

  • [ocr].timeout ist ein Timeout PRO SEITE, kein Gesamt-Timeout pro PDF. Der Wert geht als tesseract_timeout (ocrmypdf-Option --tesseract-timeout) durch; ocrmypdf kennt kein Dokument-Timeout. Wer noch den alten Default 1800 in einer Config stehen hat, gibt Tesseract 30 Minuten je Seite — Richtwert ist 300, ab 900 warnt --check-config. Ein durchgereichtes 0 würde ocrmypdf dazu bringen, OCR still zu überspringen, deshalb wird bei 0 (oder negativ) gar nichts übergeben und der ocrmypdf-Default greift.

  • TimeoutStopSec=300 in der Unit ist Absicht. Ein laufendes OCR soll beim Stoppen zu Ende laufen dürfen — ein systemctl stop kann deshalb pro Instanz bis zu 5 Minuten dauern, und update.sh (das nacheinander stoppt) entsprechend länger. Bei SIGKILL bliebe das Original in working/ liegen; das wird zwar wiederaufgenommen, kostet aber den kompletten Durchlauf.

  • Die venv hängt an der Python-Version der Distribution. Nach einem Debian-Major-Upgrade ist venv/bin/python tot (systemd: 203/EXEC) oder eine andere Version als das System-Python. Der Weg dahin und zurück steht in docs/OS-UPGRADE.md; im Code prüfen install.sh und update.sh das je mit einem eigenen venv_is_healthy() (die Variante in update.sh ist die gründlichere und schaut zusätzlich in pyvenv.cfg).

  • systemd-Hardening bricht in LXC-Containern (Error 226/NAMESPACE durch PrivateTmp, ProtectSystem usw., Issue #4). Gegenmittel ist das Drop-in systemd/lxc-compat.conf nach /etc/systemd/system/pdf-ocr-hotfolder@.service.d/; der Installer erkennt Container via systemd-detect-virt --container und bietet es an, update.sh zieht ein vorhandenes Drop-in nach.

  • Das Paket wird nicht pip-installiert, sondern nach /opt/pdf-ocr-hotfolder kopiert. Gestartet wird per python -m pdf_ocr_hotfolder, gefunden wird das Modul nur über das Arbeitsverzeichnis — WorkingDirectory=/opt/pdf-ocr-hotfolder in der Unit ist daher Pflicht, nicht Kosmetik (Issue #5). Auch update.sh ruft --check-config deshalb mit cd "$INSTALL_DIR" auf.

  • Klartext-Passwörter in der Instanz-Config: SMTP-, Nextcloud- und SFTP-Zugangsdaten stehen unverschlüsselt in /etc/pdf-ocr-hotfolder/<instanz>.toml. Deshalb chmod 640 und chown root:<service-gruppe>, und /etc/pdf-ocr-hotfolder selbst 750 root:pdfocr. Das Update-Backup enthält diese Configs und ist deshalb 0600 root:root in einem 700-Verzeichnis. Beim Debuggen weder Config noch Backup in ein Ticket kopieren.

  • Das Update-Backup enthält die venv NICHT. Ein Rollback per tar -xzf … -C / holt den Paketstand also nicht zurück, und tar löscht nichts, was neu hinzugekommen ist. Grenzen des Rollbacks: docs/UPDATE.md.

🛠️ Entwicklung

Lokaler Test ohne Installation:

cd ~/dev/gitea.sonith.de/pdf-ocr-hotfolder
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp config.example.toml /tmp/config.toml
# Pfade in /tmp/config.toml auf Test-Verzeichnisse anpassen
python -m pdf_ocr_hotfolder --config /tmp/config.toml

Tests (aus dem Repo-Root, pytest.ini setzt testpaths = tests):

pytest          # aktuell 152 Tests

ocrmypdf muss dafür nicht installiert sein: der Import in processor.py ist lazy, und tests/test_ocr_timeout.py schiebt ein Dummy-Modul in sys.modules. Die übrigen Tests mocken process_pdf bzw. arbeiten nur auf Config-Ebene.

📋 Roadmap / TODO

  • Tests (pytest) für processor und uploaders — 152 Tests
  • Wiederaufnahme abgebrochener Läufe aus working/
  • Config-Prüfung ohne Verarbeitung (--check-config) + Auswertung im Updater
  • Updater übersteht Debian-Major-Upgrades (venv-Rebuild, Pins, Rollback)
  • Test-Lücken schließen: der watchdog-Eventpfad (_Handler/Observer) wird nirgends getestet, run_verapdf() ebenso wenig (der FAIL-Pfad in process_pdf() ist getestet, die veraPDF-CLI-Anbindung selbst nicht), und run_ocr() nur gegen ein gemocktes ocrmypdf — es gibt keinen Test mit einer echten PDF-Datei. Auch upload_nextcloud() und upload_sftp() sind ungetestet (nur upload_folder()). install.sh/update.sh haben keine automatisierten Tests — die LIB_ONLY-Schnittstelle in update.sh ist dafür vorbereitet, aber ungenutzt.
  • Prometheus-Metriken (verarbeitete PDFs, Fehlerquote, Laufzeit)
  • CLI-Subkommandos: pdf-ocr-hotfolder reprocess <error-file>
  • Instanz-Löschung in install.sh statt als Handarbeit
  • Optional: S3/MinIO Upload-Target
  • Docker-Image für Setups ohne systemd

🔑 Repo

  • Repo: https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
  • Owner: sonith_ug
  • SSH-User ist gitea, nicht git: gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
  • Versionierung: Semver (PATCH bei jedem Build, MINOR bei Features, MAJOR manuell)
  • Tags: v{VERSION}, automatischer Push nach Commit

📞 Kontakt

Maintainer: Dominik Höfling (Sonith GmbH)