Files
ComfyAssets-ComfyUI_PromptM…/py/api/thumbnail_location.py
T
Vito Sansevero 57ab85b002 feat(thumbnails): keep previews in one folder, set from config.json (#320)
thumbnails.location in config.json can now name one folder for every
source's previews: <folder>/<source key>/<rel>_thumb<ext>. A source key is
a readable slug plus a short hash of the source's identity, stored in the
folder's marker (.promptmanager-thumbnails.json) with the install that
made it. Serving, on-demand and capture writes, listings, clear, delete,
duplicates, the temp sweep and History covers all follow it; a leftover in
<source>/thumbnails is never served and other text under thumbnails/ is
refused.

A missing, unplugged or foreign folder is unavailable (a probe checks it
every 5 s within 2 s; a stalled or failing store call flips it at once):
originals are served, nothing is written anywhere, the folder is never
re-created, and generate, convert and clear answer 409
location-unavailable. A bad config value is unavailable too, never a
fall-back to the sources.

No UI yet: POST /settings refuses thumbnail_location (409
location-needs-move); GET /settings reports it with its path, the
thumbnail status without one. Sources inside or around the folder and a
library move into it are refused.
2026-10-31 19:35:30 -07:00

137 lines
5.0 KiB
Python

"""The thumbnail location on the API (#320): startup, the settings payload
and the refusals while the thumbnail folder is unavailable.
The setting itself is config.json ``thumbnails.location``
(``preview_location``); the runtime state is ``preview_store``. POST
``/settings`` never changes it (409 ``location-needs-move``): the Move
thumbnails job is its only writer.
"""
import os
from aiohttp import web
from . import preview_location, preview_store
try:
from ...utils import library_location
except ImportError: # tests import the package top-level
from utils import library_location
# The node folder (its web/ is served and a reinstall wipes it).
NODE_FOLDER = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
def default_path():
"""``<data folder>/data/thumbnails`` (D7B), or None when unknown."""
try:
return library_location.cache_dir("thumbnails")
except Exception: # no data folder can be decided: no default
return None
def location_refusal(location=None):
"""The 409 ``location-unavailable`` response while the thumbnail folder
is unavailable, else None (thumbnail generate, convert, clear, prune)."""
location = location or preview_store.current()
if location.mode != preview_store.FOLDER_MODE or location.available:
return None
status, text = preview_location.message("location-unavailable")
return web.json_response(
{"success": False, "code": "location-unavailable", "error": text},
status=status,
)
def needs_move_response():
status, text = preview_location.message("location-needs-move")
return web.json_response(
{"success": False, "code": "location-needs-move", "error": text},
status=status,
)
class ThumbnailLocationMixin:
"""Startup check and payloads of the thumbnail location."""
def _all_source_roots(self):
"""Every image source of every root list: the Images roots, the
configured sources and the extra gallery roots (as strings)."""
from ..config import GalleryConfig, _extra_gallery_roots
roots = [str(root) for root in self._get_all_output_dirs()]
roots += [str(root) for root in GalleryConfig.MONITORING_DIRECTORIES]
roots += list(_extra_gallery_roots())
return tuple(dict.fromkeys(root for root in roots if root))
def _thumbnail_location_context(self):
"""What a thumbnail folder is validated against (blocking)."""
contained = []
try:
import folder_paths
for name in (
"get_output_directory",
"get_input_directory",
"get_temp_directory",
):
getter = getattr(folder_paths, name, None)
folder = getter() if getter is not None else None
if folder:
contained.append(str(folder))
except ImportError:
pass
try:
contained.append(library_location.config_location()[1])
except Exception: # no data folder can be decided
self.logger.debug("No library folder for the thumbnail check")
setting = preview_store.setting()
return preview_location.Context(
roots=self._all_source_roots(),
contained=tuple(contained),
node_folder=NODE_FOLDER,
default_path=default_path(),
current_path=(
setting.path if setting.mode == preview_store.FOLDER_MODE else None
),
)
def start_preview_location(self):
"""Parse and check the configured location once (startup thread);
the probe takes over. Never raises."""
from ..config import PromptManagerConfig
try:
value = PromptManagerConfig.THUMBNAIL_LOCATION
parsed = preview_location.parse_location(value)
context = None
if parsed.mode == preview_store.FOLDER_MODE:
context = self._thumbnail_location_context()
location = preview_store.configure(value, context=context)
except Exception:
self.logger.warning("The thumbnail location check failed", exc_info=True)
return None
if location.mode == preview_store.FOLDER_MODE and not location.available:
self.logger.warning(
"The thumbnail folder is unavailable "
f"({location.problem or 'checking'}): originals are shown and "
"no thumbnails are written until it is back"
)
return location
def _thumbnail_location_payload(self, *, with_path):
"""``{mode, available, problem}`` plus, for the settings GET only,
``path`` and ``default_path`` (D6A)."""
location = preview_store.current()
payload = {
"mode": location.mode,
"available": location.available,
"problem": location.problem,
}
if with_path:
payload["path"] = location.path
payload["default_path"] = default_path()
return payload