changes in db
This commit is contained in:
@@ -15,7 +15,7 @@ APP_BACKUP_TABLES = [
|
||||
"rtorrent_config_overrides", "poller_settings", "app_settings", "download_plan_settings",
|
||||
]
|
||||
|
||||
# Note: Profile backups contain only the active profile context and current user's profile-scoped preferences.
|
||||
# Note: Profile backups contain active profile data. User-specific preferences remain scoped to the current user.
|
||||
PROFILE_BACKUP_TABLES = [
|
||||
"rtorrent_profiles", "profile_preferences", "disk_monitor_preferences", "labels", "ratio_groups",
|
||||
"rss_feeds", "rss_rules", "smart_queue_settings", "smart_queue_exclusions",
|
||||
@@ -28,12 +28,12 @@ PROFILE_TABLE_FILTERS = {
|
||||
"disk_monitor_preferences": "user_id=? AND profile_id=?",
|
||||
"labels": "user_id=? AND profile_id=?",
|
||||
"ratio_groups": "user_id=? AND profile_id=?",
|
||||
"rss_feeds": "user_id=? AND profile_id=?",
|
||||
"rss_rules": "user_id=? AND profile_id=?",
|
||||
"smart_queue_settings": "user_id=? AND profile_id=?",
|
||||
"smart_queue_exclusions": "user_id=? AND profile_id=?",
|
||||
"rss_feeds": "profile_id=?",
|
||||
"rss_rules": "profile_id=?",
|
||||
"smart_queue_settings": "profile_id=?",
|
||||
"smart_queue_exclusions": "profile_id=?",
|
||||
"automation_rules": "user_id=? AND profile_id=?",
|
||||
"rtorrent_config_overrides": "user_id=? AND profile_id=?",
|
||||
"rtorrent_config_overrides": "profile_id=?",
|
||||
"poller_settings": "profile_id=?",
|
||||
"download_plan_settings": "user_id=? AND profile_id=?",
|
||||
}
|
||||
@@ -76,6 +76,13 @@ def _loads(value: str) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _table_columns(conn, table: str) -> set[str]:
|
||||
try:
|
||||
return {str(row["name"]) for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def _table_rows(conn, table: str, where: str | None = None, params: tuple = ()) -> list[dict]:
|
||||
try:
|
||||
sql = f"SELECT * FROM {table}" + (f" WHERE {where}" if where else "")
|
||||
@@ -191,7 +198,10 @@ def restore_app_backup(backup_id: int, user_id: int | None = None) -> dict:
|
||||
rows = tables.get(table) or []
|
||||
if not rows:
|
||||
continue
|
||||
columns = list(rows[0].keys())
|
||||
available = _table_columns(conn, table)
|
||||
columns = [col for col in rows[0].keys() if col in available]
|
||||
if not columns:
|
||||
continue
|
||||
placeholders = ",".join("?" for _ in columns)
|
||||
conn.execute(f"DELETE FROM {table}")
|
||||
for row in rows:
|
||||
@@ -245,7 +255,10 @@ def restore_profile_backup(backup_id: int, target_profile_id: int, user_id: int
|
||||
count = 0
|
||||
for row in rows:
|
||||
clean = _rewrite_profile_row(table, dict(row), user_id, int(target_profile_id))
|
||||
columns = list(clean.keys())
|
||||
available = _table_columns(conn, table)
|
||||
columns = [col for col in clean.keys() if col in available]
|
||||
if not columns:
|
||||
continue
|
||||
placeholders = ",".join("?" for _ in columns)
|
||||
conn.execute(f"INSERT INTO {table}({','.join(columns)}) VALUES({placeholders})", [clean.get(col) for col in columns])
|
||||
count += 1
|
||||
@@ -274,15 +287,24 @@ def delete_backup(backup_id: int, user_id: int | None = None) -> dict:
|
||||
return {"deleted": backup_id}
|
||||
|
||||
|
||||
def _settings_row_key(user_id: int | None = None) -> str:
|
||||
return f"{AUTO_BACKUP_SETTINGS_KEY}:{user_id or auth.current_user_id() or default_user_id()}"
|
||||
def _settings_row_key(user_id: int | None = None, backup_type: str = "app", profile_id: int | None = None) -> str:
|
||||
uid = user_id or auth.current_user_id() or default_user_id()
|
||||
scope = "profile" if backup_type == "profile" else "app"
|
||||
if scope == "profile":
|
||||
return f"{AUTO_BACKUP_SETTINGS_KEY}:profile:{uid}:{int(profile_id or 0)}"
|
||||
return f"{AUTO_BACKUP_SETTINGS_KEY}:app:{uid}"
|
||||
|
||||
|
||||
def _latest_backup_created_at(user_id: int) -> str | None:
|
||||
def _latest_backup_created_at(user_id: int, backup_type: str = "app", profile_id: int | None = None) -> str | None:
|
||||
clauses = ["user_id=?", "COALESCE(backup_type,'app')=?"]
|
||||
params: list[object] = [user_id, backup_type]
|
||||
if backup_type == "profile":
|
||||
clauses.append("profile_id=?")
|
||||
params.append(int(profile_id or 0))
|
||||
with connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT created_at FROM app_backups WHERE user_id=? AND COALESCE(backup_type,'app')='app' ORDER BY created_at DESC, id DESC LIMIT 1",
|
||||
(user_id,),
|
||||
f"SELECT created_at FROM app_backups WHERE {' AND '.join(clauses)} ORDER BY created_at DESC, id DESC LIMIT 1",
|
||||
tuple(params),
|
||||
).fetchone()
|
||||
return str(row["created_at"] or "") if row and row.get("created_at") else None
|
||||
|
||||
@@ -302,20 +324,29 @@ def _preview_row(row: dict) -> dict:
|
||||
return output
|
||||
|
||||
|
||||
def get_auto_backup_settings(user_id: int | None = None) -> dict:
|
||||
key = _settings_row_key(user_id)
|
||||
def get_auto_backup_settings(user_id: int | None = None, backup_type: str = "app", profile_id: int | None = None) -> dict:
|
||||
key = _settings_row_key(user_id, backup_type, profile_id)
|
||||
with connect() as conn:
|
||||
row = conn.execute("SELECT value FROM app_settings WHERE key=?", (key,)).fetchone()
|
||||
settings = {**DEFAULT_AUTO_BACKUP_SETTINGS, **_loads(row.get("value") if row else "{}")}
|
||||
settings["enabled"] = bool(settings.get("enabled"))
|
||||
settings["interval_hours"] = max(1, int(settings.get("interval_hours") or 24))
|
||||
settings["retention_days"] = max(1, int(settings.get("retention_days") or 30))
|
||||
settings["backup_type"] = "profile" if backup_type == "profile" else "app"
|
||||
if backup_type == "profile":
|
||||
settings["profile_id"] = int(profile_id or 0)
|
||||
return settings
|
||||
|
||||
|
||||
def save_auto_backup_settings(data: dict, user_id: int | None = None) -> dict:
|
||||
_require_admin(user_id)
|
||||
current = get_auto_backup_settings(user_id)
|
||||
def save_auto_backup_settings(data: dict, user_id: int | None = None, backup_type: str = "app", profile_id: int | None = None) -> dict:
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
backup_type = "profile" if backup_type == "profile" else "app"
|
||||
if backup_type == "app":
|
||||
_require_admin(user_id)
|
||||
else:
|
||||
if not profile_id or not auth.can_access_profile(int(profile_id), user_id):
|
||||
raise PermissionError("No access to profile")
|
||||
current = get_auto_backup_settings(user_id, backup_type, profile_id)
|
||||
settings = {
|
||||
**current,
|
||||
"enabled": bool(data.get("enabled")),
|
||||
@@ -323,7 +354,7 @@ def save_auto_backup_settings(data: dict, user_id: int | None = None) -> dict:
|
||||
"retention_days": max(1, int(data.get("retention_days") or current["retention_days"])),
|
||||
"last_run_at": data.get("last_run_at", current.get("last_run_at")),
|
||||
}
|
||||
key = _settings_row_key(user_id)
|
||||
key = _settings_row_key(user_id, backup_type, profile_id)
|
||||
with connect() as conn:
|
||||
conn.execute("INSERT OR REPLACE INTO app_settings(key,value) VALUES(?,?)", (key, json.dumps(settings)))
|
||||
return settings
|
||||
@@ -350,39 +381,69 @@ def preview_backup(backup_id: int, user_id: int | None = None) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def prune_old_backups(user_id: int | None = None, retention_days: int = 30) -> int:
|
||||
def prune_old_backups(user_id: int | None = None, retention_days: int = 30, backup_type: str = "app", profile_id: int | None = None) -> int:
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
cutoff = (datetime.now(timezone.utc) - timedelta(days=max(1, int(retention_days)))).isoformat(timespec="seconds")
|
||||
clauses = ["user_id=?", "COALESCE(backup_type,'app')=?", "created_at<?"]
|
||||
params: list[object] = [user_id, backup_type, cutoff]
|
||||
if backup_type == "profile":
|
||||
clauses.append("profile_id=?")
|
||||
params.append(int(profile_id or 0))
|
||||
with connect() as conn:
|
||||
cur = conn.execute("DELETE FROM app_backups WHERE user_id=? AND COALESCE(backup_type,'app')='app' AND created_at<?", (user_id, cutoff))
|
||||
cur = conn.execute(f"DELETE FROM app_backups WHERE {' AND '.join(clauses)}", tuple(params))
|
||||
return int(cur.rowcount or 0)
|
||||
|
||||
|
||||
def maybe_create_automatic_backup(user_id: int | None = None) -> dict | None:
|
||||
user_id = user_id or default_user_id()
|
||||
if not _is_admin_user(user_id):
|
||||
return None
|
||||
settings = get_auto_backup_settings(user_id)
|
||||
if not settings.get("enabled"):
|
||||
return None
|
||||
def _should_run(settings: dict, last_value: str | None) -> bool:
|
||||
now = datetime.now(timezone.utc)
|
||||
last_value = settings.get("last_run_at") or _latest_backup_created_at(user_id)
|
||||
try:
|
||||
last = datetime.fromisoformat(str(last_value).replace("Z", "+00:00")) if last_value else None
|
||||
except Exception:
|
||||
last = None
|
||||
if last and now - last < timedelta(hours=settings["interval_hours"]):
|
||||
return not last or now - last >= timedelta(hours=settings["interval_hours"])
|
||||
|
||||
|
||||
def maybe_create_automatic_backup(user_id: int | None = None, backup_type: str = "app", profile_id: int | None = None) -> dict | None:
|
||||
user_id = user_id or default_user_id()
|
||||
backup_type = "profile" if backup_type == "profile" else "app"
|
||||
if backup_type == "app" and not _is_admin_user(user_id):
|
||||
return None
|
||||
if backup_type == "profile" and (not profile_id or not auth.can_access_profile(int(profile_id), user_id)):
|
||||
return None
|
||||
settings = get_auto_backup_settings(user_id, backup_type, profile_id)
|
||||
if not settings.get("enabled"):
|
||||
return None
|
||||
last_value = settings.get("last_run_at") or _latest_backup_created_at(user_id, backup_type, profile_id)
|
||||
if not _should_run(settings, last_value):
|
||||
if settings.get("last_run_at") != last_value:
|
||||
settings["last_run_at"] = last_value
|
||||
save_auto_backup_settings(settings, user_id)
|
||||
save_auto_backup_settings(settings, user_id, backup_type, profile_id)
|
||||
return None
|
||||
backup = create_app_backup(f"Automatic application backup {now.isoformat(timespec='seconds')}", user_id, automatic=True)
|
||||
now = datetime.now(timezone.utc)
|
||||
if backup_type == "profile":
|
||||
backup = create_profile_backup(f"Automatic profile backup {now.isoformat(timespec='seconds')}", int(profile_id or 0), user_id, automatic=True)
|
||||
else:
|
||||
backup = create_app_backup(f"Automatic application backup {now.isoformat(timespec='seconds')}", user_id, automatic=True)
|
||||
settings["last_run_at"] = backup.get("created_at") or now.isoformat(timespec="seconds")
|
||||
save_auto_backup_settings(settings, user_id)
|
||||
prune_old_backups(user_id, settings["retention_days"])
|
||||
save_auto_backup_settings(settings, user_id, backup_type, profile_id)
|
||||
prune_old_backups(user_id, settings["retention_days"], backup_type, profile_id)
|
||||
return backup
|
||||
|
||||
|
||||
def _profile_schedule_keys() -> list[tuple[int, int]]:
|
||||
prefix = f"{AUTO_BACKUP_SETTINGS_KEY}:profile:"
|
||||
keys: list[tuple[int, int]] = []
|
||||
with connect() as conn:
|
||||
rows = conn.execute("SELECT key FROM app_settings WHERE key LIKE ?", (prefix + "%",)).fetchall()
|
||||
for row in rows:
|
||||
parts = str(row.get("key") or "").split(":")
|
||||
try:
|
||||
keys.append((int(parts[-2]), int(parts[-1])))
|
||||
except Exception:
|
||||
continue
|
||||
return keys
|
||||
|
||||
|
||||
def start_scheduler() -> None:
|
||||
global _scheduler_started
|
||||
with _scheduler_lock:
|
||||
@@ -397,7 +458,9 @@ def start_scheduler() -> None:
|
||||
rows = conn.execute("SELECT id FROM users WHERE is_active=1 AND role='admin'").fetchall()
|
||||
user_ids = [int(row["id"]) for row in rows] or [default_user_id()]
|
||||
for uid in user_ids:
|
||||
maybe_create_automatic_backup(uid)
|
||||
maybe_create_automatic_backup(uid, "app")
|
||||
for uid, pid in _profile_schedule_keys():
|
||||
maybe_create_automatic_backup(uid, "profile", pid)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(300)
|
||||
|
||||
Reference in New Issue
Block a user