Files
ComfyUI-Lora-Manager/py/routes/misc_routes.py
T
Will Miao f5e983eaaa feat: optional centralized storage for sidecar metadata and previews (#1045)
Add an opt-in 'centralized' sidecar storage mode alongside the default
'alongside' layout. In centralized mode, .metadata.json sidecars and
preview assets live under a configurable root (sidecar_storage_path,
default <settings_dir>/sidecars), mirroring the library-relative
directory structure: <root>/<library>/<root_basename>/<rel_dir>/.

Backend:
- settings: sidecar_storage_mode / sidecar_storage_path with validation;
  changing either refreshes the preview allowlist
- config: centralized root added to preview-serving allowlist
- lifecycle: delete / move / rename / folder-rename / folder-delete and
  undoable-delete staging all operate on the mirror tree in centralized
  mode (model files themselves never move); EXDEV-tolerant cross-
  filesystem moves
- scanners: pending-hash filesystem scan walks the mirror tree in
  centralized mode; preview discovery reads from the sidecar dir;
  .civitai.info stays co-located in both modes
- migration: SidecarMigrationUseCase moves sidecars+previews between
  layouts both directions (keep-newer conflict resolution, preview_url
  rewriting, WebSocket progress), exposed as POST+GET
  /api/lm/sidecars/migrate with a mode guard (force=true for the
  settings-first flow)

Frontend:
- settings modal: sidecar storage section (mode select + path input with
  browse/validation), mode-change confirmation offering immediate
  migration (force=true), and a 'Migrate Sidecars Now' action
- i18n keys synced to all locales ([TODO: Translate] placeholders)

Docs: metadata-json-schema.md gains a storage-location section;
AGENTS.md records the sidecar_paths helper convention.
2026-09-26 10:00:58 +08:00

174 lines
6.5 KiB
Python

"""Route controller for miscellaneous endpoints."""
from __future__ import annotations
import logging
import os
from typing import Awaitable, Callable, Mapping
from aiohttp import web
from server import PromptServer # pyright: ignore[reportMissingImports]
from ..services.metadata_service import (
get_metadata_archive_manager,
get_metadata_provider,
update_metadata_providers,
)
from ..services.settings_manager import get_settings_manager
from ..services.downloader import get_downloader
from ..utils.usage_stats import UsageStats
from .handlers.misc_handlers import (
CustomWordsHandler,
DoctorHandler,
ExampleWorkflowsHandler,
FileSystemHandler,
HealthCheckHandler,
LoraCodeHandler,
BackupHandler,
MetadataArchiveHandler,
MiscHandlerSet,
ModelExampleFilesHandler,
ModelLibraryHandler,
NodeRegistry,
NodeRegistryHandler,
SettingsHandler,
SidecarMigrationHandler,
SupportersHandler,
TrainedWordsHandler,
UsageStatsHandler,
WildcardsHandler,
build_service_registry_adapter,
)
from .handlers.base_model_handlers import BaseModelHandlerSet
from .handlers.model_source_handlers import ModelSourceHandler
from .handlers.agent_handlers import AgentHandler
from .handlers.download_routing_handlers import DownloadRoutingHandler
from .misc_route_registrar import MiscRouteRegistrar
logger = logging.getLogger(__name__)
standalone_mode = (
os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
or os.environ.get("HF_HUB_DISABLE_TELEMETRY", "0") == "0"
)
class MiscRoutes:
"""Route controller that mirrors the model route architecture."""
def __init__(
self,
*,
settings_service=None,
usage_stats_factory: Callable[[], UsageStats] = UsageStats,
prompt_server: type[PromptServer] = PromptServer,
service_registry_adapter=build_service_registry_adapter(),
metadata_provider_factory=get_metadata_provider,
metadata_archive_manager_factory=get_metadata_archive_manager,
metadata_provider_updater=update_metadata_providers,
downloader_factory=get_downloader,
registrar_factory=MiscRouteRegistrar,
handler_set_factory=MiscHandlerSet,
node_registry: NodeRegistry | None = None,
standalone_mode_flag: bool = standalone_mode,
) -> None:
self._settings = settings_service or get_settings_manager()
self._usage_stats_factory = usage_stats_factory
self._prompt_server = prompt_server
self._service_registry_adapter = service_registry_adapter
self._metadata_provider_factory = metadata_provider_factory
self._metadata_archive_manager_factory = metadata_archive_manager_factory
self._metadata_provider_updater = metadata_provider_updater
self._downloader_factory = downloader_factory
self._registrar_factory = registrar_factory
self._handler_set_factory = handler_set_factory
self._node_registry = node_registry or NodeRegistry()
self._standalone_mode = standalone_mode_flag
self._handler_mapping: (
Mapping[str, Callable[[web.Request], Awaitable[web.StreamResponse]]] | None
) = None
@staticmethod
def setup_routes(app: web.Application) -> None:
"""Entry point used by the application bootstrap."""
controller = MiscRoutes()
controller.bind(app)
def bind(self, app: web.Application) -> None:
registrar = self._registrar_factory(app)
registrar.register_routes(self._ensure_handler_mapping())
def _ensure_handler_mapping(
self,
) -> Mapping[str, Callable[[web.Request], Awaitable[web.StreamResponse]]]:
if self._handler_mapping is None:
handler_set = self._create_handler_set()
self._handler_mapping = handler_set.to_route_mapping()
return self._handler_mapping
def _create_handler_set(self) -> MiscHandlerSet:
health = HealthCheckHandler()
settings_handler = SettingsHandler(
settings_service=self._settings,
metadata_provider_updater=self._metadata_provider_updater,
downloader_factory=self._downloader_factory,
)
usage_stats = UsageStatsHandler(usage_stats_factory=self._usage_stats_factory)
lora_code = LoraCodeHandler(prompt_server=self._prompt_server)
trained_words = TrainedWordsHandler()
model_examples = ModelExampleFilesHandler()
metadata_archive = MetadataArchiveHandler(
metadata_archive_manager_factory=self._metadata_archive_manager_factory,
settings_service=self._settings,
metadata_provider_updater=self._metadata_provider_updater,
)
backup = BackupHandler()
filesystem = FileSystemHandler(settings_service=self._settings)
node_registry_handler = NodeRegistryHandler(
node_registry=self._node_registry,
prompt_server=self._prompt_server,
standalone_mode=self._standalone_mode,
)
model_library = ModelLibraryHandler(
service_registry=self._service_registry_adapter,
metadata_provider_factory=self._metadata_provider_factory,
)
custom_words = CustomWordsHandler()
wildcards = WildcardsHandler()
supporters = SupportersHandler()
doctor = DoctorHandler(settings_service=self._settings)
example_workflows = ExampleWorkflowsHandler()
base_model = BaseModelHandlerSet()
model_source_handler = ModelSourceHandler()
agent_handler = AgentHandler()
download_routing = DownloadRoutingHandler()
sidecar_migration = SidecarMigrationHandler()
return self._handler_set_factory(
health=health,
settings=settings_handler,
usage_stats=usage_stats,
lora_code=lora_code,
trained_words=trained_words,
model_examples=model_examples,
node_registry=node_registry_handler,
model_library=model_library,
metadata_archive=metadata_archive,
backup=backup,
filesystem=filesystem,
custom_words=custom_words,
wildcards=wildcards,
supporters=supporters,
doctor=doctor,
example_workflows=example_workflows,
base_model=base_model,
model_source_handler=model_source_handler,
agent_handler=agent_handler,
download_routing=download_routing,
sidecar_migration=sidecar_migration,
)
__all__ = ["MiscRoutes"]