Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2062476252 | |||
| 8da0b7da1c |
+52
-12
@@ -1,8 +1,8 @@
|
|||||||
# AI Agent Briefing — PDF OCR Hotfolder
|
# AI Agent Briefing — PDF OCR Hotfolder
|
||||||
|
|
||||||
**Zuletzt aktualisiert:** 2026-09-22
|
**Zuletzt aktualisiert:** 2026-09-22
|
||||||
**Version:** 0.4.0
|
**Version:** 0.5.0
|
||||||
**Status:** Multi-Instanz-Betrieb, Preflight-Checks und Fehlerzählung vorhanden, Test-Suite grün (92 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.
|
**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.
|
||||||
|
|
||||||
## 🎯 Projektziel
|
## 🎯 Projektziel
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ pdf-ocr-hotfolder/
|
|||||||
│ ├── service.py # HotfolderService (watchdog + ThreadPool), Preflight, Zähler
|
│ ├── service.py # HotfolderService (watchdog + ThreadPool), Preflight, Zähler
|
||||||
│ ├── processor.py # ocrmypdf-Call, veraPDF, Ausgabename, Original-Entsorgung
|
│ ├── processor.py # ocrmypdf-Call, veraPDF, Ausgabename, Original-Entsorgung
|
||||||
│ └── uploaders.py # folder, nextcloud (WebDAV), sftp, E-Mail-Notify
|
│ └── uploaders.py # folder, nextcloud (WebDAV), sftp, E-Mail-Notify
|
||||||
├── tests/ # pytest-Suite (92 Tests, ocrmypdf wird gemockt)
|
├── tests/ # pytest-Suite (95 Tests, ocrmypdf wird gemockt)
|
||||||
│ ├── conftest.py # Fixtures tmp_config / dummy_pdf
|
│ ├── conftest.py # Fixtures tmp_config / dummy_pdf
|
||||||
│ ├── test_config_errors.py
|
│ ├── test_config_errors.py
|
||||||
│ ├── test_error_counting.py
|
│ ├── test_error_counting.py
|
||||||
@@ -67,9 +67,17 @@ 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@.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) |
|
| `/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/lib/pdf-ocr-hotfolder/<instanz>/{incoming,working,outgoing,error}/` | Daten pro Instanz |
|
||||||
| `/var/log/pdf-ocr-hotfolder/` | vom Installer angelegt; der Service selbst loggt nach stdout → journald |
|
|
||||||
| `/var/backups/pdf-ocr-hotfolder/` | Update-Backups |
|
| `/var/backups/pdf-ocr-hotfolder/` | Update-Backups |
|
||||||
|
|
||||||
|
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@<instanz> -f # eine Instanz mitlesen
|
||||||
|
journalctl -u 'pdf-ocr-hotfolder@*' --since today # alle Instanzen, heute
|
||||||
|
```
|
||||||
|
|
||||||
## 👤 Service-User
|
## 👤 Service-User
|
||||||
|
|
||||||
- Basis-Install legt Default-User `pdfocr` an (als System-User, falls nicht schon vorhanden)
|
- Basis-Install legt Default-User `pdfocr` an (als System-User, falls nicht schon vorhanden)
|
||||||
@@ -84,9 +92,41 @@ pdf-ocr-hotfolder/
|
|||||||
|
|
||||||
- Erster Lauf: Basis-Install + erste Instanz anlegen (Pflicht)
|
- 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
|
- 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: Name (`[a-z0-9][a-z0-9-]*`), Basis-Pfad (default `/var/lib/pdf-ocr-hotfolder/<name>`), Service-User
|
- 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
|
- 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` mit sed-substituierten Pfaden generiert
|
- `<instanz>.toml` wird aus `config.example.toml` per `sed` generiert. Substituiert
|
||||||
|
werden die vier `[paths]`-Zeilen **sowie** (seit 0.5.0) `[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
|
- Instanz wird sofort `enable --now` gestartet
|
||||||
|
|
||||||
Manuelles Löschen einer Instanz:
|
Manuelles Löschen einer Instanz:
|
||||||
@@ -142,20 +182,20 @@ Unbekannte Keys in einer Sektion werden beim Laden **still verworfen** (`config.
|
|||||||
2. `_wait_until_stable()` wartet, bis die Datei nicht mehr wächst (max. ~60s)
|
2. `_wait_until_stable()` wartet, bis die Datei nicht mehr wächst (max. ~60s)
|
||||||
3. Move nach `working/`
|
3. Move nach `working/`
|
||||||
4. `ocrmypdf.ocr()` als **Library-Call** (kein Subprozess-Start pro PDF)
|
4. `ocrmypdf.ocr()` als **Library-Call** (kein Subprozess-Start pro PDF)
|
||||||
5. Optional: veraPDF-Validierung (CLI-Subprozess)
|
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()`: `prefix`/`suffix`/`none` + `name_tag` — das harte `OCR_`-Präfix aus 0.1.0 ist nur noch der Default)
|
||||||
7. Original in `working/` wird laut `original_on_success` **gelöscht** oder nach `archive_dir` **archiviert** (Kollision → Timestamp-Suffix)
|
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)
|
8. Aktive Upload-Targets ausführen (folder/nextcloud/sftp)
|
||||||
9. E-Mail-Notify je nach `[notify.email].on`
|
9. E-Mail-Notify je nach `[notify.email].on`
|
||||||
|
|
||||||
**Fehlerbehandlung (Stand 0.4.0):**
|
**Fehlerbehandlung (Stand 0.4.1):**
|
||||||
|
|
||||||
| Fehlerfall | Zählt als Fehler | Wo liegt die Datei danach |
|
| 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 |
|
| 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 | — |
|
| Datei verschwindet vor der Verarbeitung | nein | — |
|
||||||
| OCR wirft (ocrmypdf) | ja | `error/` |
|
| OCR wirft (ocrmypdf) | ja | `error/` |
|
||||||
| veraPDF FAIL | ja | OCR-Ergebnis nach `error/`, Original wird gelöscht |
|
| 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/` |
|
| 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 |
|
| Mindestens ein Upload-Ziel schlägt fehl | ja | PDF bleibt **bewusst in `outgoing/`** (das OCR war ja erfolgreich), Fehler-Mail nennt die Ziele |
|
||||||
|
|
||||||
@@ -193,15 +233,15 @@ python -m pdf_ocr_hotfolder --config /tmp/config.toml
|
|||||||
|
|
||||||
Tests (aus dem Repo-Root, `pytest.ini` setzt `testpaths = tests`):
|
Tests (aus dem Repo-Root, `pytest.ini` setzt `testpaths = tests`):
|
||||||
```bash
|
```bash
|
||||||
pytest # aktuell 92 Tests
|
pytest # aktuell 95 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.
|
`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
|
## 📋 Roadmap / TODO
|
||||||
|
|
||||||
- [x] Tests (`pytest`) für `processor` und `uploaders` — 92 Tests
|
- [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, 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()`).
|
- [ ] 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()`).
|
||||||
- [ ] Prometheus-Metriken (verarbeitete PDFs, Fehlerquote, Laufzeit)
|
- [ ] Prometheus-Metriken (verarbeitete PDFs, Fehlerquote, Laufzeit)
|
||||||
- [ ] CLI-Subkommandos: `pdf-ocr-hotfolder reprocess <error-file>`
|
- [ ] CLI-Subkommandos: `pdf-ocr-hotfolder reprocess <error-file>`
|
||||||
- [ ] Optional: S3/MinIO Upload-Target
|
- [ ] Optional: S3/MinIO Upload-Target
|
||||||
|
|||||||
@@ -1,5 +1,75 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-09-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Der Installer weist einen Archiv-Pfad ab, der auf `incoming/`, `outgoing/`,
|
||||||
|
`working/` oder `error/` der Instanz zeigt — im Eingang wuerde das Original
|
||||||
|
sonst endlos neu aufgegriffen.
|
||||||
|
- **`install.sh` fragt beim Anlegen einer Instanz die OCR-Sprachen ab**
|
||||||
|
(`Tesseract-Sprachen [deu+eng]:`). Die Wahl gilt bewusst **pro Instanz** —
|
||||||
|
ein Hotfolder `buchhaltung` kann mit `deu` laufen, ein Hotfolder `export` mit
|
||||||
|
`deu+eng+fra`. Der Installer weist vorher darauf hin, dass jede zusaetzliche
|
||||||
|
Sprache Laufzeit **und** Erkennungsqualitaet kostet, die Liste also eng
|
||||||
|
gehalten werden sollte. Das Eingabeformat wird geprueft (Sprachcodes mit `+`
|
||||||
|
verbunden, `chi_sim` & Co. erlaubt); bei Unsinn wird erneut gefragt statt
|
||||||
|
abzubrechen.
|
||||||
|
- **Sprachpakete werden nachinstalliert.** Jeder eingegebene Code wird gegen
|
||||||
|
`tesseract --list-langs` geprueft. Fehlt eine Sprachdatei, bietet der
|
||||||
|
Installer das passende apt-Paket an (`tesseract-ocr-<code>`, Unterstrich wird
|
||||||
|
zum Bindestrich: `chi_sim` → `tesseract-ocr-chi-sim`). Lehnt der User ab oder
|
||||||
|
laesst sich das Paket nicht installieren, warnt der Installer, dass OCR mit
|
||||||
|
dieser Sprache **bei jeder Datei** scheitern wuerde, und fragt die Sprachen
|
||||||
|
erneut ab — so kann die Sprache einfach wieder rausgeworfen werden. Ist
|
||||||
|
`tesseract` nicht aufrufbar, wird die Pruefung uebersprungen und die Eingabe
|
||||||
|
unveraendert uebernommen.
|
||||||
|
- **Abfrage `Original nach erfolgreichem OCR archivieren? [j/N]:`** — Default
|
||||||
|
nein, also weiterhin `original_on_success = "delete"`. Bei ja wird der
|
||||||
|
Archiv-Pfad abgefragt (Vorschlag `<basis>/archive`), angelegt und auf den
|
||||||
|
Service-User gechownt; ein Archiv ausserhalb des Instanz-Basis-Pfads bekommt
|
||||||
|
ein eigenes `chown -R`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Die Instanz-Config wird weiterhin per `sed` aus `config.example.toml`
|
||||||
|
erzeugt, substituiert jetzt aber zusaetzlich `[ocr].languages`,
|
||||||
|
`[output].original_on_success` und `[output].archive_dir` — bisher waren das
|
||||||
|
die Beispiel-Defaults, `archive_dir` musste von Hand nachgetragen werden.
|
||||||
|
Die Ausdruecke sind am Zeilenanfang verankert (`^key[[:space:]]*=`), damit die
|
||||||
|
deutschen Kommentarzeilen ueber den Keys unangetastet bleiben, und
|
||||||
|
Pfad-Variablen laufen durch `sed_escape_repl()` (maskiert `\`, `&`, `|`) —
|
||||||
|
Pfade mit Sonderzeichen landen damit korrekt in der Config.
|
||||||
|
- Nach dem sed-Lauf liest der Installer die drei Keys aus der erzeugten Config
|
||||||
|
zurueck und vergleicht sie mit der Eingabe. Erst wenn das passt, nennt die
|
||||||
|
Abschluss-Zusammenfassung zusaetzlich die gewaehlten **Sprachen** und (bei
|
||||||
|
Archivierung) das **Archiv-Verzeichnis**; sonst gibt es eine Warnung.
|
||||||
|
|
||||||
|
## [0.4.1] - 2026-09-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **veraPDF-FAIL hat das Original immer gelöscht.** Schlug die PDF/A-Validierung
|
||||||
|
fehl, wanderte das OCR-Ergebnis nach `error/` und das Original wurde per
|
||||||
|
`unlink()` entfernt — unabhängig von `[output].original_on_success`. Wer
|
||||||
|
`archive` konfiguriert hatte, verlor die Datei also ausgerechnet im
|
||||||
|
Fehlerfall. Der FAIL-Pfad nutzt jetzt dieselbe `_dispose_original()`-Logik
|
||||||
|
wie der Erfolgsfall: `archive` legt das Original samt
|
||||||
|
Timestamp-Kollisionsschutz im `archive_dir` ab, `delete` verhält sich wie
|
||||||
|
bisher. Die Log-Meldung nennt jetzt beides — wohin das OCR-Ergebnis ging und
|
||||||
|
was mit dem Original passiert ist.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Das nie benutzte Logverzeichnis `/var/log/pdf-ocr-hotfolder/` wird nicht mehr
|
||||||
|
vom Installer angelegt und ist aus README und Briefing entfernt. Es hat nie
|
||||||
|
ein Logfile enthalten: `_setup_logging()` nutzt `logging.basicConfig()` ohne
|
||||||
|
FileHandler, der Dienst loggt nach stdout → journald. **journald ist damit die
|
||||||
|
einzige Log-Quelle** (`journalctl -u pdf-ocr-hotfolder@<instanz> -f`).
|
||||||
|
Weder Installer noch Updater fassen das Verzeichnis an: ein vorhandenes,
|
||||||
|
leeres `/var/log/pdf-ocr-hotfolder/` kann auf bestehenden Installationen
|
||||||
|
gefahrlos von Hand entfernt werden (`sudo rmdir /var/log/pdf-ocr-hotfolder`).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 3 neue Tests für den veraPDF-FAIL-Pfad (`delete`, `archive`,
|
||||||
|
Archiv-Namenskollision); veraPDF wird dabei gemockt. Suite jetzt 95 Tests.
|
||||||
|
|
||||||
## [0.4.0] - 2026-09-22
|
## [0.4.0] - 2026-09-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ sudo ./install.sh
|
|||||||
|
|
||||||
Der Installer:
|
Der Installer:
|
||||||
1. Installiert einmalig Code + venv + systemd-Template-Unit
|
1. Installiert einmalig Code + venv + systemd-Template-Unit
|
||||||
2. Fragt nach Instanz-Name, Basis-Pfad, Service-User
|
2. Fragt **pro Instanz** ab:
|
||||||
|
- Instanz-Name
|
||||||
|
- Basis-Pfad für die Daten
|
||||||
|
- Service-User
|
||||||
|
- **OCR-Sprachen** (Tesseract, Default `deu+eng`) — fehlende Sprachpakete
|
||||||
|
(`tesseract-ocr-<code>`) werden erkannt und auf Wunsch nachinstalliert
|
||||||
|
- **Original nach erfolgreichem OCR archivieren?** (Default nein = löschen;
|
||||||
|
bei ja zusätzlich der Archiv-Pfad, vorgeschlagen `<basis>/archive`)
|
||||||
3. Legt so viele Hotfolder-Instanzen an, wie du willst (`Weitere Instanz anlegen? [j/N]`)
|
3. Legt so viele Hotfolder-Instanzen an, wie du willst (`Weitere Instanz anlegen? [j/N]`)
|
||||||
|
|
||||||
Bei jedem erneuten Aufruf erkennt der Installer bestehende Instanzen und fragt nur nach neuen.
|
Bei jedem erneuten Aufruf erkennt der Installer bestehende Instanzen und fragt nur nach neuen.
|
||||||
@@ -47,6 +54,27 @@ Das Tool arbeitet komplett **instanzbasiert** über eine systemd Template-Unit `
|
|||||||
- eigene Datenverzeichnisse: `/var/lib/pdf-ocr-hotfolder/<name>/{incoming,working,outgoing,error}/`
|
- eigene Datenverzeichnisse: `/var/lib/pdf-ocr-hotfolder/<name>/{incoming,working,outgoing,error}/`
|
||||||
- eigene systemd-Unit: `pdf-ocr-hotfolder@<name>.service`
|
- eigene systemd-Unit: `pdf-ocr-hotfolder@<name>.service`
|
||||||
- optional eigenen Service-User (via Drop-in `/etc/systemd/system/pdf-ocr-hotfolder@<name>.service.d/user.conf`)
|
- optional eigenen Service-User (via Drop-in `/etc/systemd/system/pdf-ocr-hotfolder@<name>.service.d/user.conf`)
|
||||||
|
- **eigene OCR-Sprachen und eigene Original-Behandlung** (löschen oder archivieren)
|
||||||
|
|
||||||
|
### Sprachen pro Instanz
|
||||||
|
|
||||||
|
Die Tesseract-Sprachen werden bewusst **je Instanz** abgefragt, nicht global:
|
||||||
|
Hotfolder haben unterschiedliche Post. Ein Buchhaltungs-Hotfolder sieht nur
|
||||||
|
deutsche Belege, ein Export-Hotfolder internationale Korrespondenz:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# /etc/pdf-ocr-hotfolder/buchhaltung.toml
|
||||||
|
languages = "deu"
|
||||||
|
|
||||||
|
# /etc/pdf-ocr-hotfolder/export.toml
|
||||||
|
languages = "deu+eng+fra"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Die Liste so eng wie möglich halten.** Jede zusätzliche Sprache kostet
|
||||||
|
Laufzeit *und* Erkennungsqualität: Tesseract muss mehr Modelle gegeneinander
|
||||||
|
abwägen und verwechselt dabei Wörter, die in der einen Sprache eindeutig wären.
|
||||||
|
`deu+eng+fra` auf reinen Deutsch-Scans ist also kein Sicherheitsnetz, sondern
|
||||||
|
ein Rückschritt.
|
||||||
|
|
||||||
Beispiel für 3 Hotfolder:
|
Beispiel für 3 Hotfolder:
|
||||||
|
|
||||||
@@ -71,16 +99,19 @@ Manuell eine weitere Instanz anlegen geht auch — einfach `install.sh` erneut s
|
|||||||
| `/var/lib/pdf-ocr-hotfolder/<instanz>/working` | Arbeitsverzeichnis während OCR |
|
| `/var/lib/pdf-ocr-hotfolder/<instanz>/working` | Arbeitsverzeichnis während OCR |
|
||||||
| `/var/lib/pdf-ocr-hotfolder/<instanz>/outgoing` | Ausgang (fertige PDFs) |
|
| `/var/lib/pdf-ocr-hotfolder/<instanz>/outgoing` | Ausgang (fertige PDFs) |
|
||||||
| `/var/lib/pdf-ocr-hotfolder/<instanz>/error` | Fehlgeschlagene PDFs |
|
| `/var/lib/pdf-ocr-hotfolder/<instanz>/error` | Fehlgeschlagene PDFs |
|
||||||
| `/var/log/pdf-ocr-hotfolder/` | Logs (zusätzlich zu journald) |
|
|
||||||
| `/var/backups/pdf-ocr-hotfolder/` | Update-Backups |
|
| `/var/backups/pdf-ocr-hotfolder/` | Update-Backups |
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
Vollständiges Beispiel: [`config.example.toml`](config.example.toml). Wichtigste Sektionen:
|
Vollständiges Beispiel: [`config.example.toml`](config.example.toml). Wichtigste Sektionen:
|
||||||
|
|
||||||
|
Der Installer fragt `[ocr].languages`, `[output].original_on_success` und
|
||||||
|
`[output].archive_dir` pro Instanz ab und schreibt sie direkt in die
|
||||||
|
Instanz-Config — die Werte unten sind nur die Beispiel-Defaults.
|
||||||
|
|
||||||
### `[ocr]`
|
### `[ocr]`
|
||||||
```toml
|
```toml
|
||||||
languages = "deu+eng" # Tesseract-Sprachen
|
languages = "deu+eng" # Tesseract-Sprachen (Installer fragt pro Instanz)
|
||||||
jobs = 4 # Threads pro PDF
|
jobs = 4 # Threads pro PDF
|
||||||
skip_text = true # bereits OCR-haltige Seiten überspringen
|
skip_text = true # bereits OCR-haltige Seiten überspringen
|
||||||
pdfa_level = "" # "1", "2", "3" oder "" für reines PDF (Default "" wegen Ghostscript-Bug, s.u.)
|
pdfa_level = "" # "1", "2", "3" oder "" für reines PDF (Default "" wegen Ghostscript-Bug, s.u.)
|
||||||
@@ -101,6 +132,7 @@ name_tag = "OCR_"
|
|||||||
# Nach erfolgreichem OCR mit dem Original:
|
# Nach erfolgreichem OCR mit dem Original:
|
||||||
# "delete" → löschen
|
# "delete" → löschen
|
||||||
# "archive" → in archive_dir verschieben
|
# "archive" → in archive_dir verschieben
|
||||||
|
# Beides fragt der Installer beim Anlegen der Instanz ab:
|
||||||
original_on_success = "delete"
|
original_on_success = "delete"
|
||||||
archive_dir = "" # absoluter Pfad, Pflicht bei "archive"
|
archive_dir = "" # absoluter Pfad, Pflicht bei "archive"
|
||||||
```
|
```
|
||||||
@@ -148,6 +180,16 @@ sudo systemctl status 'pdf-ocr-hotfolder@*'
|
|||||||
sudo systemctl restart 'pdf-ocr-hotfolder@*'
|
sudo systemctl restart 'pdf-ocr-hotfolder@*'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Der Dienst schreibt **kein eigenes Logfile** — alles geht nach stdout und damit
|
||||||
|
ins journal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u pdf-ocr-hotfolder@<instanz> -f # eine Instanz mitlesen
|
||||||
|
journalctl -u 'pdf-ocr-hotfolder@*' --since today # alle Instanzen, heute
|
||||||
|
```
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -239,5 +281,5 @@ MIT — © Sonith UG
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 0.4.0
|
**Version:** 0.5.0
|
||||||
**Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
|
**Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
|
||||||
|
|||||||
+163
-7
@@ -26,7 +26,6 @@ fi
|
|||||||
INSTALL_DIR="/opt/pdf-ocr-hotfolder"
|
INSTALL_DIR="/opt/pdf-ocr-hotfolder"
|
||||||
CONFIG_DIR="/etc/pdf-ocr-hotfolder"
|
CONFIG_DIR="/etc/pdf-ocr-hotfolder"
|
||||||
DATA_ROOT="/var/lib/pdf-ocr-hotfolder"
|
DATA_ROOT="/var/lib/pdf-ocr-hotfolder"
|
||||||
LOG_DIR="/var/log/pdf-ocr-hotfolder"
|
|
||||||
SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service"
|
SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service"
|
||||||
DEFAULT_USER="pdfocr"
|
DEFAULT_USER="pdfocr"
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ install_base() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log_step "Verzeichnisse anlegen"
|
log_step "Verzeichnisse anlegen"
|
||||||
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$DATA_ROOT" "$LOG_DIR"
|
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$DATA_ROOT"
|
||||||
chown root:"$DEFAULT_USER" "$CONFIG_DIR"
|
chown root:"$DEFAULT_USER" "$CONFIG_DIR"
|
||||||
chmod 750 "$CONFIG_DIR"
|
chmod 750 "$CONFIG_DIR"
|
||||||
|
|
||||||
@@ -140,7 +139,7 @@ install_base() {
|
|||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
log_info "Template-Unit installiert ✓"
|
log_info "Template-Unit installiert ✓"
|
||||||
|
|
||||||
chown -R "$DEFAULT_USER":"$DEFAULT_USER" "$INSTALL_DIR" "$LOG_DIR"
|
chown -R "$DEFAULT_USER":"$DEFAULT_USER" "$INSTALL_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -170,6 +169,66 @@ show_existing_instances() {
|
|||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Liest den Wert eines Keys (erste Zuweisung am Zeilenanfang) aus einer Config
|
||||||
|
config_value() {
|
||||||
|
local file="$1" key="$2"
|
||||||
|
sed -n "s|^${key}[[:space:]]*=[[:space:]]*\"\(.*\)\"[[:space:]]*$|\1|p" "$file" | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Maskiert Sonderzeichen, damit ein Pfad gefahrlos in eine sed-Ersetzung darf
|
||||||
|
# (Trennzeichen '|', Rueckverweis '&', Backslash).
|
||||||
|
sed_escape_repl() {
|
||||||
|
printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prueft jeden Tesseract-Sprachcode gegen die installierten Sprachdateien und
|
||||||
|
# bietet fehlende Pakete zur Installation an.
|
||||||
|
# Rueckgabe: 0 = alle Sprachen verfuegbar (oder Pruefung nicht moeglich),
|
||||||
|
# 1 = mindestens eine Sprache fehlt weiterhin.
|
||||||
|
ensure_tesseract_langs() {
|
||||||
|
local langs="$1"
|
||||||
|
local raw installed code pkg answer rc=0
|
||||||
|
local -a codes
|
||||||
|
|
||||||
|
if ! command -v tesseract >/dev/null 2>&1; then
|
||||||
|
log_warn "tesseract ist nicht aufrufbar — Sprachpruefung wird uebersprungen."
|
||||||
|
log_warn "Eingabe '$langs' wird unveraendert uebernommen."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! raw="$(tesseract --list-langs 2>/dev/null)"; then
|
||||||
|
log_warn "'tesseract --list-langs' schlug fehl — Sprachpruefung wird uebersprungen."
|
||||||
|
log_warn "Eingabe '$langs' wird unveraendert uebernommen."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
installed="$(printf '%s\n' "$raw" | grep -vi '^List of available' || true)"
|
||||||
|
|
||||||
|
IFS='+' read -r -a codes <<< "$langs"
|
||||||
|
for code in "${codes[@]}"; do
|
||||||
|
[ -n "$code" ] || continue
|
||||||
|
if printf '%s\n' "$installed" | grep -qxF "$code"; then
|
||||||
|
log_info "Sprache '$code' ist installiert ✓"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
pkg="tesseract-ocr-${code//_/-}"
|
||||||
|
log_warn "Sprache '$code' ist nicht installiert (Paket: $pkg)."
|
||||||
|
read -r -p "Paket '$pkg' jetzt installieren? [J/n]: " answer
|
||||||
|
answer="${answer:-J}"
|
||||||
|
if [[ "$answer" =~ ^[JjYy]$ ]]; then
|
||||||
|
if ! apt-get install -y --no-install-recommends "$pkg"; then
|
||||||
|
log_error "Paket '$pkg' liess sich nicht installieren."
|
||||||
|
elif tesseract --list-langs 2>/dev/null | grep -qxF "$code"; then
|
||||||
|
log_info "Paket '$pkg' installiert ✓"
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
log_error "Paket '$pkg' ist da, aber tesseract kennt '$code' weiterhin nicht."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log_warn "Ohne die Sprachdatei '$code' scheitert das OCR bei JEDER Datei."
|
||||||
|
rc=1
|
||||||
|
done
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
create_instance() {
|
create_instance() {
|
||||||
echo
|
echo
|
||||||
read -r -p "Instanz-Name (nur a-z, 0-9, -): " INST
|
read -r -p "Instanz-Name (nur a-z, 0-9, -): " INST
|
||||||
@@ -207,20 +266,111 @@ create_instance() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- OCR-Sprachen ---
|
||||||
|
echo
|
||||||
|
log_info "Tesseract-Sprachen — gelten NUR fuer diese Instanz '$INST'."
|
||||||
|
log_info "Jede zusaetzliche Sprache kostet Laufzeit und verschlechtert zugleich"
|
||||||
|
log_info "die Erkennung — also so eng wie moeglich waehlen (z.B. nur 'deu')."
|
||||||
|
local LANGS
|
||||||
|
while true; do
|
||||||
|
read -r -p "Tesseract-Sprachen [deu+eng]: " LANGS
|
||||||
|
LANGS="${LANGS:-deu+eng}"
|
||||||
|
if [[ ! "$LANGS" =~ ^[a-z]{3}(_[A-Za-z]+)?(\+[a-z]{3}(_[A-Za-z]+)?)*$ ]]; then
|
||||||
|
log_error "Ungueltiges Format. Erwartet: Sprachcodes mit '+' verbunden,"
|
||||||
|
log_error "z.B. 'deu', 'deu+eng' oder 'chi_sim+eng'."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ensure_tesseract_langs "$LANGS"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
log_warn "Bitte Sprachen erneut angeben (fehlende Sprache einfach weglassen)."
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Original archivieren? ---
|
||||||
|
echo
|
||||||
|
local ORIG_MODE="delete"
|
||||||
|
local ARCHIVE_DIR=""
|
||||||
|
local ARCHIVE_ANS
|
||||||
|
read -r -p "Original nach erfolgreichem OCR archivieren? [j/N]: " ARCHIVE_ANS
|
||||||
|
ARCHIVE_ANS="${ARCHIVE_ANS:-N}"
|
||||||
|
if [[ "$ARCHIVE_ANS" =~ ^[JjYy]$ ]]; then
|
||||||
|
ORIG_MODE="archive"
|
||||||
|
local default_archive="$BASE/archive"
|
||||||
|
while true; do
|
||||||
|
read -r -p "Archiv-Verzeichnis [$default_archive]: " ARCHIVE_DIR
|
||||||
|
ARCHIVE_DIR="${ARCHIVE_DIR:-$default_archive}"
|
||||||
|
if [[ "$ARCHIVE_DIR" != /* ]]; then
|
||||||
|
log_error "Bitte einen absoluten Pfad angeben (beginnt mit '/')."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Das Archiv darf keines der Arbeitsverzeichnisse sein: im Eingang
|
||||||
|
# wuerde das Original endlos neu aufgegriffen, in den uebrigen
|
||||||
|
# kollidiert es mit der Verarbeitung.
|
||||||
|
case "${ARCHIVE_DIR%/}" in
|
||||||
|
"$BASE/incoming"|"$BASE/outgoing"|"$BASE/working"|"$BASE/error")
|
||||||
|
log_error "Das Archiv darf nicht incoming/outgoing/working/error sein."
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
break
|
||||||
|
done
|
||||||
|
else
|
||||||
|
log_info "Original wird nach erfolgreichem OCR geloescht (original_on_success = \"delete\")."
|
||||||
|
fi
|
||||||
|
|
||||||
log_info "Lege Datenverzeichnisse unter $BASE an..."
|
log_info "Lege Datenverzeichnisse unter $BASE an..."
|
||||||
mkdir -p "$BASE"/{incoming,outgoing,working,error}
|
mkdir -p "$BASE"/{incoming,outgoing,working,error}
|
||||||
|
if [ -n "$ARCHIVE_DIR" ]; then
|
||||||
|
mkdir -p "$ARCHIVE_DIR"
|
||||||
|
fi
|
||||||
chown -R "$SVC_USER":"$SVC_GROUP" "$BASE"
|
chown -R "$SVC_USER":"$SVC_GROUP" "$BASE"
|
||||||
|
# Innerhalb von $BASE erledigt das chown -R oben schon alles; nur ein Archiv
|
||||||
|
# ausserhalb braucht eigenes mkdir/chown.
|
||||||
|
if [ -n "$ARCHIVE_DIR" ] && [[ "$ARCHIVE_DIR" != "$BASE"/* ]] && [ "$ARCHIVE_DIR" != "$BASE" ]; then
|
||||||
|
chown -R "$SVC_USER":"$SVC_GROUP" "$ARCHIVE_DIR"
|
||||||
|
log_info "Archiv-Verzeichnis $ARCHIVE_DIR angelegt (liegt ausserhalb von $BASE)"
|
||||||
|
fi
|
||||||
|
|
||||||
log_info "Erstelle Config $CONFIG_DIR/$INST.toml..."
|
log_info "Erstelle Config $CONFIG_DIR/$INST.toml..."
|
||||||
|
# Verankerte Ausdruecke (Zeilenanfang + Key + '='), damit die deutschen
|
||||||
|
# Kommentarzeilen ueber den Keys unangetastet bleiben.
|
||||||
|
local ESC_BASE ESC_ARCHIVE ESC_LANGS
|
||||||
|
ESC_BASE="$(sed_escape_repl "$BASE")"
|
||||||
|
ESC_ARCHIVE="$(sed_escape_repl "$ARCHIVE_DIR")"
|
||||||
|
ESC_LANGS="$(sed_escape_repl "$LANGS")"
|
||||||
sed \
|
sed \
|
||||||
-e "s|/var/lib/pdf-ocr-hotfolder/incoming|$BASE/incoming|" \
|
-e "s|^incoming[[:space:]]*=.*|incoming = \"$ESC_BASE/incoming\"|" \
|
||||||
-e "s|/var/lib/pdf-ocr-hotfolder/outgoing|$BASE/outgoing|" \
|
-e "s|^outgoing[[:space:]]*=.*|outgoing = \"$ESC_BASE/outgoing\"|" \
|
||||||
-e "s|/var/lib/pdf-ocr-hotfolder/working|$BASE/working|" \
|
-e "s|^working[[:space:]]*=.*|working = \"$ESC_BASE/working\"|" \
|
||||||
-e "s|/var/lib/pdf-ocr-hotfolder/error|$BASE/error|" \
|
-e "s|^error[[:space:]]*=.*|error = \"$ESC_BASE/error\"|" \
|
||||||
|
-e "s|^languages[[:space:]]*=.*|languages = \"$ESC_LANGS\"|" \
|
||||||
|
-e "s|^original_on_success[[:space:]]*=.*|original_on_success = \"$ORIG_MODE\"|" \
|
||||||
|
-e "s|^archive_dir[[:space:]]*=.*|archive_dir = \"$ESC_ARCHIVE\"|" \
|
||||||
"$INSTALL_DIR/config.example.toml" > "$CONFIG_DIR/$INST.toml"
|
"$INSTALL_DIR/config.example.toml" > "$CONFIG_DIR/$INST.toml"
|
||||||
chown root:"$SVC_GROUP" "$CONFIG_DIR/$INST.toml"
|
chown root:"$SVC_GROUP" "$CONFIG_DIR/$INST.toml"
|
||||||
chmod 640 "$CONFIG_DIR/$INST.toml"
|
chmod 640 "$CONFIG_DIR/$INST.toml"
|
||||||
|
|
||||||
|
# Erzeugte Config gegenpruefen: tragen die drei Keys wirklich die Auswahl?
|
||||||
|
local CFG_OK=1 got key want
|
||||||
|
for key in languages original_on_success archive_dir; do
|
||||||
|
case "$key" in
|
||||||
|
languages) want="$LANGS" ;;
|
||||||
|
original_on_success) want="$ORIG_MODE" ;;
|
||||||
|
archive_dir) want="$ARCHIVE_DIR" ;;
|
||||||
|
esac
|
||||||
|
got="$(config_value "$CONFIG_DIR/$INST.toml" "$key")"
|
||||||
|
if [ "$got" != "$want" ]; then
|
||||||
|
log_error "Config-Pruefung: $key ist \"$got\", erwartet \"$want\""
|
||||||
|
CFG_OK=0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$CFG_OK" -eq 1 ]; then
|
||||||
|
log_info "Config-Pruefung ok ✓ (languages / original_on_success / archive_dir)"
|
||||||
|
else
|
||||||
|
log_warn "Bitte $CONFIG_DIR/$INST.toml von Hand nachziehen."
|
||||||
|
fi
|
||||||
|
|
||||||
# Drop-in für abweichenden Service-User
|
# Drop-in für abweichenden Service-User
|
||||||
if [ "$SVC_USER" != "$DEFAULT_USER" ]; then
|
if [ "$SVC_USER" != "$DEFAULT_USER" ]; then
|
||||||
local DROPIN_DIR="/etc/systemd/system/pdf-ocr-hotfolder@${INST}.service.d"
|
local DROPIN_DIR="/etc/systemd/system/pdf-ocr-hotfolder@${INST}.service.d"
|
||||||
@@ -247,6 +397,12 @@ EOF
|
|||||||
echo " Eingang: $BASE/incoming"
|
echo " Eingang: $BASE/incoming"
|
||||||
echo " Ausgang: $BASE/outgoing"
|
echo " Ausgang: $BASE/outgoing"
|
||||||
echo " User: $SVC_USER ($SVC_GROUP)"
|
echo " User: $SVC_USER ($SVC_GROUP)"
|
||||||
|
if [ "$CFG_OK" -eq 1 ]; then
|
||||||
|
echo " Sprachen: $LANGS"
|
||||||
|
if [ "$ORIG_MODE" = "archive" ]; then
|
||||||
|
echo " Archiv: $ARCHIVE_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen."""
|
"""PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen."""
|
||||||
|
|
||||||
__version__ = "0.4.0"
|
__version__ = "0.5.0"
|
||||||
|
|||||||
@@ -133,8 +133,17 @@ def process_pdf(
|
|||||||
if vera_cfg.enabled:
|
if vera_cfg.enabled:
|
||||||
vera_ok = run_verapdf(work_out, vera_cfg)
|
vera_ok = run_verapdf(work_out, vera_cfg)
|
||||||
if not vera_ok:
|
if not vera_ok:
|
||||||
|
# Das OCR-Ergebnis ist unbrauchbar und wandert nach error/. Das
|
||||||
|
# Original wird aber NICHT bedingungslos gelöscht: es folgt derselben
|
||||||
|
# [output].original_on_success-Regel wie im Erfolgsfall, sonst
|
||||||
|
# verliert man es ausgerechnet im Fehlerfall (archive!).
|
||||||
_move_to_error(work_out, error_dir)
|
_move_to_error(work_out, error_dir)
|
||||||
work_src.unlink(missing_ok=True)
|
_dispose_original(work_src, src.name, output_cfg)
|
||||||
|
log.error(
|
||||||
|
"veraPDF FAIL: %s — OCR-Ergebnis nach %s verschoben, Original %s",
|
||||||
|
src.name, error_dir,
|
||||||
|
"archiviert" if output_cfg.original_on_success == "archive" else "gelöscht",
|
||||||
|
)
|
||||||
return ProcessResult(src, final_out, False,
|
return ProcessResult(src, final_out, False,
|
||||||
"verapdf validation failed", verapdf_passed=False)
|
"verapdf validation failed", verapdf_passed=False)
|
||||||
|
|
||||||
@@ -145,7 +154,11 @@ def process_pdf(
|
|||||||
|
|
||||||
|
|
||||||
def _dispose_original(work_src: Path, original_name: str, cfg: OutputConfig) -> None:
|
def _dispose_original(work_src: Path, original_name: str, cfg: OutputConfig) -> None:
|
||||||
"""Entsorgt das Original nach erfolgreichem OCR — löschen oder archivieren."""
|
"""Entsorgt das Original laut [output].original_on_success — löschen oder archivieren.
|
||||||
|
|
||||||
|
Wird nach erfolgreichem OCR aufgerufen und ebenso, wenn veraPDF die
|
||||||
|
Validierung ablehnt: auch dann soll `archive` das Original erhalten.
|
||||||
|
"""
|
||||||
if not work_src.exists():
|
if not work_src.exists():
|
||||||
return
|
return
|
||||||
mode = cfg.original_on_success
|
mode = cfg.original_on_success
|
||||||
|
|||||||
@@ -244,3 +244,72 @@ def test_process_pdf_archive_name_collision(tmp_path: Path) -> None:
|
|||||||
archived = list(env["archive"].glob("scan_*.pdf"))
|
archived = list(env["archive"].glob("scan_*.pdf"))
|
||||||
assert len(archived) == 1
|
assert len(archived) == 1
|
||||||
assert archived[0].read_bytes() == b"%PDF-1.4 original\n"
|
assert archived[0].read_bytes() == b"%PDF-1.4 original\n"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- veraPDF FAIL: Original folgt original_on_success ----------------
|
||||||
|
|
||||||
|
def _run_with_vera_fail(env: dict, out_cfg: OutputConfig):
|
||||||
|
"""process_pdf mit gemocktem OCR und einem veraPDF, das FAIL meldet."""
|
||||||
|
with patch("pdf_ocr_hotfolder.processor.run_ocr", side_effect=_fake_ocr), \
|
||||||
|
patch("pdf_ocr_hotfolder.processor.run_verapdf", return_value=False):
|
||||||
|
return process_pdf(
|
||||||
|
src=env["src"],
|
||||||
|
working_dir=env["working"],
|
||||||
|
outgoing_dir=env["outgoing"],
|
||||||
|
error_dir=env["error"],
|
||||||
|
ocr_cfg=OcrConfig(),
|
||||||
|
vera_cfg=VeraPdfConfig(enabled=True),
|
||||||
|
output_cfg=out_cfg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_pdf_verapdf_fail_delete_removes_original(tmp_path: Path) -> None:
|
||||||
|
"""delete: Verhalten wie bisher — OCR-Ergebnis nach error/, Original weg."""
|
||||||
|
env = _prepare(tmp_path)
|
||||||
|
out_cfg = OutputConfig(name_mode="prefix", name_tag="OCR_",
|
||||||
|
original_on_success="delete")
|
||||||
|
result = _run_with_vera_fail(env, out_cfg)
|
||||||
|
|
||||||
|
assert not result.success
|
||||||
|
assert result.verapdf_passed is False
|
||||||
|
# OCR-Ergebnis liegt in error/
|
||||||
|
assert (env["error"] / "__ocr_OCR_scan.pdf").exists()
|
||||||
|
# Original ist weg
|
||||||
|
assert not env["src"].exists()
|
||||||
|
assert not (env["working"] / "scan.pdf").exists()
|
||||||
|
assert not (env["outgoing"] / "OCR_scan.pdf").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_pdf_verapdf_fail_archive_keeps_original(tmp_path: Path) -> None:
|
||||||
|
"""archive: das Original darf im Fehlerfall NICHT verloren gehen."""
|
||||||
|
env = _prepare(tmp_path)
|
||||||
|
out_cfg = OutputConfig(name_mode="prefix", name_tag="OCR_",
|
||||||
|
original_on_success="archive",
|
||||||
|
archive_dir=str(env["archive"]))
|
||||||
|
result = _run_with_vera_fail(env, out_cfg)
|
||||||
|
|
||||||
|
assert not result.success
|
||||||
|
assert result.verapdf_passed is False
|
||||||
|
# OCR-Ergebnis liegt in error/
|
||||||
|
assert (env["error"] / "__ocr_OCR_scan.pdf").exists()
|
||||||
|
# Original liegt unversehrt im Archiv
|
||||||
|
archived = env["archive"] / "scan.pdf"
|
||||||
|
assert archived.exists()
|
||||||
|
assert archived.read_bytes() == b"%PDF-1.4 original\n"
|
||||||
|
assert not (env["working"] / "scan.pdf").exists()
|
||||||
|
assert not (env["outgoing"] / "OCR_scan.pdf").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_pdf_verapdf_fail_archive_name_collision(tmp_path: Path) -> None:
|
||||||
|
"""Auch im veraPDF-FAIL-Pfad greift der Timestamp-Kollisionsschutz."""
|
||||||
|
env = _prepare(tmp_path)
|
||||||
|
(env["archive"] / "scan.pdf").write_bytes(b"old")
|
||||||
|
out_cfg = OutputConfig(name_mode="prefix", name_tag="OCR_",
|
||||||
|
original_on_success="archive",
|
||||||
|
archive_dir=str(env["archive"]))
|
||||||
|
_run_with_vera_fail(env, out_cfg)
|
||||||
|
|
||||||
|
assert (env["archive"] / "scan.pdf").read_bytes() == b"old"
|
||||||
|
archived = list(env["archive"].glob("scan_*.pdf"))
|
||||||
|
assert len(archived) == 1
|
||||||
|
assert archived[0].read_bytes() == b"%PDF-1.4 original\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user