508decffe3
Lets operators opt into keeping the local archive after upload (global keep_local_backups setting), pull a remote archive back down into the Proxmox dump directory for manual restore, and drop a locally-retained copy without touching the remote object. Consolidates deletion so retention, job-cascade delete, manual delete, and crash-recovery resume all clean up local copies alongside remote ones. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
from .db import db, utc_now
|
|
from .migrations import DEFAULT_SETTINGS, run_migrations
|
|
|
|
|
|
INT_KEYS = {"default_retention_value", "max_concurrent_backups"}
|
|
BOOL_KEYS = {"setup_complete", "keep_local_backups"}
|
|
|
|
|
|
def parse_value(key: str, value: str):
|
|
if key in INT_KEYS:
|
|
return int(value)
|
|
if key in BOOL_KEYS:
|
|
return value.lower() == "true"
|
|
if key == "allowed_cors_origins":
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
return value
|
|
|
|
|
|
def serialize_value(key: str, value) -> str:
|
|
if key in BOOL_KEYS:
|
|
return "true" if bool(value) else "false"
|
|
if key == "allowed_cors_origins":
|
|
if isinstance(value, list):
|
|
return ",".join(str(item).strip() for item in value if str(item).strip())
|
|
return str(value)
|
|
|
|
|
|
def _is_missing_settings_table(exc: sqlite3.OperationalError) -> bool:
|
|
return "no such table: settings" in str(exc).lower()
|
|
|
|
|
|
def get_settings() -> dict:
|
|
try:
|
|
with db() as conn:
|
|
rows = conn.execute("SELECT key, value FROM settings").fetchall()
|
|
except sqlite3.OperationalError as exc:
|
|
if not _is_missing_settings_table(exc):
|
|
raise
|
|
run_migrations()
|
|
with db() as conn:
|
|
rows = conn.execute("SELECT key, value FROM settings").fetchall()
|
|
raw = {row["key"]: row["value"] for row in rows}
|
|
for key, value in DEFAULT_SETTINGS.items():
|
|
raw.setdefault(key, value)
|
|
return {key: parse_value(key, value) for key, value in raw.items()}
|
|
|
|
|
|
def set_settings(values: dict) -> dict:
|
|
now = utc_now()
|
|
valid_keys = set(DEFAULT_SETTINGS)
|
|
unknown = set(values) - valid_keys
|
|
if unknown:
|
|
raise ValueError(f"Unknown settings: {', '.join(sorted(unknown))}")
|
|
try:
|
|
with db() as conn:
|
|
for key, value in values.items():
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO settings(key, value, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
""",
|
|
(key, serialize_value(key, value), now),
|
|
)
|
|
except sqlite3.OperationalError as exc:
|
|
if not _is_missing_settings_table(exc):
|
|
raise
|
|
run_migrations()
|
|
with db() as conn:
|
|
for key, value in values.items():
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO settings(key, value, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
""",
|
|
(key, serialize_value(key, value), now),
|
|
)
|
|
return get_settings()
|
|
|
|
|
|
def get_setting(key: str, default: str = "") -> str:
|
|
try:
|
|
with db() as conn:
|
|
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
|
|
except sqlite3.OperationalError as exc:
|
|
if not _is_missing_settings_table(exc):
|
|
raise
|
|
run_migrations()
|
|
with db() as conn:
|
|
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
|
|
if row:
|
|
return row["value"]
|
|
return default
|