cd803a3dfe
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>
118 lines
4.1 KiB
Python
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)
|