Initial commit: PDF OCR Hotfolder v0.1.0

Komplettes Rewrite des alten Bash-Tools `pdf-tool` in Python.
- ocrmypdf als Library, watchdog für Hotfolder, ThreadPool für Parallelität
- Upload-Targets: folder, Nextcloud (WebDAV), SFTP
- E-Mail-Notify, optional veraPDF
- Interaktiver Installer mit Service-User-Support (lokal + AD via SSSD)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:22:31 +02:00
commit 76c3a991df
16 changed files with 1261 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
"""Upload-Ziele: lokaler Ordner, Nextcloud (WebDAV), SFTP. Plus E-Mail-Notify."""
from __future__ import annotations
import logging
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
dest.write_bytes(pdf.read_bytes())
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)