mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-26 21:44:09 -03:00
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.
This commit is contained in:
+51
-14
@@ -64,15 +64,8 @@ def is_centralized() -> bool:
|
||||
return get_storage_mode() == STORAGE_MODE_CENTRALIZED and bool(get_sidecar_root())
|
||||
|
||||
|
||||
def get_sidecar_root() -> str:
|
||||
"""Return the absolute root directory for centralized sidecar storage.
|
||||
|
||||
Empty string when centralized storage is not usable (mode alongside or an
|
||||
unresolvable configured path).
|
||||
"""
|
||||
|
||||
if get_storage_mode() != STORAGE_MODE_CENTRALIZED:
|
||||
return ""
|
||||
def _resolve_root_from_settings() -> str:
|
||||
"""Resolve the configured/default centralized root, ignoring the active mode."""
|
||||
|
||||
configured = _get_settings_value("sidecar_storage_path", "")
|
||||
if configured and isinstance(configured, str):
|
||||
@@ -90,6 +83,31 @@ def get_sidecar_root() -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def get_sidecar_root() -> str:
|
||||
"""Return the absolute root directory for centralized sidecar storage.
|
||||
|
||||
Empty string when centralized storage is not usable (mode alongside or an
|
||||
unresolvable configured path).
|
||||
"""
|
||||
|
||||
if get_storage_mode() != STORAGE_MODE_CENTRALIZED:
|
||||
return ""
|
||||
|
||||
return _resolve_root_from_settings()
|
||||
|
||||
|
||||
def get_configured_sidecar_root() -> str:
|
||||
"""Return the centralized sidecar root regardless of the active mode.
|
||||
|
||||
Unlike :func:`get_sidecar_root`, this resolves the configured
|
||||
``sidecar_storage_path`` (or the ``<settings_dir>/sidecars`` default) even
|
||||
when the storage mode is ``alongside``. Migration tooling needs both
|
||||
layouts at once and must not depend on which mode is currently active.
|
||||
"""
|
||||
|
||||
return _resolve_root_from_settings()
|
||||
|
||||
|
||||
def sanitize_path_component(name: str) -> str:
|
||||
"""Return a filesystem-safe single path component."""
|
||||
|
||||
@@ -137,19 +155,38 @@ def resolve_centralized_dir(model_path: str) -> Optional[str]:
|
||||
or the path is not under any configured model root.
|
||||
"""
|
||||
|
||||
root = get_sidecar_root()
|
||||
return resolve_centralized_dir_for_dir(
|
||||
os.path.dirname(_normalize_for_match(model_path))
|
||||
)
|
||||
|
||||
|
||||
def resolve_centralized_dir_for_dir(
|
||||
model_dir: str, *, sidecar_root: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Return the centralized mirror directory for a model *directory*.
|
||||
|
||||
Same layout as :func:`resolve_centralized_dir`, but accepts the directory
|
||||
itself. Used by folder-level operations (folder rename, mirror-tree walks)
|
||||
that have no model file path to derive from. Passing a configured model
|
||||
root returns the mirror base for that root.
|
||||
|
||||
``sidecar_root`` overrides the root lookup; pass
|
||||
:func:`get_configured_sidecar_root` to resolve mirror paths independently
|
||||
of the active storage mode (migration tooling).
|
||||
"""
|
||||
|
||||
root = sidecar_root if sidecar_root is not None else get_sidecar_root()
|
||||
if not root:
|
||||
return None
|
||||
|
||||
target = _normalize_for_match(model_path)
|
||||
model_dir = os.path.dirname(target)
|
||||
normalized_dir = _normalize_for_match(model_dir)
|
||||
|
||||
best_root: Optional[str] = None
|
||||
for candidate in _iter_model_roots():
|
||||
if not candidate:
|
||||
continue
|
||||
normalized = _normalize_for_match(candidate)
|
||||
if model_dir == normalized or model_dir.startswith(normalized + os.sep):
|
||||
if normalized_dir == normalized or normalized_dir.startswith(normalized + os.sep):
|
||||
if best_root is None or len(normalized) > len(best_root):
|
||||
best_root = normalized
|
||||
|
||||
@@ -163,7 +200,7 @@ def resolve_centralized_dir(model_path: str) -> Optional[str]:
|
||||
except Exception: # pragma: no cover - defensive fallback
|
||||
library = "default"
|
||||
|
||||
rel_dir = os.path.relpath(model_dir, best_root)
|
||||
rel_dir = os.path.relpath(normalized_dir, best_root)
|
||||
parts = [root, sanitize_path_component(library), sanitize_path_component(os.path.basename(best_root))]
|
||||
if rel_dir and rel_dir != os.curdir:
|
||||
parts.extend(sanitize_path_component(part) for part in rel_dir.split(os.sep) if part not in ("", os.curdir))
|
||||
|
||||
Reference in New Issue
Block a user