fixes and improvements
This commit is contained in:
+24
-28
@@ -55,36 +55,33 @@ Set environment variables directly in your shell or a wrapper script before runn
|
||||
For local `.env` development, you can still create one if you prefer, but it is optional.
|
||||
|
||||
```bash
|
||||
JELLYFIN_URL=https://jellyfin.example.com
|
||||
JELLYFIN_API_KEY=your-api-key
|
||||
JELLYFIN_USER_ID=
|
||||
|
||||
# Optional Jellyseerr enrichment for the Users tab
|
||||
JELLYSEERR_URL=https://requests.example.com
|
||||
JELLYSEERR_API_KEY=your-jellyseerr-api-key
|
||||
|
||||
# Optional backend logging level
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Optional SMTP settings for the Users -> message popup
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=your-smtp-username
|
||||
SMTP_PASSWORD=your-smtp-password
|
||||
SMTP_FROM_ADDRESS=no-reply@example.com
|
||||
SMTP_FROM_NAME=Manage
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_USE_SSL=false
|
||||
SMTP_TIMEOUT=30
|
||||
|
||||
# Jellyfin, Jellyseerr, and SSH targets are now configured per machine in the app's Settings tab.
|
||||
# The backend seeds a local machine automatically, so no global Jellyfin or SSH env vars are required.
|
||||
#
|
||||
# If you still want to keep a fallback SSH key available for legacy/manual use, provide a single key file path:
|
||||
SSH_KEY_HOST_PATH=/absolute/path/to/id_ed25519
|
||||
# The container-side secret path is fixed at /run/secrets/ssh_private_key.
|
||||
|
||||
# Authentik / OIDC
|
||||
AUTH_ENABLED=true
|
||||
OIDC_ISSUER_URL=https://authentik.example/application/o/media-library-viewer/
|
||||
OIDC_AUDIENCE=media-library-viewer
|
||||
OIDC_JWKS_URL=
|
||||
OIDC_CLOCK_SKEW_SECONDS=30
|
||||
|
||||
SSH_HOST=media-server.example.com
|
||||
SSH_USERNAME=username
|
||||
SSH_PORT=22
|
||||
# Host-side directory mounted into the backend container at /root/.ssh.
|
||||
SSH_KEY_HOST_DIR=/absolute/path/to/your/ssh-dir
|
||||
# Container-side path assembled by the app:
|
||||
SSH_KEY_DIRECTORY=/root/.ssh
|
||||
SSH_KEY_NAME=id_ed25519
|
||||
SSH_PASSWORD=
|
||||
|
||||
REMOTE_MEDIA_ROOT=/srv/media
|
||||
REMOTE_PATH_PREFIX=
|
||||
```
|
||||
|
||||
## Running
|
||||
@@ -113,12 +110,6 @@ The production compose file expects required environment variables to be supplie
|
||||
1. Export your runtime variables before launching Compose:
|
||||
|
||||
```bash
|
||||
export JELLYFIN_URL=https://jellyfin.example.com
|
||||
export JELLYFIN_API_KEY=your-api-key
|
||||
export SSH_HOST=media-server.example.com
|
||||
export SSH_USERNAME=username
|
||||
export SSH_KEY_HOST_DIR=$HOME/.ssh
|
||||
export SSH_KEY_NAME=id_ed25519
|
||||
export BACKEND_APP_HOST=manage.example.com
|
||||
export FRONTEND_APP_HOST=manage.example.com
|
||||
export CERT_RESOLVER=letsencrypt
|
||||
@@ -127,12 +118,17 @@ export VITE_OIDC_CLIENT_ID=manage
|
||||
export VITE_OIDC_REDIRECT_URI=https://manage.example.com/
|
||||
export VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/
|
||||
|
||||
# Optional: provide a single SSH private key file for remote machines configured later in Settings.
|
||||
# Compose mounts it as a Docker secret, so you do not need to mount your whole ~/.ssh directory.
|
||||
# If you prefer SSH agent forwarding, Paramiko will use SSH_AUTH_SOCK when you provide it.
|
||||
export SSH_KEY_HOST_PATH=$HOME/.ssh/id_ed25519
|
||||
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
2. After the API is running, open the app, go to **Settings**, and add machine entries:
|
||||
- **Local**: monitors the API host itself without SSH.
|
||||
- **SSH**: monitors another machine using a host, username, key directory, and key name.
|
||||
- **SSH**: monitors another machine using a host, username, and a key file path stored in the container.
|
||||
|
||||
3. Open **Monitoring** to see one section per configured machine. Each section uses its own collector state, disk path, metrics queries, and recent action history, which are populated automatically by the backend poller.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import logging
|
||||
import posixpath
|
||||
import shlex
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import paramiko
|
||||
@@ -41,6 +42,7 @@ class RemoteSSHClient:
|
||||
port: int = 22,
|
||||
key_filename: str | None = None,
|
||||
password: str | None = None,
|
||||
known_hosts_path: str | None = None,
|
||||
timeout: int = 20,
|
||||
):
|
||||
if not host or not username:
|
||||
@@ -50,19 +52,23 @@ class RemoteSSHClient:
|
||||
self.port = port
|
||||
self.key_filename = key_filename or None
|
||||
self.password = password or None
|
||||
self.known_hosts_path = known_hosts_path or None
|
||||
self.timeout = timeout
|
||||
self._client: paramiko.SSHClient | None = None
|
||||
|
||||
def connect(self) -> paramiko.SSHClient:
|
||||
"""Create or reuse the Paramiko connection.
|
||||
|
||||
Unknown host keys are rejected. Users should connect once manually with
|
||||
ssh so the server is present in known_hosts.
|
||||
Unknown host keys are rejected. The application can synthesize a managed
|
||||
known_hosts file under its cache directory so users do not need to mount
|
||||
their local SSH directory into the container.
|
||||
"""
|
||||
if self._client:
|
||||
return self._client
|
||||
client = paramiko.SSHClient()
|
||||
client.load_system_host_keys()
|
||||
if self.known_hosts_path and Path(self.known_hosts_path).is_file():
|
||||
client.load_host_keys(self.known_hosts_path)
|
||||
client.set_missing_host_key_policy(paramiko.RejectPolicy())
|
||||
client.connect(
|
||||
self.host,
|
||||
@@ -119,7 +125,6 @@ class RemoteSSHClient:
|
||||
was a source of file-browser confusion. Output is NUL-delimited before
|
||||
Python serializes it, making spaces in filenames safe.
|
||||
"""
|
||||
# JSON-ish output: type, size, mtime epoch, filename. Handles spaces/newlines reasonably via NUL boundaries.
|
||||
quoted = shlex.quote(path)
|
||||
not_dir_message = shlex.quote(f"Not a directory: {path}")
|
||||
command = (
|
||||
|
||||
@@ -21,12 +21,12 @@ logger = logging.getLogger(__name__)
|
||||
class Settings(BaseSettings):
|
||||
"""Flat application settings read from env vars / .env file."""
|
||||
|
||||
# Jellyfin
|
||||
# Jellyfin (legacy fallback only; machine settings are preferred)
|
||||
jellyfin_url: str = ""
|
||||
jellyfin_api_key: str = ""
|
||||
jellyfin_user_id: str = ""
|
||||
|
||||
# Jellyseerr (optional)
|
||||
# Jellyseerr (legacy fallback only; machine settings are preferred)
|
||||
jellyseerr_url: str = ""
|
||||
jellyseerr_api_key: str = ""
|
||||
|
||||
@@ -51,13 +51,15 @@ class Settings(BaseSettings):
|
||||
smtp_use_ssl: bool = False
|
||||
smtp_timeout: int = 30
|
||||
|
||||
# SSH
|
||||
# Legacy SSH fallback (new preferred path is machine-specific settings)
|
||||
ssh_host: str = ""
|
||||
ssh_username: str = ""
|
||||
ssh_port: int = 22
|
||||
ssh_key_directory: str = ""
|
||||
ssh_key_name: str = ""
|
||||
ssh_key_file: str = "/run/secrets/ssh_private_key"
|
||||
ssh_password: str = ""
|
||||
ssh_known_hosts_path: str = ""
|
||||
|
||||
# Monitoring poller
|
||||
monitoring_poll_interval_seconds: int = 300
|
||||
@@ -79,10 +81,16 @@ class Settings(BaseSettings):
|
||||
|
||||
@property
|
||||
def ssh_key_path(self) -> str:
|
||||
if self.ssh_key_file:
|
||||
return self.ssh_key_file
|
||||
if not self.ssh_key_directory or not self.ssh_key_name:
|
||||
return ""
|
||||
return str(Path(self.ssh_key_directory) / self.ssh_key_name)
|
||||
|
||||
@property
|
||||
def ssh_known_hosts_file(self) -> Path:
|
||||
return Path(self.ssh_known_hosts_path or ".cache/media_library_viewer/known_hosts")
|
||||
|
||||
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
|
||||
|
||||
|
||||
@@ -93,7 +101,6 @@ def _find_env_file() -> str | None:
|
||||
candidate = directory / ".env"
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
# Stop at repo root (has .git)
|
||||
if (directory / ".git").exists():
|
||||
break
|
||||
return None
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
"""Dependency injection for FastAPI.
|
||||
|
||||
Provides singleton-like access to SSH and Jellyfin clients via FastAPI's
|
||||
dependency system. Uses lru_cache so connections are reused across requests.
|
||||
Provides access to machine-specific Jellyfin/SSH clients via FastAPI's request
|
||||
context. The selected machine can be chosen with a ``machine_id`` query
|
||||
parameter; otherwise the backend falls back to the first enabled machine that
|
||||
matches the requested service.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||
from media_library_viewer_api.clients.jellyseerr import JellyseerrClient
|
||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||
from media_library_viewer_api.services.known_hosts import ensure_known_host
|
||||
from media_library_viewer_api.config import get_settings
|
||||
from media_library_viewer_api.services.mail_queue import MailQueue, get_mail_queue as _get_mail_queue
|
||||
from media_library_viewer_api.services.monitoring_poller import (
|
||||
@@ -23,17 +29,100 @@ from media_library_viewer_api.services.settings_store import SettingsStore, get_
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_jellyfin_client() -> JellyfinClient:
|
||||
"""Return a cached Jellyfin client."""
|
||||
def _request_machine_id(request: Request | None) -> str | None:
|
||||
if request is None:
|
||||
return None
|
||||
machine_id = request.query_params.get("machine_id")
|
||||
return machine_id or None
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _jellyfin_client_for(cache_key: tuple[str, str, str]) -> JellyfinClient:
|
||||
machine_id, url, api_key = cache_key
|
||||
logger.info("Creating Jellyfin client machine_id=%s url=%s", machine_id or "<default>", url.rstrip("/") or "<unset>")
|
||||
return JellyfinClient(url, api_key)
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _jellyseerr_client_for(cache_key: tuple[str, str]) -> JellyseerrClient | None:
|
||||
machine_id, url = cache_key
|
||||
if not url:
|
||||
return None
|
||||
settings = get_settings_store().get_machine_config(machine_id) if machine_id else None
|
||||
api_key = (settings or {}).get("jellyseerr_api_key") if settings else ""
|
||||
if not api_key:
|
||||
return None
|
||||
logger.info("Creating Jellyseerr client machine_id=%s url=%s", machine_id or "<default>", url.rstrip("/") or "<unset>")
|
||||
return JellyseerrClient(url, api_key)
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _ssh_client_for(cache_key: tuple[str, str, str, int, str, str | None, str | None]) -> RemoteSSHClient:
|
||||
machine_id, host, username, port, key_filename, password, known_hosts_path = cache_key
|
||||
logger.info(
|
||||
"Creating SSH client machine_id=%s host=%s user=%s port=%s key=%s password=%s",
|
||||
machine_id or "<default>",
|
||||
host or "<unset>",
|
||||
username or "<unset>",
|
||||
port,
|
||||
key_filename or "<unset>",
|
||||
"set" if password else "missing",
|
||||
)
|
||||
client = RemoteSSHClient(
|
||||
host=host,
|
||||
username=username,
|
||||
port=port,
|
||||
key_filename=key_filename or None,
|
||||
password=password or None,
|
||||
known_hosts_path=known_hosts_path or None,
|
||||
)
|
||||
try:
|
||||
client.connect()
|
||||
except Exception:
|
||||
logger.exception("Failed to establish SSH connection to %s", host or "<unset>")
|
||||
raise
|
||||
return client
|
||||
|
||||
|
||||
def _resolve_machine(service: str, request: Request | None = None) -> dict[str, Any] | None:
|
||||
store = get_settings_store()
|
||||
machine_id = _request_machine_id(request)
|
||||
if machine_id:
|
||||
machine = store.get_machine(machine_id)
|
||||
if machine and (service in machine.get("services", []) or service == "ssh"):
|
||||
return machine
|
||||
return machine
|
||||
if service == "jellyfin":
|
||||
machines = store.list_machines_for_service("jellyfin")
|
||||
elif service == "jellyseerr":
|
||||
machines = [m for m in store.list_machines_for_service("jellyfin") if m.get("jellyseerr_url")]
|
||||
elif service == "ssh":
|
||||
machines = store.list_machines_for_service("files") or store.list_machines_for_service("monitoring")
|
||||
else:
|
||||
machines = store.list_machines_for_service(service)
|
||||
return machines[0] if machines else None
|
||||
|
||||
|
||||
def get_jellyfin_client(request: Request = None) -> JellyfinClient:
|
||||
"""Return a Jellyfin client for the selected machine or legacy env fallback."""
|
||||
machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("jellyfin", request)
|
||||
if machine and machine.get("jellyfin_url") and machine.get("jellyfin_api_key"):
|
||||
cache_key = (machine["id"], machine["jellyfin_url"], machine.get("jellyfin_api_key") or "")
|
||||
return _jellyfin_client_for(cache_key)
|
||||
|
||||
settings = get_settings()
|
||||
logger.info("Creating Jellyfin client for %s", settings.jellyfin_url.rstrip("/") or "<unset>")
|
||||
return JellyfinClient(settings.jellyfin_url, settings.jellyfin_api_key)
|
||||
if not settings.jellyfin_url or not settings.jellyfin_api_key:
|
||||
raise RuntimeError("No Jellyfin machine is configured and JELLYFIN_URL/API_KEY are not set")
|
||||
logger.info("Falling back to legacy Jellyfin env settings")
|
||||
return _jellyfin_client_for(("legacy", settings.jellyfin_url, settings.jellyfin_api_key))
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_jellyseerr_client() -> JellyseerrClient | None:
|
||||
def get_jellyseerr_client(request: Request = None) -> JellyseerrClient | None:
|
||||
"""Return a cached Jellyseerr client when configured, otherwise None."""
|
||||
machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("jellyseerr", request)
|
||||
if machine and machine.get("jellyseerr_url") and machine.get("jellyseerr_api_key"):
|
||||
return JellyseerrClient(machine["jellyseerr_url"], machine.get("jellyseerr_api_key") or "")
|
||||
|
||||
settings = get_settings()
|
||||
if not settings.jellyseerr_url or not settings.jellyseerr_api_key:
|
||||
logger.info(
|
||||
@@ -42,16 +131,30 @@ def get_jellyseerr_client() -> JellyseerrClient | None:
|
||||
"set" if settings.jellyseerr_api_key else "missing",
|
||||
)
|
||||
return None
|
||||
logger.info("Creating Jellyseerr client for %s", settings.jellyseerr_url.rstrip("/") or "<unset>")
|
||||
logger.info("Falling back to legacy Jellyseerr env settings")
|
||||
return JellyseerrClient(settings.jellyseerr_url, settings.jellyseerr_api_key)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_ssh_client() -> RemoteSSHClient:
|
||||
"""Return a cached SSH client (connects on first use)."""
|
||||
def get_ssh_client(request: Request = None) -> RemoteSSHClient:
|
||||
"""Return a cached SSH client for the selected machine or legacy env fallback."""
|
||||
machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("ssh", request)
|
||||
if machine and machine.get("host") and machine.get("username") and machine.get("key_directory") and machine.get("key_name"):
|
||||
known_hosts_path = get_settings().ssh_known_hosts_file
|
||||
ensure_known_host(str(machine.get("host")), int(machine.get("port") or 22), known_hosts_path)
|
||||
cache_key = (
|
||||
machine["id"],
|
||||
machine["host"],
|
||||
machine["username"],
|
||||
int(machine.get("port") or 22),
|
||||
f"{machine.get('key_directory')}/{machine.get('key_name')}",
|
||||
machine.get("password") or None,
|
||||
str(known_hosts_path),
|
||||
)
|
||||
return _ssh_client_for(cache_key)
|
||||
|
||||
settings = get_settings()
|
||||
logger.info(
|
||||
"Creating SSH client host=%s user=%s port=%s key_dir=%s key_name=%s password=%s",
|
||||
"Creating SSH client from legacy env host=%s user=%s port=%s key_dir=%s key_name=%s password=%s",
|
||||
settings.ssh_host or "<unset>",
|
||||
settings.ssh_username or "<unset>",
|
||||
settings.ssh_port,
|
||||
@@ -60,20 +163,9 @@ def get_ssh_client() -> RemoteSSHClient:
|
||||
"set" if settings.ssh_password else "missing",
|
||||
)
|
||||
if not settings.ssh_key_path:
|
||||
raise RuntimeError("SSH_KEY_DIRECTORY and SSH_KEY_NAME must be configured")
|
||||
client = RemoteSSHClient(
|
||||
host=settings.ssh_host,
|
||||
username=settings.ssh_username,
|
||||
port=settings.ssh_port,
|
||||
key_filename=settings.ssh_key_path,
|
||||
password=settings.ssh_password or None,
|
||||
)
|
||||
try:
|
||||
client.connect()
|
||||
except Exception:
|
||||
logger.exception("Failed to establish SSH connection to %s", settings.ssh_host or "<unset>")
|
||||
raise
|
||||
return client
|
||||
raise RuntimeError("No SSH machine is configured and SSH key settings must be configured")
|
||||
ensure_known_host(settings.ssh_host, settings.ssh_port, settings.ssh_known_hosts_file)
|
||||
return _ssh_client_for(("legacy", settings.ssh_host, settings.ssh_username, settings.ssh_port, settings.ssh_key_path, settings.ssh_password or None, str(settings.ssh_known_hosts_file)))
|
||||
|
||||
|
||||
def get_mail_queue() -> MailQueue:
|
||||
@@ -91,13 +183,16 @@ def get_settings_store() -> SettingsStore:
|
||||
return _get_settings_store()
|
||||
|
||||
|
||||
def get_user_id() -> str:
|
||||
"""Return the configured Jellyfin user ID, or discover the first available user."""
|
||||
def get_user_id(request: Request = None) -> str:
|
||||
"""Return the configured Jellyfin user ID or discover the first available one."""
|
||||
machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("jellyfin", request)
|
||||
if machine and machine.get("jellyfin_user_id"):
|
||||
return str(machine["jellyfin_user_id"])
|
||||
settings = get_settings()
|
||||
if settings.jellyfin_user_id:
|
||||
return settings.jellyfin_user_id
|
||||
client = get_jellyfin_client()
|
||||
client = get_jellyfin_client(request)
|
||||
users = client.users()
|
||||
if not users:
|
||||
raise RuntimeError("No Jellyfin users found and JELLYFIN_USER_ID not set")
|
||||
raise RuntimeError("No Jellyfin users found and no machine/user id configured")
|
||||
return users[0]["Id"]
|
||||
|
||||
@@ -15,7 +15,8 @@ from media_library_viewer_api.config import get_settings
|
||||
from media_library_viewer_api.logging_utils import configure_logging, describe_settings
|
||||
from media_library_viewer_api.routers import dashboard, monitoring, media, files, jobs, users
|
||||
from media_library_viewer_api.routers.settings import router as settings_router
|
||||
from media_library_viewer_api.dependencies import get_mail_queue, get_monitoring_poller
|
||||
from media_library_viewer_api.dependencies import get_mail_queue, get_monitoring_poller, get_settings_store
|
||||
from media_library_viewer_api.services.known_hosts import ensure_known_hosts_for_machines
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,6 +28,12 @@ async def lifespan(app: FastAPI):
|
||||
configure_logging(settings.log_level)
|
||||
validate_auth_settings(settings)
|
||||
logger.info("Backend startup complete: %s", describe_settings(settings))
|
||||
store = get_settings_store()
|
||||
try:
|
||||
changed = ensure_known_hosts_for_machines(store.list_machines(), settings.ssh_known_hosts_file)
|
||||
logger.info("Managed known_hosts updated entries=%s file=%s", changed, settings.ssh_known_hosts_file)
|
||||
except Exception:
|
||||
logger.exception("Failed to synthesize managed known_hosts file")
|
||||
mail_queue = get_mail_queue()
|
||||
monitoring_poller = get_monitoring_poller()
|
||||
mail_queue.start()
|
||||
|
||||
@@ -23,6 +23,7 @@ from media_library_viewer_api.dependencies import (
|
||||
from media_library_viewer_api.clients.ssh import CommandResult
|
||||
from media_library_viewer_api.routers.media import get_media_index
|
||||
from media_library_viewer_api.services.media_index import MediaIndex
|
||||
from media_library_viewer_api.services.settings_store import SettingsStore
|
||||
|
||||
|
||||
# --- Fixtures ---
|
||||
@@ -263,6 +264,69 @@ class TestDashboard:
|
||||
assert len(data) == 2
|
||||
|
||||
|
||||
# --- Settings reset ---
|
||||
|
||||
class TestSettingsReset:
|
||||
def test_reset_local_database_requires_full_confirmation(self, test_client, tmp_path):
|
||||
store = SettingsStore(tmp_path / "settings.sqlite")
|
||||
store.ensure_defaults()
|
||||
media_db = tmp_path / "media.sqlite"
|
||||
media_db.write_text("placeholder", encoding="utf-8")
|
||||
media_wal = tmp_path / "media.sqlite-wal"
|
||||
media_wal.write_text("wal", encoding="utf-8")
|
||||
|
||||
app.dependency_overrides[get_settings_store] = lambda: store
|
||||
settings_module = __import__("media_library_viewer_api.routers.settings", fromlist=["MediaIndex"])
|
||||
original_media_index = settings_module.MediaIndex
|
||||
settings_module.MediaIndex = lambda: SimpleNamespace(db_path=media_db)
|
||||
try:
|
||||
response = test_client.post(
|
||||
"/api/settings/reset-local-database",
|
||||
json={
|
||||
"confirm_phrase": "RESET LOCAL DATABASE",
|
||||
"acknowledge_settings_loss": True,
|
||||
"acknowledge_media_index_loss": False,
|
||||
"acknowledge_irreversible": True,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_settings_store, None)
|
||||
settings_module.MediaIndex = original_media_index
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_reset_local_database_wipes_state_and_reseeds_local_machine(self, test_client, tmp_path):
|
||||
store = SettingsStore(tmp_path / "settings.sqlite")
|
||||
store.ensure_defaults()
|
||||
media_db = tmp_path / "media.sqlite"
|
||||
media_db.write_text("placeholder", encoding="utf-8")
|
||||
media_wal = tmp_path / "media.sqlite-wal"
|
||||
media_wal.write_text("wal", encoding="utf-8")
|
||||
|
||||
app.dependency_overrides[get_settings_store] = lambda: store
|
||||
settings_module = __import__("media_library_viewer_api.routers.settings", fromlist=["MediaIndex"])
|
||||
original_media_index = settings_module.MediaIndex
|
||||
settings_module.MediaIndex = lambda: SimpleNamespace(db_path=media_db)
|
||||
try:
|
||||
response = test_client.post(
|
||||
"/api/settings/reset-local-database",
|
||||
json={
|
||||
"confirm_phrase": "RESET LOCAL DATABASE",
|
||||
"acknowledge_settings_loss": True,
|
||||
"acknowledge_media_index_loss": True,
|
||||
"acknowledge_irreversible": True,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_settings_store, None)
|
||||
settings_module.MediaIndex = original_media_index
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["status"] == "reset"
|
||||
assert not media_db.exists()
|
||||
assert not media_wal.exists()
|
||||
assert store.get_machine("local") is not None
|
||||
assert len(store.list_machines()) == 1
|
||||
|
||||
# --- Users ---
|
||||
|
||||
class TestUsers:
|
||||
|
||||
Reference in New Issue
Block a user