add auth support
This commit is contained in:
@@ -5,7 +5,7 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from . import rtorrent
|
||||
from . import rtorrent, auth
|
||||
from .preferences import get_profile
|
||||
from ..config import WORKERS
|
||||
from ..db import connect, utcnow, default_user_id
|
||||
@@ -23,7 +23,13 @@ def set_socketio(socketio):
|
||||
|
||||
|
||||
def _emit(name: str, payload: dict):
|
||||
if _socketio:
|
||||
if not _socketio:
|
||||
return
|
||||
profile_id = payload.get("profile_id")
|
||||
if auth.enabled() and profile_id:
|
||||
# Note: Job/socket events are sent only to clients joined to the affected profile room.
|
||||
_socketio.emit(name, payload, to=f"profile:{int(profile_id)}")
|
||||
else:
|
||||
_socketio.emit(name, payload)
|
||||
|
||||
|
||||
@@ -97,7 +103,7 @@ def _set_job(job_id: str, status: str, error: str = "", result: dict | None = No
|
||||
|
||||
|
||||
def enqueue(action_name: str, profile_id: int, payload: dict, user_id: int | None = None, max_attempts: int = 2) -> str:
|
||||
user_id = user_id or default_user_id()
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
job_id = uuid.uuid4().hex
|
||||
now = utcnow()
|
||||
with connect() as conn:
|
||||
@@ -130,7 +136,7 @@ def _run(job_id: str):
|
||||
profile = get_profile(int(job["profile_id"]), int(job["user_id"]))
|
||||
if not profile:
|
||||
_set_job(job_id, "failed", "rTorrent profile does not exist", finished=True)
|
||||
_emit("job_update", {"id": job_id, "status": "failed", "error": "profile not found"})
|
||||
_emit("job_update", {"id": job_id, "profile_id": job.get("profile_id"), "status": "failed", "error": "profile not found"})
|
||||
return
|
||||
profile_id = int(profile["id"])
|
||||
ordered_lock = None
|
||||
@@ -150,26 +156,26 @@ def _run(job_id: str):
|
||||
with connect() as conn:
|
||||
conn.execute("UPDATE jobs SET status='running', attempts=?, started_at=COALESCE(started_at, ?), updated_at=? WHERE id=?", (attempts, utcnow(), utcnow(), job_id))
|
||||
_emit("operation_started", {"job_id": job_id, "action": job["action"], "profile_id": profile["id"], "hashes": payload.get("hashes") or [], "hash_count": len(payload.get("hashes") or []), "bulk": len(payload.get("hashes") or []) > 1})
|
||||
_emit("job_update", {"id": job_id, "status": "running", "attempts": attempts})
|
||||
_emit("job_update", {"id": job_id, "profile_id": profile["id"], "status": "running", "attempts": attempts})
|
||||
result = _execute(profile, job["action"], payload)
|
||||
fresh = _job_row(job_id)
|
||||
# Awaryjne anulowanie: jeżeli użytkownik anuluje zadanie w trakcie pracy, wynik nie nadpisuje statusu cancelled.
|
||||
# Note: Emergency cancel keeps a cancelled job from being overwritten when work finishes later.
|
||||
if fresh and fresh["status"] == "cancelled":
|
||||
return
|
||||
_set_job(job_id, "done", result=result, finished=True)
|
||||
_emit("operation_finished", {"job_id": job_id, "action": job["action"], "profile_id": profile["id"], "hashes": payload.get("hashes") or [], "hash_count": len(payload.get("hashes") or []), "bulk": len(payload.get("hashes") or []) > 1, "result": result})
|
||||
_emit("job_update", {"id": job_id, "status": "done", "result": result})
|
||||
_emit("job_update", {"id": job_id, "profile_id": profile["id"], "status": "done", "result": result})
|
||||
except Exception as exc:
|
||||
fresh = _job_row(job_id) or {}
|
||||
attempts = int(fresh.get("attempts") or 1)
|
||||
max_attempts = int(fresh.get("max_attempts") or 2)
|
||||
# Awaryjne anulowanie: wyjątek z anulowanego zadania nie przywraca go do retry ani failed.
|
||||
# Note: Emergency cancel keeps an exception from a cancelled job from moving it back to retry or failed.
|
||||
if fresh and fresh.get("status") == "cancelled":
|
||||
return
|
||||
status = "pending" if attempts < max_attempts else "failed"
|
||||
_set_job(job_id, status, str(exc), finished=(status == "failed"))
|
||||
_emit("operation_failed", {"job_id": job_id, "action": job.get("action"), "profile_id": job.get("profile_id"), "hashes": payload.get("hashes") or [], "error": str(exc)})
|
||||
_emit("job_update", {"id": job_id, "status": status, "error": str(exc), "attempts": attempts})
|
||||
_emit("job_update", {"id": job_id, "profile_id": job.get("profile_id"), "status": status, "error": str(exc), "attempts": attempts})
|
||||
if status == "pending":
|
||||
_executor.submit(_run, job_id)
|
||||
finally:
|
||||
@@ -225,12 +231,23 @@ def _public_job(row) -> dict:
|
||||
return d
|
||||
|
||||
|
||||
def _job_scope_sql(writable: bool = False) -> tuple[str, tuple]:
|
||||
visible = auth.writable_profile_ids() if writable else auth.visible_profile_ids()
|
||||
if visible is None:
|
||||
return "", ()
|
||||
if not visible:
|
||||
return " WHERE 1=0", ()
|
||||
placeholders = ",".join("?" for _ in visible)
|
||||
return f" WHERE profile_id IN ({placeholders})", tuple(visible)
|
||||
|
||||
|
||||
def list_jobs(limit: int = 200, offset: int = 0):
|
||||
limit = max(1, min(int(limit or 50), 500))
|
||||
offset = max(0, int(offset or 0))
|
||||
where, params = _job_scope_sql()
|
||||
with connect() as conn:
|
||||
rows = conn.execute("SELECT * FROM jobs ORDER BY created_at DESC LIMIT ? OFFSET ?", (limit, offset)).fetchall()
|
||||
total = conn.execute("SELECT COUNT(*) AS n FROM jobs").fetchone()["n"]
|
||||
rows = conn.execute(f"SELECT * FROM jobs{where} ORDER BY created_at DESC LIMIT ? OFFSET ?", (*params, limit, offset)).fetchall()
|
||||
total = conn.execute(f"SELECT COUNT(*) AS n FROM jobs{where}", params).fetchone()["n"]
|
||||
return {"rows": [_public_job(r) for r in rows], "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@@ -238,24 +255,30 @@ def cancel_job(job_id: str) -> bool:
|
||||
row = _job_row(job_id)
|
||||
if not row or row["status"] not in {"pending", "running"}:
|
||||
return False
|
||||
# Note: Emergency cancel ma sens tylko dla niedokonczonych zadan; failed/done zostaja tylko do retry albo czyszczenia logow.
|
||||
# Note: Emergency cancel is useful only for unfinished jobs; failed/done entries stay available for retry or log cleanup.
|
||||
_set_job(job_id, "cancelled", finished=True)
|
||||
_emit("job_update", {"id": job_id, "status": "cancelled"})
|
||||
_emit("job_update", {"id": job_id, "profile_id": row.get("profile_id"), "status": "cancelled"})
|
||||
return True
|
||||
|
||||
|
||||
def clear_jobs() -> int:
|
||||
where, params = _job_scope_sql(writable=True)
|
||||
status_clause = "status NOT IN ('pending', 'running')"
|
||||
sql = f"DELETE FROM jobs{where} AND {status_clause}" if where else f"DELETE FROM jobs WHERE {status_clause}"
|
||||
with connect() as conn:
|
||||
cur = conn.execute("DELETE FROM jobs WHERE status NOT IN ('pending', 'running')")
|
||||
cur = conn.execute(sql, params)
|
||||
return int(cur.rowcount or 0)
|
||||
|
||||
|
||||
def emergency_clear_jobs() -> int:
|
||||
# Awaryjne czyszczenie: najpierw zamyka aktywne zadania jako cancelled, potem czyści całą listę job logów.
|
||||
# Note: Emergency cleanup first marks active jobs as cancelled, then clears the whole job log list.
|
||||
now = utcnow()
|
||||
where, params = _job_scope_sql(writable=True)
|
||||
status_clause = "status IN ('pending', 'running')"
|
||||
update_sql = f"UPDATE jobs SET status='cancelled', error='Emergency cancelled by user', finished_at=COALESCE(finished_at, ?), updated_at=?{where} AND {status_clause}" if where else "UPDATE jobs SET status='cancelled', error='Emergency cancelled by user', finished_at=COALESCE(finished_at, ?), updated_at=? WHERE status IN ('pending', 'running')"
|
||||
with connect() as conn:
|
||||
conn.execute("UPDATE jobs SET status='cancelled', error='Emergency cancelled by user', finished_at=COALESCE(finished_at, ?), updated_at=? WHERE status IN ('pending', 'running')", (now, now))
|
||||
cur = conn.execute("DELETE FROM jobs")
|
||||
conn.execute(update_sql, (now, now, *params) if where else (now, now))
|
||||
cur = conn.execute(f"DELETE FROM jobs{where}", params) if where else conn.execute("DELETE FROM jobs")
|
||||
deleted = int(cur.rowcount or 0)
|
||||
_emit("job_update", {"status": "cleared", "emergency": True})
|
||||
return deleted
|
||||
@@ -267,6 +290,6 @@ def retry_job(job_id: str) -> bool:
|
||||
return False
|
||||
with connect() as conn:
|
||||
conn.execute("UPDATE jobs SET status='pending', error='', finished_at=NULL, updated_at=? WHERE id=?", (utcnow(), job_id))
|
||||
_emit("job_update", {"id": job_id, "status": "pending"})
|
||||
_emit("job_update", {"id": job_id, "profile_id": row.get("profile_id"), "status": "pending"})
|
||||
_executor.submit(_run, job_id)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user