Files
MajoorWaldi-ComfyUI-Majoor-…/backend/settings.py
T

228 lines
9.8 KiB
Python

"""
Application settings persisted in the local metadata store.
"""
from __future__ import annotations
import threading
import asyncio
import time
from typing import Any, Mapping, Optional
from .shared import Result, get_logger
from .config import MEDIA_PROBE_BACKEND
from .utils import env_bool, parse_bool
logger = get_logger(__name__)
_PROBE_BACKEND_KEY = "media_probe_backend"
_NATIVE_EXTRACTION_KEY = "native_extraction_enabled"
_SETTINGS_VERSION_KEY = "__settings_version"
_VALID_PROBE_MODES = {"auto", "exiftool", "ffprobe", "both"}
_SECURITY_PREFS_INFO: Mapping[str, dict[str, bool | str]] = {
"safe_mode": {"env": "MAJOOR_SAFE_MODE", "default": False},
"allow_write": {"env": "MAJOOR_ALLOW_WRITE", "default": False},
"allow_delete": {"env": "MAJOOR_ALLOW_DELETE", "default": True},
"allow_rename": {"env": "MAJOOR_ALLOW_RENAME", "default": True},
"allow_open_in_folder": {"env": "MAJOOR_ALLOW_OPEN_IN_FOLDER", "default": True},
"allow_reset_index": {"env": "MAJOOR_ALLOW_RESET_INDEX", "default": True},
}
_SETTINGS_CACHE_TTL_S = 10.0
_VERSION_CACHE_TTL_S = 1.0
_MS_PER_S = 1000.0
class AppSettings:
"""
Simple settings manager backed by the metadata table.
"""
def __init__(self, db):
self._db = db
self._lock = asyncio.Lock()
self._cache: dict[str, str] = {}
self._cache_at: dict[str, float] = {}
self._cache_version: dict[str, int] = {}
self._cache_ttl_s = _SETTINGS_CACHE_TTL_S
self._version_cache_ttl_s = _VERSION_CACHE_TTL_S
self._version_cached: int = 0
self._version_cached_at: float = 0.0
self._default_probe_mode = MEDIA_PROBE_BACKEND
async def _read_setting(self, key: str) -> Optional[str]:
result = await self._db.aquery("SELECT value FROM metadata WHERE key = ?", (key,))
if not result.ok or not result.data:
return None
raw = result.data[0].get("value")
if isinstance(raw, str):
return raw.strip().lower()
return None
async def _write_setting(self, key: str, value: str) -> Result[str]:
return await self._db.aexecute(
"INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)",
(key, value),
)
async def _get_security_prefs_locked(self) -> dict[str, bool]:
output: dict[str, bool] = {}
for key, info in _SECURITY_PREFS_INFO.items():
raw = await self._read_setting(key)
if raw is not None:
output[key] = parse_bool(raw, bool(info.get("default", False)))
else:
default = bool(info.get("default", False))
env_var = str(info.get("env") or "")
output[key] = env_bool(env_var, default)
return output
async def get_security_prefs(self) -> dict[str, bool]:
async with self._lock:
return await self._get_security_prefs_locked()
async def set_security_prefs(self, prefs: Mapping[str, Any]) -> Result[dict[str, bool]]:
to_write: dict[str, bool] = {}
for key in _SECURITY_PREFS_INFO:
if key in prefs:
to_write[key] = parse_bool(prefs[key], False)
if not to_write:
return Result.Err("INVALID_INPUT", "No security settings provided")
async with self._lock:
for key, value in to_write.items():
res = await self._write_setting(key, "1" if value else "0")
if not res.ok:
return Result.Err("DB_ERROR", res.error or f"Failed to persist {key}")
bump = await self._bump_settings_version_locked()
if not bump.ok:
try:
logger.warning("Failed to bump settings version: %s", bump.error)
except Exception:
pass
return Result.Ok(await self._get_security_prefs_locked())
async def _read_settings_version(self) -> int:
try:
raw = await self._read_setting(_SETTINGS_VERSION_KEY)
n = int(str(raw or "0").strip() or "0")
return max(0, n)
except Exception:
return 0
async def _get_settings_version(self) -> int:
now = time.monotonic()
try:
ts = float(self._version_cached_at or 0.0)
except Exception:
ts = 0.0
if ts and (now - ts) < float(self._version_cache_ttl_s):
return int(self._version_cached or 0)
v = await self._read_settings_version()
self._version_cached = int(v or 0)
self._version_cached_at = now
return self._version_cached
async def _bump_settings_version_locked(self) -> Result[int]:
"""
Bump a monotonically increasing settings version in the DB.
Used to reduce stale caches in multi-instance deployments.
"""
try:
# Unix ms timestamp is monotonic enough for our purposes and works across processes.
v = int(time.time() * _MS_PER_S)
except Exception:
v = int(time.time())
res = await self._write_setting(_SETTINGS_VERSION_KEY, str(v))
if not res.ok:
return Result.Err("DB_ERROR", res.error or "Failed to bump settings version")
self._version_cached = v
self._version_cached_at = time.monotonic()
return Result.Ok(v)
async def get_probe_backend(self) -> str:
"""Return the configured media probe backend mode."""
async with self._lock:
current_version = await self._get_settings_version()
cached = self._cache.get(_PROBE_BACKEND_KEY)
if cached:
try:
ts = float(self._cache_at.get(_PROBE_BACKEND_KEY) or 0.0)
except Exception:
ts = 0.0
cached_ver = int(self._cache_version.get(_PROBE_BACKEND_KEY) or 0)
if cached_ver == int(current_version or 0) and ts and (time.monotonic() - ts) < self._cache_ttl_s:
return cached
mode = await self._read_setting(_PROBE_BACKEND_KEY)
if mode not in _VALID_PROBE_MODES:
mode = self._default_probe_mode
self._cache[_PROBE_BACKEND_KEY] = mode
self._cache_at[_PROBE_BACKEND_KEY] = time.monotonic()
self._cache_version[_PROBE_BACKEND_KEY] = int(current_version or 0)
return mode
async def set_probe_backend(self, mode: str) -> Result[str]:
"""Persist the media probe backend mode and bump the settings version."""
normalized = (mode or "").strip().lower()
if not normalized:
normalized = self._default_probe_mode
if normalized not in _VALID_PROBE_MODES:
return Result.Err("INVALID_INPUT", f"Invalid probe mode: {mode}")
async with self._lock:
result = await self._write_setting(_PROBE_BACKEND_KEY, normalized)
if result.ok:
bump = await self._bump_settings_version_locked()
if not bump.ok:
try:
logger.warning("Failed to bump settings version: %s", bump.error)
except Exception:
pass
self._cache[_PROBE_BACKEND_KEY] = normalized
self._cache_at[_PROBE_BACKEND_KEY] = time.monotonic()
self._cache_version[_PROBE_BACKEND_KEY] = int(bump.data or await self._get_settings_version() or 0)
logger.info("Media probe backend set to %s", normalized)
return Result.Ok(normalized)
return Result.Err("DB_ERROR", result.error or "Failed to persist probe backend")
async def get_native_extraction_enabled(self) -> bool:
"""Return whether native extraction (Pillow) is enabled."""
async with self._lock:
current_version = await self._get_settings_version()
cached = self._cache.get(_NATIVE_EXTRACTION_KEY)
if cached is not None:
try:
ts = float(self._cache_at.get(_NATIVE_EXTRACTION_KEY) or 0.0)
except Exception:
ts = 0.0
cached_ver = int(self._cache_version.get(_NATIVE_EXTRACTION_KEY) or 0)
if cached_ver == int(current_version or 0) and ts and (time.monotonic() - ts) < self._cache_ttl_s:
return parse_bool(cached, False)
# Default is False (disabled by default as requested)
val = await self._read_setting(_NATIVE_EXTRACTION_KEY)
is_enabled = parse_bool(val, False)
self._cache[_NATIVE_EXTRACTION_KEY] = str(is_enabled).lower()
self._cache_at[_NATIVE_EXTRACTION_KEY] = time.monotonic()
self._cache_version[_NATIVE_EXTRACTION_KEY] = int(current_version or 0)
return is_enabled
async def set_native_extraction_enabled(self, enabled: bool) -> Result[bool]:
"""Persist the native extraction setting and bump the settings version."""
val = "true" if enabled else "false"
async with self._lock:
result = await self._write_setting(_NATIVE_EXTRACTION_KEY, val)
if result.ok:
bump = await self._bump_settings_version_locked()
if not bump.ok:
try:
logger.warning("Failed to bump settings version: %s", bump.error)
except Exception:
pass
self._cache[_NATIVE_EXTRACTION_KEY] = val
self._cache_at[_NATIVE_EXTRACTION_KEY] = time.monotonic()
self._cache_version[_NATIVE_EXTRACTION_KEY] = int(bump.data or await self._get_settings_version() or 0)
logger.info("Native extraction set to %s", val)
return Result.Ok(enabled)
return Result.Err("DB_ERROR", result.error or "Failed to persist native extraction setting")