fix profile-scoped backups and shared profile rules
This commit is contained in:
@@ -8,7 +8,7 @@ from typing import Any
|
||||
import psutil
|
||||
|
||||
from ..db import connect, default_user_id, utcnow
|
||||
from . import rtorrent
|
||||
from . import auth, rtorrent
|
||||
|
||||
DEFAULTS = {
|
||||
"enabled": False,
|
||||
@@ -140,14 +140,31 @@ def normalize(data: dict | None) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _row(user_id: int, profile_id: int) -> dict | None:
|
||||
def _row(user_id: int | None, profile_id: int) -> dict | None:
|
||||
with connect() as conn:
|
||||
return conn.execute(
|
||||
"SELECT * FROM download_plan_settings WHERE user_id=? AND profile_id=?",
|
||||
(user_id, profile_id),
|
||||
row = conn.execute(
|
||||
"SELECT * FROM download_plan_settings WHERE profile_id=? ORDER BY updated_at DESC, user_id ASC LIMIT 1",
|
||||
(profile_id,),
|
||||
).fetchone()
|
||||
if row:
|
||||
return row
|
||||
if user_id:
|
||||
return conn.execute(
|
||||
"SELECT * FROM download_plan_settings WHERE user_id=? AND profile_id=?",
|
||||
(user_id, profile_id),
|
||||
).fetchone()
|
||||
return None
|
||||
|
||||
|
||||
def _user_label(user_id: int | None) -> str:
|
||||
if not user_id:
|
||||
return "system"
|
||||
with connect() as conn:
|
||||
row = conn.execute("SELECT display_name, username, email FROM users WHERE id=?", (int(user_id),)).fetchone()
|
||||
if row:
|
||||
return str(row.get("display_name") or row.get("username") or row.get("email") or f"user {user_id}")
|
||||
return f"user {user_id}"
|
||||
|
||||
|
||||
|
||||
def _preference_row_for_disk_source(profile_id: int, user_id: int | None = None) -> dict | None:
|
||||
@@ -269,12 +286,13 @@ def get_settings(profile_id: int, user_id: int | None = None) -> dict:
|
||||
row = _row(user_id, profile_id)
|
||||
if not row:
|
||||
migrated = normalize({**DEFAULTS, **_legacy_disk_guard_defaults(int(profile_id), user_id)})
|
||||
return {**migrated, "profile_id": int(profile_id), "user_id": int(user_id)}
|
||||
return {**migrated, "profile_id": int(profile_id), "owner_user_id": int(user_id), "owner_name": _user_label(user_id)}
|
||||
try:
|
||||
data = json.loads(row.get("settings_json") or "{}")
|
||||
except Exception:
|
||||
data = {}
|
||||
settings = {**normalize(data), "profile_id": int(profile_id), "user_id": int(user_id), "updated_at": row.get("updated_at")}
|
||||
owner_user_id = int(row.get("user_id") or user_id)
|
||||
settings = {**normalize(data), "profile_id": int(profile_id), "owner_user_id": owner_user_id, "owner_name": _user_label(owner_user_id), "updated_at": row.get("updated_at")}
|
||||
runtime_override = _override_until(int(profile_id))
|
||||
if runtime_override:
|
||||
settings["manual_override_until"] = runtime_override
|
||||
@@ -283,18 +301,20 @@ def get_settings(profile_id: int, user_id: int | None = None) -> dict:
|
||||
|
||||
def save_settings(profile_id: int, data: dict, user_id: int | None = None) -> dict:
|
||||
user_id = user_id or default_user_id()
|
||||
if not auth.can_write_profile(int(profile_id), user_id):
|
||||
raise PermissionError("No write access to profile")
|
||||
settings = normalize(data)
|
||||
now = utcnow()
|
||||
with connect() as conn:
|
||||
conn.execute("DELETE FROM download_plan_settings WHERE profile_id=?", (int(profile_id),))
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO download_plan_settings(user_id, profile_id, settings_json, updated_at)
|
||||
VALUES(?,?,?,?)
|
||||
ON CONFLICT(user_id, profile_id) DO UPDATE SET settings_json=excluded.settings_json, updated_at=excluded.updated_at
|
||||
""",
|
||||
(user_id, profile_id, json.dumps(settings), now),
|
||||
)
|
||||
return {**settings, "profile_id": int(profile_id), "user_id": int(user_id), "updated_at": now}
|
||||
return {**settings, "profile_id": int(profile_id), "owner_user_id": int(user_id), "owner_name": _user_label(user_id), "updated_at": now}
|
||||
|
||||
|
||||
def _active_downloading_hashes(profile: dict) -> list[str]:
|
||||
@@ -445,9 +465,10 @@ def evaluate(profile: dict, settings: dict | None = None, now: datetime | None =
|
||||
|
||||
def enforce(profile: dict, force: bool = False, user_id: int | None = None) -> dict:
|
||||
profile_id = int(profile.get("id") or 0)
|
||||
user_id = user_id or int(profile.get("user_id") or default_user_id())
|
||||
# Note: Background planner runs without Flask session state, so settings are resolved with the profile owner.
|
||||
settings = get_settings(profile_id, user_id)
|
||||
settings = get_settings(profile_id, user_id or int(profile.get("user_id") or default_user_id()))
|
||||
user_id = int(settings.get("owner_user_id") or user_id or profile.get("user_id") or default_user_id())
|
||||
if not auth.can_write_profile(profile_id, user_id):
|
||||
return {"ok": True, "enabled": False, "profile_id": profile_id, "skipped": True, "reason": "planner owner has no write access", "history": history(profile_id, 20), "history_total": history_count(profile_id)}
|
||||
if not settings.get("enabled"):
|
||||
return {"ok": True, "enabled": False, "profile_id": profile_id, "history": history(profile_id, 20), "history_total": history_count(profile_id), "preview": preview(profile, user_id=user_id)}
|
||||
now = time.monotonic()
|
||||
@@ -505,8 +526,7 @@ def enforce(profile: dict, force: bool = False, user_id: int | None = None) -> d
|
||||
|
||||
def preview(profile: dict, user_id: int | None = None) -> dict:
|
||||
profile_id = int(profile.get("id") or 0)
|
||||
user_id = user_id or int(profile.get("user_id") or default_user_id())
|
||||
settings = get_settings(profile_id, user_id)
|
||||
settings = get_settings(profile_id, user_id or int(profile.get("user_id") or default_user_id()))
|
||||
decision = evaluate(profile, settings)
|
||||
return {
|
||||
"profile_id": profile_id,
|
||||
|
||||
Reference in New Issue
Block a user