Files
pdf-ocr-hotfolder/pdf_ocr_hotfolder/uploaders.py
T
techadmin 578472872e fix: Fehlerzaehlung, Upload-Fehler und ocr.timeout scharf (v0.4.0)
- [ocr].timeout wird als tesseract_timeout (pro Seite) an ocrmypdf
  durchgereicht; Default 1800 -> 300, 0 = ocrmypdf-Default
- Exceptions nach dem OCR zaehlen als Fehler, Datei wird nach error/ gerettet
- Fehlgeschlagene Uploads zaehlen als Fehler und loesen Fehler-Mail aus
- name_mode wird im Preflight geprueft, nicht erst pro Datei
- Fehlende [paths]-Sektion -> ConfigError mit klarer Meldung statt KeyError
- Stabilitaets-Timeout zaehlt als Fehler (--once liefert Exit 1)
- upload_folder nutzt shutil.copyfile statt read_bytes/write_bytes
- OcrConfig.pdfa_level Default "2" -> "" (Ghostscript-Bug, Issue #3)
- 35 neue Tests (92 gesamt), pytest.ini
- AI_AGENT_BRIEFING.md auf Stand 0.4.0 gebracht

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 21:01:38 +02:00

108 lines
3.6 KiB
Python

"""Upload-Ziele: lokaler Ordner, Nextcloud (WebDAV), SFTP. Plus E-Mail-Notify."""
from __future__ import annotations
import logging
import shutil
import smtplib
import ssl
from email.message import EmailMessage
from pathlib import Path
from urllib.parse import quote
import paramiko
import requests
from .config import EmailNotify, FolderUpload, NextcloudUpload, SftpUpload
log = logging.getLogger(__name__)
def upload_folder(pdf: Path, cfg: FolderUpload, default_target: Path) -> bool:
if not cfg.enabled:
return True
target = Path(cfg.target) if cfg.target else default_target
target.mkdir(parents=True, exist_ok=True)
dest = target / pdf.name
try:
if pdf.resolve() == dest.resolve():
return True
# copyfile statt read_bytes/write_bytes: große PDFs nicht komplett
# in den Speicher laden
shutil.copyfile(pdf, dest)
log.info("Folder upload OK: %s", dest)
return True
except OSError as e:
log.error("Folder upload failed: %s", e)
return False
def upload_nextcloud(pdf: Path, cfg: NextcloudUpload) -> bool:
if not cfg.enabled:
return True
base = cfg.url.rstrip("/")
remote = "/".join(quote(part) for part in cfg.remote_path.strip("/").split("/") if part)
url = f"{base}/remote.php/dav/files/{quote(cfg.username)}/{remote}/{quote(pdf.name)}"
try:
with pdf.open("rb") as f:
r = requests.put(url, data=f, auth=(cfg.username, cfg.password),
verify=cfg.verify_ssl, timeout=300)
if r.status_code in (200, 201, 204):
log.info("Nextcloud upload OK: %s", pdf.name)
return True
log.error("Nextcloud upload HTTP %s: %s", r.status_code, r.text[:200])
return False
except requests.RequestException as e:
log.error("Nextcloud upload failed: %s", e)
return False
def upload_sftp(pdf: Path, cfg: SftpUpload) -> bool:
if not cfg.enabled:
return True
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
connect_kwargs: dict = {
"hostname": cfg.host, "port": cfg.port, "username": cfg.username,
"timeout": 30,
}
if cfg.key_file:
connect_kwargs["key_filename"] = cfg.key_file
if cfg.password:
connect_kwargs["password"] = cfg.password
client.connect(**connect_kwargs)
sftp = client.open_sftp()
try:
remote = f"{cfg.remote_path.rstrip('/')}/{pdf.name}"
sftp.put(str(pdf), remote)
log.info("SFTP upload OK: %s", remote)
return True
finally:
sftp.close()
client.close()
except (paramiko.SSHException, OSError) as e:
log.error("SFTP upload failed: %s", e)
return False
def notify_email(cfg: EmailNotify, subject: str, body: str, success: bool) -> None:
if not cfg.enabled or cfg.on == "never":
return
if cfg.on == "errors" and success:
return
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = cfg.from_addr
msg["To"] = ", ".join(cfg.to_addrs)
msg.set_content(body)
try:
with smtplib.SMTP(cfg.smtp_host, cfg.smtp_port, timeout=30) as s:
if cfg.use_starttls:
s.starttls(context=ssl.create_default_context())
if cfg.smtp_user:
s.login(cfg.smtp_user, cfg.smtp_password)
s.send_message(msg)
log.info("E-Mail-Notify gesendet: %s", subject)
except (smtplib.SMTPException, OSError) as e:
log.error("E-Mail-Notify fehlgeschlagen: %s", e)