Sicherheits-Härtung und Bugfixes
This commit is contained in:
+28
-22
@@ -1,8 +1,12 @@
|
||||
"""NTFY push notification plugin."""
|
||||
|
||||
import subprocess
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger("plugins.ntfy")
|
||||
|
||||
EVENT_TAGS = {
|
||||
"start": "red_circle",
|
||||
"stop": "white_check_mark",
|
||||
@@ -11,6 +15,23 @@ EVENT_TAGS = {
|
||||
}
|
||||
|
||||
|
||||
def _post(url: str, title: str, message: str, token: str, priority: str, tags: str):
|
||||
params = {"title": title, "priority": priority or "default"}
|
||||
if tags:
|
||||
params["tags"] = tags
|
||||
headers = {"Content-Type": "text/plain; charset=utf-8"}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
try:
|
||||
resp = httpx.post(url, params=params, content=message.encode("utf-8"),
|
||||
headers=headers, timeout=10.0)
|
||||
if resp.status_code >= 400:
|
||||
logger.warning("NTFY-Benachrichtigung fehlgeschlagen: HTTP %s %s",
|
||||
resp.status_code, resp.text[:200])
|
||||
except Exception as e:
|
||||
logger.warning("NTFY-Benachrichtigung fehlgeschlagen: %s", e)
|
||||
|
||||
|
||||
def send_notification(
|
||||
url: str,
|
||||
title: str,
|
||||
@@ -21,26 +42,11 @@ def send_notification(
|
||||
):
|
||||
if not url:
|
||||
return
|
||||
|
||||
cmd = [
|
||||
"curl", "-s", "-o", "/dev/null", "--max-time", "10",
|
||||
"-H", f"Title: {title}",
|
||||
"-H", f"Priority: {priority}",
|
||||
]
|
||||
if tags:
|
||||
cmd += ["-H", f"Tags: {tags}"]
|
||||
cmd += ["-d", message]
|
||||
if token:
|
||||
cmd += ["-H", f"Authorization: Bearer {token}"]
|
||||
cmd.append(url)
|
||||
|
||||
def _send():
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=15, stdin=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=_send, daemon=True).start()
|
||||
threading.Thread(
|
||||
target=_post,
|
||||
args=(url, title, message, token, priority, tags),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
||||
class NtfyNotifier:
|
||||
@@ -53,7 +59,7 @@ class NtfyNotifier:
|
||||
url = self.config.get("ntfy", "url", default="")
|
||||
if not url:
|
||||
return False
|
||||
events = self.config.get("ntfy", "events", default="error")
|
||||
events = self.config.get("ntfy", "events", default="error") or ""
|
||||
return event in [e.strip() for e in events.split(",")]
|
||||
|
||||
def notify(self, event: str, title: str, message: str, job_ntfy_enabled: bool = False, priority: str = "default"):
|
||||
|
||||
+89
-96
@@ -3,10 +3,13 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
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:
|
||||
@@ -23,6 +26,41 @@ def _parse_ttl_seconds(ttl: str) -> int | None:
|
||||
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,
|
||||
@@ -40,58 +78,28 @@ def upload_file(
|
||||
plik_base = plik_url.rstrip("/")
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
cmd = ["curl", "-sS", "--fail-with-body", "--max-time", "7200"]
|
||||
headers = {}
|
||||
if api_key:
|
||||
cmd += ["-H", f"X-PlikToken: {api_key}"]
|
||||
|
||||
cmd += ["-F", f"file=@{file_path};filename={filename}"]
|
||||
headers["X-PlikToken"] = api_key
|
||||
|
||||
form = {}
|
||||
ttl_secs = _parse_ttl_seconds(ttl)
|
||||
if ttl_secs is not None:
|
||||
cmd += ["-F", f"ttl={ttl_secs}"]
|
||||
|
||||
cmd.append(plik_base)
|
||||
form["ttl"] = str(ttl_secs)
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=7200, stdin=subprocess.DEVNULL)
|
||||
if result.returncode != 0:
|
||||
err = result.stderr.strip() or result.stdout.strip() or f"curl exit code {result.returncode}"
|
||||
return {"error": f"curl-Fehler: {err[:500]}"}
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
|
||||
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: {result.stdout[: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}
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
urls = re.findall(r'https?://\S+', result.stdout)
|
||||
if urls:
|
||||
download_url = urls[-1]
|
||||
prefix = f"{plik_base}/file/"
|
||||
if download_url.startswith(prefix):
|
||||
rest = download_url[len(prefix):]
|
||||
uid = rest.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: {result.stdout[:300]}"}
|
||||
except subprocess.TimeoutExpired:
|
||||
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)}
|
||||
@@ -103,9 +111,8 @@ class PlikUploader:
|
||||
self._log = log_fn
|
||||
self._notify = notify_fn
|
||||
|
||||
def upload_recording(self, output_dir: str, output_file: str, job_name: str,
|
||||
segment_mode: bool, delete_after: bool, ntfy_enabled: bool,
|
||||
save_url_fn: Callable = None):
|
||||
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)
|
||||
@@ -113,75 +120,61 @@ class PlikUploader:
|
||||
|
||||
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:
|
||||
if segment_mode:
|
||||
self._upload_segments(output_dir, job_name, plik_url, api_key, ttl, delete_after, ntfy_enabled, save_url_fn)
|
||||
else:
|
||||
self._upload_single(output_file, job_name, plik_url, api_key, ttl, delete_after, ntfy_enabled, save_url_fn)
|
||||
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 _upload_single(self, file_path, job_name, plik_url, api_key, ttl, delete_after, ntfy_enabled, save_url_fn=None):
|
||||
if not os.path.isfile(file_path) or os.path.getsize(file_path) == 0:
|
||||
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
|
||||
|
||||
self._log("INFO", f"Plik-Upload: {os.path.basename(file_path)} [{job_name}]", job_name)
|
||||
result = upload_file(file_path, plik_url, api_key, ttl)
|
||||
|
||||
if result and "error" in result:
|
||||
self._log("ERROR", f"Plik-Upload fehlgeschlagen: {result['error']} [{job_name}]", job_name)
|
||||
elif result and result.get("browser_url"):
|
||||
self._log("INFO", f"Plik-Upload abgeschlossen: {result['browser_url']} [{job_name}]", job_name)
|
||||
if result.get("download_url"):
|
||||
self._log("INFO", f"Plik-Download: {result['download_url']} [{job_name}]", job_name)
|
||||
if save_url_fn:
|
||||
save_url_fn(result["browser_url"])
|
||||
if self._notify:
|
||||
self._notify("upload", "Plik-Upload", f"{job_name}: {result['browser_url']}", ntfy_enabled)
|
||||
if delete_after:
|
||||
os.remove(file_path)
|
||||
self._log("INFO", f"Lokale Datei gelöscht: {file_path} [{job_name}]", job_name)
|
||||
else:
|
||||
self._log("ERROR", f"Plik-Upload fehlgeschlagen (unbekannter Fehler) [{job_name}]", job_name)
|
||||
|
||||
def _upload_segments(self, output_dir, job_name, plik_url, api_key, ttl, delete_after, ntfy_enabled, save_url_fn=None):
|
||||
if not os.path.isdir(output_dir):
|
||||
return
|
||||
|
||||
files = sorted(f for f in os.listdir(output_dir) if not f.startswith(".") and os.path.isfile(os.path.join(output_dir, f)))
|
||||
if not files:
|
||||
return
|
||||
|
||||
label = "Segment" if segment_mode else "Teil"
|
||||
multi = len(files) > 1
|
||||
uploaded = 0
|
||||
last_browser_url = None
|
||||
for fname in files:
|
||||
fpath = os.path.join(output_dir, fname)
|
||||
if os.path.getsize(fpath) == 0:
|
||||
continue
|
||||
self._log("INFO", f"Plik Segment-Upload: {fname} [{job_name}]", job_name)
|
||||
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"Plik Segment-Upload fehlgeschlagen: {fname} - {result['error']} [{job_name}]", job_name)
|
||||
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"Plik Segment hochgeladen: {last_browser_url} [{job_name}]", job_name)
|
||||
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:
|
||||
os.remove(fpath)
|
||||
self._log("INFO", f"Segment gelöscht: {fname} [{job_name}]", job_name)
|
||||
self._remove(fpath, job_name)
|
||||
else:
|
||||
self._log("ERROR", f"Plik Segment-Upload fehlgeschlagen: {fname} (unbekannter Fehler) [{job_name}]", job_name)
|
||||
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:
|
||||
msg = f"{job_name}: {uploaded} Segment(e) hochgeladen"
|
||||
if last_browser_url:
|
||||
msg += f"\n{last_browser_url}"
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user