Files
DumiFlex-ComfyUI-Wildcard-P…/wp_api/database.py
T

172 lines
5.9 KiB
Python

"""/wp/api/database/* — read-only info + maintenance dispatch + location config."""
from __future__ import annotations
import json as _json
import os
from pathlib import Path
from typing import Any
from aiohttp import web
from engine.db import config as db_config
from engine.db.connection import (
global_location_path,
resolve_db_path_with_source,
root_location_path,
user_location_path,
)
from engine.db.info import analyze, gather_info, integrity_check, run_migrations, vacuum
from wp_api._helpers import db_session, json_error, json_ok
_OPS = {
"vacuum": vacuum,
"integrity": integrity_check,
"analyze": analyze,
"migrate": run_migrations,
}
_VALID_PREFERENCES = {"user", "global", "root"}
_VALID_MODES = {"copy", "move"}
async def get_info(request: web.Request) -> web.Response:
db_path, source = resolve_db_path_with_source()
with db_session(request) as conn:
info = gather_info(conn, db_path, source=source)
return json_ok(info)
async def run_maintenance(request: web.Request) -> web.Response:
try:
body = await request.json()
except _json.JSONDecodeError:
return json_error("invalid JSON body", status=400)
if not isinstance(body, dict):
return json_error("body must be an object", status=400)
op = body.get("op")
if op not in _OPS:
return json_error(
f"unknown op {op!r}; expected one of: {sorted(_OPS)}",
status=400,
)
handler = _OPS[op]
with db_session(request) as conn:
result = handler(conn)
# result already includes ok / op / duration_ms (+ op-specific keys)
return json_ok(result)
def _location_entry(path: Path | None) -> dict[str, Any]:
"""Render one location dict for the locations payload.
``path`` is ``None`` when the user-dir detector chain failed; in that
case the entry still has the same shape so the frontend can render it
consistently (it'll just hide/disable the radio for that option)."""
if path is None:
return {"path": None, "exists": False, "size_bytes": None}
exists = path.is_file()
size = path.stat().st_size if exists else None
return {"path": str(path), "exists": exists, "size_bytes": size}
def _resolve_all_locations() -> dict[str, dict[str, Any]]:
"""Compute the three potential DB locations for the config payload."""
return {
"user": _location_entry(user_location_path()),
"global": _location_entry(global_location_path()),
"root": _location_entry(root_location_path()),
}
def _build_config_response() -> dict[str, Any]:
"""Compose the GET /wp/api/database/config response from sidecar +
detected paths + env-lock detection.
Shared by GET, PUT, and DELETE handlers so the response shape stays
in one place."""
cfg = db_config.load()
return {
"preference": cfg.get("preference"),
"pending_move": cfg.get("pending_move"),
"locations": _resolve_all_locations(),
"env_locked": bool(
os.environ.get("WP_DB_PATH") or os.environ.get("COMFYUI_USER_DIR")
),
}
def _validate_pending_move(pm: Any) -> tuple[dict[str, str] | None, str | None]:
"""Validate a pending_move payload.
Returns ``(value, None)`` on success where ``value`` is either a
normalized dict or ``None`` (explicit clear). Returns ``(None, err)``
on validation failure."""
if pm is None:
return None, None
if not isinstance(pm, dict):
return None, "pending_move must be an object or null"
src = pm.get("from")
dst = pm.get("to")
mode = pm.get("mode")
if not (isinstance(src, str) and isinstance(dst, str) and isinstance(mode, str)):
return None, "pending_move requires string fields 'from', 'to', and 'mode'"
if mode not in _VALID_MODES:
return None, f"pending_move.mode must be one of: {sorted(_VALID_MODES)}"
if not Path(src).is_absolute() or not Path(dst).is_absolute():
return None, "pending_move.from and pending_move.to must be absolute paths"
return {"from": src, "to": dst, "mode": mode}, None
async def get_config(request: web.Request) -> web.Response:
return json_ok(_build_config_response())
async def put_config(request: web.Request) -> web.Response:
try:
body = await request.json()
except _json.JSONDecodeError:
return json_error("invalid JSON body", status=400)
if not isinstance(body, dict):
return json_error("body must be an object", status=400)
current = db_config.load()
new_cfg: dict[str, Any] = dict(current)
if "preference" in body:
pref = body["preference"]
if pref is None:
new_cfg.pop("preference", None)
elif isinstance(pref, str) and pref in _VALID_PREFERENCES:
new_cfg["preference"] = pref
else:
return json_error(
f"preference must be null or one of: {sorted(_VALID_PREFERENCES)}",
status=400,
)
if "pending_move" in body:
pm_raw = body["pending_move"]
if pm_raw is None:
new_cfg.pop("pending_move", None)
else:
value, err = _validate_pending_move(pm_raw)
if err is not None:
return json_error(err, status=400)
new_cfg["pending_move"] = value # type: ignore[assignment]
db_config.save(new_cfg) # type: ignore[arg-type]
return json_ok(_build_config_response())
async def delete_pending_move(request: web.Request) -> web.Response:
db_config.clear_pending_move()
return json_ok(_build_config_response())
def register(router: web.UrlDispatcher) -> None:
router.add_get("/wp/api/database/info", get_info)
router.add_post("/wp/api/database/maintenance", run_maintenance)
router.add_get("/wp/api/database/config", get_config)
router.add_put("/wp/api/database/config", put_config)
router.add_delete("/wp/api/database/config/pending-move", delete_pending_move)