"""Plik file upload plugin.""" import json import os import re import threading from typing import Callable import httpx UPLOAD_TIMEOUT = httpx.Timeout(7200.0, connect=30.0) def _parse_ttl_seconds(ttl: str) -> int | None: if not ttl: return None m = re.match(r"^(\d+)d$", ttl) if m: return int(m.group(1)) * 86400 m = re.match(r"^(\d+)h$", ttl) if m: return int(m.group(1)) * 3600 m = re.match(r"^(\d+)$", ttl) if m: return int(m.group(1)) return None def _parse_response(body: str, plik_base: str, filename: str) -> dict: try: data = json.loads(body) except (json.JSONDecodeError, ValueError): data = None if isinstance(data, dict): if "error" in data: return {"error": f"Plik-Server: {data['error']}"} upload_id = data.get("id", "") if not upload_id: return {"error": f"Keine Upload-ID in Antwort: {body[:300]}"} browser_url = f"{plik_base}/#/?id={upload_id}" download_url = None files = data.get("files", []) if files: file_id = files[0].get("id", "") file_name = files[0].get("fileName", filename) if file_id: download_url = f"{plik_base}/file/{upload_id}/{file_id}/{file_name}" return {"browser_url": browser_url, "download_url": download_url} urls = re.findall(r'https?://\S+', body) if urls: download_url = urls[-1] prefix = f"{plik_base}/file/" if download_url.startswith(prefix): uid = download_url[len(prefix):].split("/")[0] if uid: return {"browser_url": f"{plik_base}/#/?id={uid}", "download_url": download_url} return {"browser_url": download_url, "download_url": download_url} return {"error": f"Antwort nicht lesbar: {body[:300]}"} def upload_file( file_path: str, plik_url: str, api_key: str = "", ttl: str = "30d", ) -> dict | None: """Upload a file to Plik. Returns dict with 'browser_url', 'download_url', or {'error': reason} on failure.""" if not plik_url: return {"error": "Keine Plik-URL konfiguriert"} if not os.path.isfile(file_path): return {"error": f"Datei nicht gefunden: {file_path}"} if os.path.getsize(file_path) == 0: return {"error": "Datei ist leer"} plik_base = plik_url.rstrip("/") filename = os.path.basename(file_path) headers = {} if api_key: headers["X-PlikToken"] = api_key form = {} ttl_secs = _parse_ttl_seconds(ttl) if ttl_secs is not None: form["ttl"] = str(ttl_secs) try: with open(file_path, "rb") as fh: resp = httpx.post( plik_base, headers=headers, data=form, files={"file": (filename, fh, "application/octet-stream")}, timeout=UPLOAD_TIMEOUT, ) if resp.status_code >= 400: return {"error": f"HTTP {resp.status_code}: {resp.text.strip()[:500]}"} return _parse_response(resp.text, plik_base, filename) except httpx.TimeoutException: return {"error": "Upload-Timeout (max. 2h)"} except Exception as e: return {"error": str(e)} class PlikUploader: def __init__(self, config, log_fn: Callable, notify_fn: Callable = None): self.config = config self._log = log_fn self._notify = notify_fn def upload_recording(self, files: list[str], job_name: str, segment_mode: bool, delete_after: bool, ntfy_enabled: bool, save_url_fn: Callable = None): plik_url = self.config.get("plik", "url", default="") if not plik_url: self._log("WARN", f"Plik-Upload übersprungen: Keine URL konfiguriert [{job_name}]", job_name) return api_key = self.config.get("plik", "api_key", default="") ttl = self.config.get("plik", "ttl", default="30d") files = [f for f in files if not os.path.basename(f).startswith(".")] def _do_upload(): try: self._upload_files(files, job_name, segment_mode, plik_url, api_key, ttl, delete_after, ntfy_enabled, save_url_fn) except Exception as e: self._log("ERROR", f"Plik-Upload Fehler: {e} [{job_name}]", job_name) threading.Thread(target=_do_upload, daemon=True).start() def _remove(self, path: str, job_name: str): try: os.remove(path) self._log("INFO", f"Lokale Datei gelöscht: {os.path.basename(path)} [{job_name}]", job_name) except OSError as e: self._log("ERROR", f"Lokale Datei konnte nicht gelöscht werden: {path} - {e} [{job_name}]", job_name) def _upload_files(self, files, job_name, segment_mode, plik_url, api_key, ttl, delete_after, ntfy_enabled, save_url_fn=None): files = [f for f in files if os.path.isfile(f) and os.path.getsize(f) > 0] if not files: self._log("WARN", f"Plik-Upload übersprungen: Datei fehlt oder leer [{job_name}]", job_name) return label = "Segment" if segment_mode else "Teil" multi = len(files) > 1 uploaded = 0 last_browser_url = None for fpath in files: fname = os.path.basename(fpath) prefix = f"Plik {label}-Upload" if multi else "Plik-Upload" self._log("INFO", f"{prefix}: {fname} [{job_name}]", job_name) result = upload_file(fpath, plik_url, api_key, ttl) if result and "error" in result: self._log("ERROR", f"{prefix} fehlgeschlagen: {fname} - {result['error']} [{job_name}]", job_name) elif result and result.get("browser_url"): last_browser_url = result["browser_url"] self._log("INFO", f"{prefix} abgeschlossen: {last_browser_url} [{job_name}]", job_name) if result.get("download_url"): self._log("INFO", f"Plik-Download: {result['download_url']} [{job_name}]", job_name) uploaded += 1 if delete_after: self._remove(fpath, job_name) else: self._log("ERROR", f"{prefix} fehlgeschlagen: {fname} (unbekannter Fehler) [{job_name}]", job_name) if uploaded > 0: if save_url_fn and last_browser_url: save_url_fn(last_browser_url) if self._notify: if multi: msg = f"{job_name}: {uploaded} {label}(e) hochgeladen" if last_browser_url: msg += f"\n{last_browser_url}" else: msg = f"{job_name}: {last_browser_url}" self._notify("upload", "Plik-Upload", msg, ntfy_enabled)