# AI Agent Briefing — PDF OCR Hotfolder **Zuletzt aktualisiert:** 2026-09-23 **Version:** 0.6.3 **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](README.md) (Einstieg, Layout, Config-Überblick) · > [docs/INSTALLATION.md](docs/INSTALLATION.md) (Erstinstallation, Instanzen, Konfigurationsreferenz) · > [docs/UPDATE.md](docs/UPDATE.md) (Update, Backup, Rollback, `--check-config`) · > [docs/OS-UPGRADE.md](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 17.x — 16.x ist unbrauchbar, s. 0.6.1) ├── 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](docs/OS-UPGRADE.md#pins-in-requirementstxt). ## 🖥️ 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/.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@.service.d/user.conf` | Drop-in für abweichenden User (optional) | | `/var/lib/pdf-ocr-hotfolder//{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. ```bash journalctl -u pdf-ocr-hotfolder@ -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@.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](docs/INSTALLATION.md#die-abfragen-pro-instanz). 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-` 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. - `.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](docs/INSTALLATION.md#instanz-manuell-löschen). ## 🔄 Update-Verhalten (Kurzfassung) Vollständig: [docs/UPDATE.md](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-`, 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](docs/INSTALLATION.md#konfigurationsreferenz). 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 ` 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_` 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](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/.toml`. Deshalb `chmod 640` und `chown root:`, 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](docs/UPDATE.md#grenzen-des-rollbacks). ## 🛠️ Entwicklung Lokaler Test ohne Installation: ```bash 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`): ```bash 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 - [x] Tests (`pytest`) für `processor` und `uploaders` — 152 Tests - [x] Wiederaufnahme abgebrochener Läufe aus `working/` - [x] Config-Prüfung ohne Verarbeitung (`--check-config`) + Auswertung im Updater - [x] 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 ` - [ ] 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)