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>
24 KiB
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 |
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
pdfocran (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) mitUser=/Group=Override - Existierende User (lokal oder AD via SSSD/Winbind) werden übernommen, primäre Gruppe via
id -gnermittelt - 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()ininstall.sh, schlankere Variante der Prüfung inupdate.sh). - Abfragen pro Instanz: Name, Basis-Pfad, Service-User, OCR-Sprachen,
Original archivieren? —
LANGS/ORIG_MODE/ARCHIVE_DIRsindlocalincreate_instance(), gelten also instanz-lokal und nicht global. - Sprachprüfung gegen
tesseract --list-langs, fehlende Pakete werden alstesseract-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>.tomlwird ausconfig.example.tomlpersederzeugt. Substituiert werden die vier[paths]-Zeilen sowie[ocr].languages,[output].original_on_successund[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 durchsed_escape_repl()(maskiert\,&,|). Nach dem sed-Lauf liestconfig_value()die drei Keys zurück und vergleicht sie mit der Eingabe.- Die apt-Paketliste steht als einzige Quelle in
install.shzwischen den Marken# --- BEGIN apt-packages/# --- END apt-packagesin der Funktionpdf_ocr_apt_packages().update.shschneidet diesen Block persedheraus und evaluiert ihn — Marken und Funktionsname dürfen sich nicht ändern, ohneupdate.shanzupassen. - Instanz wird sofort
enable --nowgestartet. 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.shhat--helpund--rebuild-venv, läuft mitset -Eeuo pipefailund 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-filesund die Configs unter/etc/pdf-ocr-hotfolder/ab. Drei Gruppen:PREV_OK,PREV_BROKEN,PREV_STOPPED— bewusst gestoppte bleiben gestoppt. - Verifikation:
verify_unit()wartetVERIFY_WAIT(6 s) und prüftis-active,is-failedundNRestarts— sonst würde ein Crash-Loop beiType=simpleals Erfolg durchgehen. Vorherreset-failed. - venv-Health (
venv_is_healthy()inupdate.sh): Verzeichnis, ausführbarer Interpreter, Interpreter läuft überhaupt,major.minor== System-Python,pyvenv.cfgstimmt mit dem Interpreter überein. Bei Drift wird auch ohne--rebuild-venvneu gebaut. rebuild_venv()ist ganz oder gar nicht: alte venv nachvenv.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 nurAPT_WARN, sie brechen nicht ab. - Backup (
create_backup()): Code,/etc/pdf-ocr-hotfolder/, Template-Unit, alle Drop-ins und einpip-freeze.txtder alten venv. Ohne venv und ohne Datenverzeichnisse.umask 077+chmod 600 root:root, weil die Configs Klartext-Passwörter enthalten. Rotation: letzteBACKUP_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.shkopiert daraus (.repo_path). PDF_OCR_UPDATE_LIB_ONLY=1 source ./update.shlädt nur die Funktionen, ohne irgendetwas zu tun — dafür sindINSTALL_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 (RichtwertRECOMMENDED_PAGE_TIMEOUT= 300); gesetztespdfa_levelweist 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:
check_preflight(pdfa_level, skip_text)—tesseractundgsmü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 undskip_textund (pdfa_levelgesetzt oder ocrmypdf < 17))check_output_config()— validiertoriginal_on_success,archive_dir(Pflicht beiarchive) undname_mode- Scheitert eines davon →
PreflightError, CLI beendet sich mit Exit-Code 2 (ebenso bei kaputter/fehlender Config) ensure_dirs(), dann_scan_existing(): zuerstworking/, danachincoming/
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 (mitlog.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, brichtprocess_pdf()für die neue ab und lässt sie inincoming/liegen, statt den laufenden Vorgang stillschweigend zu überschreiben.
Pro Datei:
watchdogtriggert aufcreated/moved/closedinincoming/_wait_until_stable()wartet, bis die Datei nicht mehr wächst (max. ~60s)- Move nach
working/(entfällt bei Wiederaufnahme) ocrmypdf.ocr()als Library-Call (kein Subprozess-Start pro PDF), Ziel istworking/__ocr_<zielname>- Optional: veraPDF-Validierung (CLI-Subprozess) — bei FAIL geht das OCR-Ergebnis nach
error/, das Original folgtoriginal_on_success(wird also beiarchivenicht gelöscht) - Move nach
outgoing/unter dem laut[output]gebauten Namen (build_output_name()) - Original in
working/wird lautoriginal_on_successgelöscht oder nacharchive_dirarchiviert (Kollision → Timestamp-Suffix) - Aktive Upload-Targets ausführen (folder/nextcloud/sftp)
- 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 --jobsan ocrmypdf: Tesseract parallelisiert Seiten innerhalb eines PDFsskip_text=True: bereits OCR-haltige Seiten werden nicht neu verarbeitet- Stabilitäts-Check statt magic-file
new(alte Bash-Krücke) upload_folder()nutztshutil.copyfile()stattread_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 = trueallein reicht —output_typewird 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.txtpinnt daher 17.x; ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar, bei grünemsystemctl status. Der Preflight bildet die reale Bedingung ab (_gs_block_reason()) und bricht mit Exit 2 ab,--check-configmeldet denselben Zustand als Fehler.redo_ocrist bewusst nicht in der Bedingung: die Config kennt keinen solchen Key. Abhilfe: Ghostscript ≥ 10.02.1 aus bookworm-backports (der Installer bietet das an) oderskip_text = false. - ocrmypdf ≤ 16.x: die Prüfung in
-
[ocr].timeoutist ein Timeout PRO SEITE, kein Gesamt-Timeout pro PDF. Der Wert geht alstesseract_timeout(ocrmypdf-Option--tesseract-timeout) durch; ocrmypdf kennt kein Dokument-Timeout. Wer noch den alten Default1800in einer Config stehen hat, gibt Tesseract 30 Minuten je Seite — Richtwert ist 300, ab 900 warnt--check-config. Ein durchgereichtes0würde ocrmypdf dazu bringen, OCR still zu überspringen, deshalb wird bei0(oder negativ) gar nichts übergeben und der ocrmypdf-Default greift. -
TimeoutStopSec=300in der Unit ist Absicht. Ein laufendes OCR soll beim Stoppen zu Ende laufen dürfen — einsystemctl stopkann deshalb pro Instanz bis zu 5 Minuten dauern, undupdate.sh(das nacheinander stoppt) entsprechend länger. Bei SIGKILL bliebe das Original inworking/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/pythontot (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üfeninstall.shundupdate.shdas je mit einem eigenenvenv_is_healthy()(die Variante inupdate.shist die gründlichere und schaut zusätzlich inpyvenv.cfg). -
systemd-Hardening bricht in LXC-Containern (
Error 226/NAMESPACEdurchPrivateTmp,ProtectSystemusw., Issue #4). Gegenmittel ist das Drop-insystemd/lxc-compat.confnach/etc/systemd/system/pdf-ocr-hotfolder@.service.d/; der Installer erkennt Container viasystemd-detect-virt --containerund bietet es an,update.shzieht ein vorhandenes Drop-in nach. -
Das Paket wird nicht pip-installiert, sondern nach
/opt/pdf-ocr-hotfolderkopiert. Gestartet wird perpython -m pdf_ocr_hotfolder, gefunden wird das Modul nur über das Arbeitsverzeichnis —WorkingDirectory=/opt/pdf-ocr-hotfolderin der Unit ist daher Pflicht, nicht Kosmetik (Issue #5). Auchupdate.shruft--check-configdeshalb mitcd "$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. Deshalbchmod 640undchown root:<service-gruppe>, und/etc/pdf-ocr-hotfolderselbst750 root:pdfocr. Das Update-Backup enthält diese Configs und ist deshalb0600 root:rootin einem700-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, undtarlö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ürprocessorunduploaders— 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 inprocess_pdf()ist getestet, die veraPDF-CLI-Anbindung selbst nicht), undrun_ocr()nur gegen ein gemocktes ocrmypdf — es gibt keinen Test mit einer echten PDF-Datei. Auchupload_nextcloud()undupload_sftp()sind ungetestet (nurupload_folder()).install.sh/update.shhaben keine automatisierten Tests — dieLIB_ONLY-Schnittstelle inupdate.shist dafür vorbereitet, aber ungenutzt. - Prometheus-Metriken (verarbeitete PDFs, Fehlerquote, Laufzeit)
- CLI-Subkommandos:
pdf-ocr-hotfolder reprocess <error-file> - Instanz-Löschung in
install.shstatt 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, nichtgit: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)