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.
137 lines
5.0 KiB
Python
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
|