181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
"""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)
|