Files
2025-08-10 16:39:10 +02:00

162 lines
6.1 KiB
Python

# Version: 1.0.0
# __init__.py in custom_nodes/ComfyUI-KVTools
# - Registry scan for the frontend dropdown
# - Safe web endpoints:
# GET /kvtools/image?file=<jsonNameOrPath>&key=<key>&ext=png
# POST /kvtools/refresh_registry
# POST /kvtools/peek {file_name|path, key} -> {"ok":true,"value":...}
import os, json, re
# --- CORE: node implementation
from . import kv_nodes # relative import!
NODE_CLASS_MAPPINGS = kv_nodes.NODE_CLASS_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS = kv_nodes.NODE_DISPLAY_NAME_MAPPINGS
WEB_DIRECTORY = "./web"
def _root():
return os.getcwd()
BASE_DIR = os.path.join(_root(), "custom_kv_stores")
REG_PATH = os.path.join(os.path.dirname(__file__), "web", "kv_registry.json")
IMAGES_ROOT = os.path.join(BASE_DIR, "images")
def _scan():
os.makedirs(BASE_DIR, exist_ok=True)
files = {}
for name in sorted(os.listdir(BASE_DIR)):
if not name.lower().endswith(".json"):
continue
f = os.path.join(BASE_DIR, name)
try:
with open(f, "r", encoding="utf-8") as fh:
data = json.load(fh)
if isinstance(data, dict):
files[name] = {"keys": sorted([str(k) for k in data.keys()])}
except Exception:
pass
return {"base_dir": BASE_DIR, "files": files}
def _write(reg):
web_dir = os.path.join(os.path.dirname(__file__), "web")
os.makedirs(web_dir, exist_ok=True)
with open(REG_PATH, "w", encoding="utf-8") as fh:
json.dump(reg, fh, ensure_ascii=False, indent=2)
def _sanitize_name(s: str) -> str:
s = os.path.basename(str(s or "").strip())
return re.sub(r"[^A-Za-z0-9._ \-]+", "_", s)
def _json_basename_from_param(file_or_path: str) -> str:
"""
Accepts either "foo.json" or a full path and returns "foo".
"""
if not file_or_path:
return ""
base = os.path.basename(str(file_or_path).strip())
base, _ = os.path.splitext(base)
return _sanitize_name(base)
# --- Build registry at startup
try:
_write(_scan())
print("[ComfyUI-KVTools] registry written:", REG_PATH)
except Exception as e:
print("[ComfyUI-KVTools] registry build failed:", e)
# --- Web endpoints (only when running inside ComfyUI)
try:
from aiohttp import web
from server import PromptServer # ComfyUI-internal
# POST /kvtools/refresh_registry -> {"ok":true, "written": ".../kv_registry.json"}
@PromptServer.instance.routes.post("/kvtools/refresh_registry")
async def kvtools_refresh_registry(request):
try:
reg = _scan()
_write(reg)
return web.json_response({"ok": True, "written": REG_PATH})
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=500)
# POST /kvtools/peek {file_name|path, key} -> {"ok":true,"value": "..."}
# reads a value from a JSON in BASE_DIR, no paths outside allowed.
# POST /kvtools/peek {file_name|path, key} -> {"ok":true,"value": "..."}
# Reads ONLY whitelisted (file,key) pairs that exist in the registry.
@PromptServer.instance.routes.post("/kvtools/peek")
async def kvtools_peek(request):
try:
data = await request.json()
except Exception:
data = {}
file_name = data.get("file_name") or data.get("path") or ""
key = data.get("key") or ""
if not file_name or not key:
return web.json_response({"ok": False, "error": "missing file_name or key"}, status=400)
# --- load registry (prefer on-disk JSON; fallback to fresh scan)
try:
with open(REG_PATH, "r", encoding="utf-8") as fh:
reg = json.load(fh)
except Exception:
reg = _scan()
files = (reg or {}).get("files", {})
# normalize to registry key (json basename with extension)
json_name = os.path.basename(str(file_name).strip())
# require: listed in registry, key exists
entry = files.get(json_name)
if not entry or "keys" not in entry or str(key) not in entry["keys"]:
return web.json_response({"ok": False, "error": "not whitelisted (file/key)"}, status=403)
# path under BASE_DIR, file must exist
json_path = os.path.join(BASE_DIR, json_name)
if not (os.path.isfile(json_path) and os.path.abspath(json_path).startswith(os.path.abspath(BASE_DIR))):
return web.json_response({"ok": False, "error": "invalid file"}, status=400)
try:
with open(json_path, "r", encoding="utf-8") as fh:
doc = json.load(fh)
val = doc.get(key, "")
if isinstance(val, (dict, list)):
val = json.dumps(val, ensure_ascii=False)
return web.json_response({"ok": True, "value": str(val)})
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=500)
# GET /kvtools/image?file=<jsonNameOrPath>&key=<key>&ext=png
# Serves ONLY images from <ComfyUI>/custom_kv_stores/images/<json-base>/<key>.<ext>
@PromptServer.instance.routes.get("/kvtools/image")
async def kvtools_image(request):
params = request.rel_url.query
file_or_path = params.get("file") or params.get("registry") or params.get("path") or ""
key = params.get("key") or ""
ext = params.get("ext") or "png"
ext = "png" if ext.lower() != "png" else "png" # fixed to png
json_base = _json_basename_from_param(file_or_path)
if not json_base or not key:
raise web.HTTPNotFound()
folder = os.path.join(IMAGES_ROOT, json_base)
fname = f"{_sanitize_name(key)}.{ext}"
full = os.path.join(folder, fname)
# Whitelist check: must stay under IMAGES_ROOT
if not os.path.abspath(full).startswith(os.path.abspath(IMAGES_ROOT)):
raise web.HTTPForbidden()
if not os.path.isfile(full):
raise web.HTTPNotFound()
return web.FileResponse(full)
print("[ComfyUI-KVTools] web endpoints ready (/kvtools/*)")
except Exception as e:
# Outside of ComfyUI (pure Python env) we ignore the endpoints.
print("[ComfyUI-KVTools] web endpoints not active:", e)