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:
@@ -14,6 +14,10 @@ log = logging.getLogger(__name__)
|
||||
# Erlaubte Werte für [output].name_mode — wird auch vom Preflight geprüft
|
||||
VALID_NAME_MODES = ("prefix", "suffix", "none")
|
||||
|
||||
# Präfix der Zwischendatei, in die ocrmypdf schreibt. Bleibt sie nach einem
|
||||
# harten Stopp in working/ liegen, ist sie ein unvollständiges Fragment.
|
||||
OCR_TEMP_PREFIX = "__ocr_"
|
||||
|
||||
|
||||
def build_output_name(src_name: str, mode: str, tag: str) -> str:
|
||||
"""Erzeugt den Ziel-Dateinamen für ein OCR-PDF.
|
||||
@@ -114,13 +118,29 @@ def process_pdf(
|
||||
"""Verarbeitet eine einzelne PDF: move→OCR→validate→outgoing/error."""
|
||||
out_name = build_output_name(src.name, output_cfg.name_mode, output_cfg.name_tag)
|
||||
work_src = working_dir / src.name
|
||||
work_out = working_dir / f"__ocr_{out_name}" # Temp-Name, damit er != src.name ist
|
||||
work_out = working_dir / f"{OCR_TEMP_PREFIX}{out_name}" # Temp-Name, damit er != src.name ist
|
||||
final_out = outgoing_dir / out_name
|
||||
|
||||
try:
|
||||
shutil.move(str(src), str(work_src))
|
||||
except OSError as e:
|
||||
return ProcessResult(src, final_out, False, f"move to working failed: {e}")
|
||||
if _is_same_file(src, work_src):
|
||||
# Wiederaufnahme: die Datei liegt bereits in working/, weil ein
|
||||
# früherer Lauf hart abgebrochen wurde. Kein zweiter Move — der würde
|
||||
# die Datei bestenfalls auf sich selbst schieben.
|
||||
log.warning("Wiederaufnahme aus %s: %s wird erneut per OCR verarbeitet",
|
||||
working_dir, src.name)
|
||||
elif work_src.exists():
|
||||
# Gleicher Dateiname, andere Datei: ein Move würde den laufenden bzw.
|
||||
# wiederaufgenommenen Vorgang in working/ stillschweigend überschreiben.
|
||||
return ProcessResult(
|
||||
src, final_out, False,
|
||||
f"in {working_dir} liegt bereits eine andere Datei namens "
|
||||
f"{src.name} — Original bleibt in {src.parent} liegen und wird "
|
||||
"beim nächsten Lauf erneut versucht",
|
||||
)
|
||||
else:
|
||||
try:
|
||||
shutil.move(str(src), str(work_src))
|
||||
except OSError as e:
|
||||
return ProcessResult(src, final_out, False, f"move to working failed: {e}")
|
||||
|
||||
try:
|
||||
run_ocr(work_src, work_out, ocr_cfg)
|
||||
@@ -153,6 +173,14 @@ def process_pdf(
|
||||
return ProcessResult(src, final_out, True, verapdf_passed=vera_ok)
|
||||
|
||||
|
||||
def _is_same_file(a: Path, b: Path) -> bool:
|
||||
"""Zeigen beide Pfade auf dieselbe Datei? (verträgt fehlende Dateien)"""
|
||||
try:
|
||||
return a.resolve() == b.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _dispose_original(work_src: Path, original_name: str, cfg: OutputConfig) -> None:
|
||||
"""Entsorgt das Original laut [output].original_on_success — löschen oder archivieren.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user