Files
pdf-ocr-hotfolder/pdf_ocr_hotfolder/uploaders.py
T
techadmin cd803a3dfe feat: stille Datenverlust-Pfade geschlossen, gemeinsame Shell-Lib (v0.7.0)
Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an
denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde.

Datenverlust:
- veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight
  geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" —
  Ergebnis nach error/, Original geloescht (Default delete). run_verapdf()
  trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung
  (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der
  Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das
  Original wird nicht entsorgt.
- Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload
  mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel
  daneben, mit Warnung; ProcessResult.output traegt den echten Pfad.

Robustheit:
- Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback.
- RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft
  nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen.
- Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu
  auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr.
- Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt
  still unter /opt zu landen.
- Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und
  Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail.
- Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet.
- Logging explizit nach stdout (die Doku versprach das schon).

Struktur:
- Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte
  venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung —
  die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte
  venv als gesund durchgewunken (nachgewiesen).
- install.sh warnt in Containern, wenn systemd-journald nicht laeuft.

Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify),
Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS,
AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte,
Exit-Code-Tabelle.

254 Tests gruen (vorher 152).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 00:59:17 +02:00

118 lines
4.1 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
from .processor import _collision_free_path
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
# Gleichnamige Datei im Ziel wurde bisher kommentarlos ersetzt.
# Derselbe Zeitstempel-Ausweg wie in outgoing/, archive/ und error/.
dest = _collision_free_path(dest)
if dest.name != pdf.name:
log.warning(
"In %s liegt bereits eine Datei %s — die Kopie wird als %s "
"abgelegt, damit die ältere nicht überschrieben wird",
target, pdf.name, dest.name,
)
# 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)