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:
+168
-84
@@ -1,8 +1,15 @@
|
||||
# AI Agent Briefing — PDF OCR Hotfolder
|
||||
|
||||
**Zuletzt aktualisiert:** 2026-09-22
|
||||
**Version:** 0.5.0
|
||||
**Status:** Multi-Instanz-Betrieb, Preflight-Checks und Fehlerzählung vorhanden, Test-Suite grün (95 pytest-Tests). Ein Produktiv-Einsatz ist im Repo (README/CHANGELOG) nicht dokumentiert — die bisherigen Fixes stammen aus Issues #1–#6, nicht aus einem belegten Dauerbetrieb.
|
||||
**Version:** 0.6.0
|
||||
**Status:** Multi-Instanz-Betrieb, Preflight-Checks, Fehlerzählung, Wiederaufnahme aus `working/` und ein abgesicherter Updater (venv-Rebuild, Backup, Verifikation). Test-Suite grün (135 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
|
||||
|
||||
@@ -14,32 +21,40 @@ Eingehende gescannte PDFs werden automatisch durch OCR (ocrmypdf + Tesseract) in
|
||||
pdf-ocr-hotfolder/
|
||||
├── pdf_ocr_hotfolder/
|
||||
│ ├── __init__.py # Versionsstring (__version__)
|
||||
│ ├── __main__.py # CLI (argparse: --config, --once, --version); Exit 0/1/2
|
||||
│ ├── config.py # TOML-Loader, Dataclasses, ConfigError
|
||||
│ ├── service.py # HotfolderService (watchdog + ThreadPool), Preflight, Zähler
|
||||
│ ├── __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 (95 Tests, ocrmypdf wird gemockt)
|
||||
├── tests/ # pytest-Suite (135 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)
|
||||
│ ├── 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 # Update aus Repo
|
||||
├── requirements.txt
|
||||
├── update.sh # Updater (--help, --rebuild-venv), ~860 Zeilen
|
||||
├── requirements.txt # feste Pins (ocrmypdf 16.x!)
|
||||
├── VERSION
|
||||
├── CHANGELOG.md
|
||||
└── README.md
|
||||
├── README.md
|
||||
└── AI_AGENT_BRIEFING.md
|
||||
```
|
||||
|
||||
## 🔧 Stack
|
||||
@@ -56,6 +71,12 @@ pdf-ocr-hotfolder/
|
||||
| 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 |
|
||||
@@ -67,7 +88,7 @@ pdf-ocr-hotfolder/
|
||||
| `/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 |
|
||||
| `/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
|
||||
@@ -86,75 +107,87 @@ journalctl -u 'pdf-ocr-hotfolder@*' --since today # alle Instanzen, heute
|
||||
- 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
|
||||
## 🗂️ Instanz-Management (Kurzfassung)
|
||||
|
||||
`install.sh` ist gleichzeitig **Installer und Instanz-Manager**:
|
||||
`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:
|
||||
|
||||
- Erster Lauf: Basis-Install + erste Instanz anlegen (Pflicht)
|
||||
- Folgender Lauf: Basis-Install wird übersprungen (erkannt an `venv` + Template-Unit), bestehende Instanzen werden gelistet, weitere Instanzen können ergänzt werden
|
||||
- Eingaben pro Instanz (seit 0.5.0 fünf statt drei):
|
||||
1. Name (`[a-z0-9][a-z0-9-]*`)
|
||||
2. Basis-Pfad (default `/var/lib/pdf-ocr-hotfolder/<name>`)
|
||||
3. Service-User (default `pdfocr`)
|
||||
4. **OCR-Sprachen** (default `deu+eng`) — Format `^[a-z]{3}(_[A-Za-z]+)?(\+…)*$`,
|
||||
bei Unsinn wird erneut gefragt. Jeder Code wird gegen `tesseract --list-langs`
|
||||
geprüft; fehlt einer, bietet der Installer `tesseract-ocr-<code>` an
|
||||
(Unterstrich → Bindestrich, `chi_sim` → `tesseract-ocr-chi-sim`). Ablehnung
|
||||
oder fehlgeschlagene Installation → Warnung, dass OCR mit dieser Sprache
|
||||
**pro Datei** scheitert, und die Sprach-Abfrage beginnt von vorn (kein
|
||||
harter Abbruch). Ist `tesseract` nicht aufrufbar, wird die Prüfung
|
||||
übersprungen und die Eingabe unverändert übernommen.
|
||||
5. **Original nach erfolgreichem OCR archivieren?** (default **nein** →
|
||||
`original_on_success = "delete"`). Bei ja wird der Archiv-Pfad abgefragt
|
||||
(Vorschlag `$BASE/archive`, absoluter Pfad Pflicht), angelegt und auf
|
||||
`$SVC_USER:$SVC_GROUP` gechownt — innerhalb von `$BASE` erledigt das
|
||||
bestehende `chown -R` das schon, nur ein Archiv **außerhalb** bekommt ein
|
||||
eigenes `chown -R`.
|
||||
- **Sprachen sind bewusst instanz-lokal**, nicht global: ein Hotfolder
|
||||
`buchhaltung` läuft mit `deu`, ein Hotfolder `export` mit `deu+eng+fra`.
|
||||
`LANGS`/`ORIG_MODE`/`ARCHIVE_DIR` sind `local` in `create_instance()` — jeder
|
||||
Durchlauf fragt neu, `deu+eng` ist nur der vorgeschlagene Default. Die Liste
|
||||
gehört eng gehalten: jede zusätzliche Sprache kostet Laufzeit **und**
|
||||
Erkennungsqualität.
|
||||
- Basis-Install prüft zusätzlich die Ghostscript-Version und bietet auf Debian 12 bookworm-backports an; erkennt Container (`systemd-detect-virt --container`) und bietet das LXC-Drop-in an
|
||||
- `<instanz>.toml` wird aus `config.example.toml` per `sed` generiert. Substituiert
|
||||
werden die vier `[paths]`-Zeilen **sowie** (seit 0.5.0) `[ocr].languages`,
|
||||
- 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 (im Beispiel steht z.B.
|
||||
`"archive" : Original wird in archive_dir verschoben` als Kommentar);
|
||||
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; erst wenn das passt, nennt die Zusammenfassung Sprachen
|
||||
und Archiv-Verzeichnis.
|
||||
- Instanz wird sofort `enable --now` gestartet
|
||||
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).
|
||||
|
||||
Manuelles Löschen einer Instanz:
|
||||
```bash
|
||||
systemctl disable --now pdf-ocr-hotfolder@<name>
|
||||
rm /etc/pdf-ocr-hotfolder/<name>.toml
|
||||
rm -rf /etc/systemd/system/pdf-ocr-hotfolder@<name>.service.d
|
||||
systemctl daemon-reload
|
||||
# Datenverzeichnis /var/lib/pdf-ocr-hotfolder/<name> manuell aufräumen
|
||||
```
|
||||
## 🔄 Update-Verhalten (Kurzfassung)
|
||||
|
||||
## 🔄 Update-Verhalten
|
||||
Vollständig: [docs/UPDATE.md](docs/UPDATE.md). Für die Arbeit am Skript wichtig:
|
||||
|
||||
`update.sh`:
|
||||
1. Findet das Repo (eigenes Verzeichnis oder `/opt/pdf-ocr-hotfolder/.repo_path`)
|
||||
2. Ermittelt alle **aktiven** `pdf-ocr-hotfolder@*.service` Units und stoppt sie
|
||||
3. Backup nach `/var/backups/pdf-ocr-hotfolder/` (tar.gz, ohne venv/`__pycache__`)
|
||||
4. Kopiert Code + requirements + VERSION + config.example aus dem Repo
|
||||
5. `pip install --upgrade` im venv
|
||||
6. Aktualisiert Template-Unit + `daemon-reload`
|
||||
7. Setzt den Code-Eigentümer auf den User, dem `venv` gehört (default `pdfocr`)
|
||||
8. Startet alle zuvor aktiven Instanzen wieder, Exit 1 wenn eine nicht mehr hochkommt
|
||||
|
||||
Config-Dateien werden **nie** überschrieben. Das Repo muss erhalten bleiben — `update.sh` kopiert daraus.
|
||||
- `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)
|
||||
|
||||
Vollständiges Beispiel mit Kommentaren: `config.example.toml`. Sektionen:
|
||||
Key-für-Key-Referenz: [docs/INSTALLATION.md](docs/INSTALLATION.md#konfigurationsreferenz).
|
||||
Vollständiges Beispiel mit Kommentaren: `config.example.toml`.
|
||||
|
||||
| Sektion | Zweck |
|
||||
|---------|-------|
|
||||
@@ -168,7 +201,31 @@ Vollständiges Beispiel mit Kommentaren: `config.example.toml`. Sektionen:
|
||||
| `[notify.email]` | `enabled`, SMTP-Daten, `from_addr`, `to_addrs`, `on` = `always`/`errors`/`never` |
|
||||
| `[logging]` | `level` = DEBUG/INFO/WARNING/ERROR |
|
||||
|
||||
Unbekannte Keys in einer Sektion werden beim Laden **still verworfen** (`config.py` filtert gegen die Dataclass-Annotationen) — Tippfehler in Key-Namen fallen also nicht auf.
|
||||
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
|
||||
|
||||
@@ -176,24 +233,43 @@ Unbekannte Keys in einer Sektion werden beim Laden **still verworfen** (`config.
|
||||
1. `check_preflight()` — `tesseract` und `gs` müssen im PATH sein; ist `pdfa_level` gesetzt, wird zusätzlich die Ghostscript-Version gegen den 10.0.0–10.02.0-Bug geprüft
|
||||
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/` (beim Start greift `_scan_existing()` bereits liegende PDFs auf)
|
||||
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/`
|
||||
4. `ocrmypdf.ocr()` als **Library-Call** (kein Subprozess-Start pro PDF)
|
||||
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()`: `prefix`/`suffix`/`none` + `name_tag` — das harte `OCR_`-Präfix aus 0.1.0 ist nur noch der Default)
|
||||
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 (Stand 0.4.1):**
|
||||
**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/` |
|
||||
@@ -213,11 +289,14 @@ Der Service läuft in allen Fällen weiter (kein `exit 1` wie im alten Bash-Tool
|
||||
|
||||
## ⚠️ Fallstricke
|
||||
|
||||
- **Ghostscript 10.0.0–10.02.0 zerschießt OCR.** Das ist der Debian-12-Default. In Kombination aus `[ocr].pdfa_level` + `skip_text = true` blockiert ocrmypdf komplett (Issue #3). Deshalb ist `pdfa_level = ""` der sichere Default, und der Preflight bricht mit Exit 2 ab, wenn `pdfa_level` gesetzt **und** die GS-Version betroffen ist. Abhilfe: Ghostscript ≥ 10.02.1 aus bookworm-backports (der Installer bietet das an).
|
||||
- **`[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. 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.
|
||||
- **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.
|
||||
- **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).
|
||||
- **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`. Beim Debuggen nicht versehentlich in ein Ticket oder Log kopieren.
|
||||
- **Ghostscript 10.0.0–10.02.0 zerschießt OCR.** Das ist der Debian-12-Default. In Kombination aus `[ocr].pdfa_level` + `skip_text = true` blockiert ocrmypdf komplett (Issue #3). Deshalb ist `pdfa_level = ""` der sichere Default, der Preflight bricht mit Exit 2 ab, wenn `pdfa_level` gesetzt **und** die GS-Version betroffen ist, und `--check-config` warnt bei gesetztem `pdfa_level` grundsätzlich. Abhilfe: Ghostscript ≥ 10.02.1 aus bookworm-backports (der Installer bietet das an).
|
||||
- **`[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/<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](docs/UPDATE.md#grenzen-des-rollbacks).
|
||||
|
||||
## 🛠️ Entwicklung
|
||||
|
||||
@@ -233,17 +312,21 @@ python -m pdf_ocr_hotfolder --config /tmp/config.toml
|
||||
|
||||
Tests (aus dem Repo-Root, `pytest.ini` setzt `testpaths = tests`):
|
||||
```bash
|
||||
pytest # aktuell 95 Tests
|
||||
pytest # aktuell 135 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` — 95 Tests
|
||||
- [ ] 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()`).
|
||||
- [x] Tests (`pytest`) für `processor` und `uploaders` — 135 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 <error-file>`
|
||||
- [ ] Instanz-Löschung in `install.sh` statt als Handarbeit
|
||||
- [ ] Optional: S3/MinIO Upload-Target
|
||||
- [ ] Docker-Image für Setups ohne systemd
|
||||
|
||||
@@ -251,6 +334,7 @@ pytest # aktuell 95 Tests
|
||||
|
||||
- **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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user