"""Zwischenspeicher für hochgeladene Anhänge. Beim Bestätigungs-Schritt werden Dateien temporär unter data/uploads/ abgelegt und erst beim verbindlichen Senden gelesen und (base64) an Zammad übergeben. Danach wird der Zwischenspeicher gelöscht. """ import base64 import json import re import shutil import uuid from datetime import datetime, timezone from pathlib import Path from . import config UPLOAD_DIR = config.DATA_DIR / "uploads" UPLOAD_DIR.mkdir(exist_ok=True) _TOKEN_RE = re.compile(r"^[0-9a-f]{32}$") def _dir(token: str) -> Path | None: if not token or not _TOKEN_RE.match(token): return None return UPLOAD_DIR / token def save_pending(files) -> tuple[str | None, list[str]]: """Hochgeladene Dateien (werkzeug FileStorage) zwischenspeichern. Gibt (token, Dateinamen) zurück; (None, []) wenn nichts dabei war.""" real = [f for f in files if f and getattr(f, "filename", "")] if not real: return None, [] token = uuid.uuid4().hex d = UPLOAD_DIR / token d.mkdir(parents=True, exist_ok=True) manifest, names = [], [] for i, f in enumerate(real): data = f.read() if not data: continue (d / str(i)).write_bytes(data) manifest.append({ "stored": str(i), "filename": f.filename, "mimetype": getattr(f, "mimetype", None) or "application/octet-stream", "size": len(data), }) names.append(f.filename) (d / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") return token, names def _manifest(token: str) -> list[dict]: d = _dir(token) if not d: return [] mf = d / "manifest.json" if not mf.exists(): return [] return json.loads(mf.read_text(encoding="utf-8")) def names(token: str) -> list[str]: return [m["filename"] for m in _manifest(token)] def load_attachments(token: str) -> list[dict]: """Zammad-Inline-Format: {filename, data(base64), mime-type}.""" d = _dir(token) out = [] for m in _manifest(token): p = d / m["stored"] if not p.exists(): continue out.append({ "filename": m["filename"], "data": base64.b64encode(p.read_bytes()).decode("ascii"), "mime-type": m["mimetype"], }) return out def discard(token: str) -> None: d = _dir(token) if d and d.exists(): shutil.rmtree(d, ignore_errors=True) def cleanup_stale(max_age_seconds: int = 3600) -> None: """Verwaiste Zwischenspeicher (abgebrochene Vorgänge) aufräumen.""" if not UPLOAD_DIR.exists(): return now = datetime.now(timezone.utc).timestamp() for d in UPLOAD_DIR.iterdir(): try: if d.is_dir() and (now - d.stat().st_mtime) > max_age_seconds: shutil.rmtree(d, ignore_errors=True) except OSError: pass