mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-27 05:54:08 -03:00
feat(sidecars): surface storage location and cover excluded models in migration
After migrating to centralized sidecar storage users had no indication where their files went, and portable-mode installs silently placed the sidecar root inside the plugin folder where a reinstall or git clean would delete it. - Migration now also covers models excluded from the library view and returns the resolved sidecar root in its result payload - get_settings exposes the resolved sidecar root, whether it is the default, and whether it lives inside the installation folder - New POST /api/lm/sidecars/open-location endpoint opens (or copies) the sidecar storage folder - Settings UI always shows the effective storage path with an open-folder button, and warns when the root is inside the installation folder (portable-mode hazard) - Migration confirmation shows the destination; on completion a result dialog summarizes moved/skipped/conflict counts with the storage location and an open-folder action - Ignore /sidecars/ at the repository root so portable-mode sidecars are never committed Refs #1045
This commit is contained in:
@@ -97,19 +97,29 @@ class _FakeCache:
|
||||
|
||||
|
||||
class _FakeScanner:
|
||||
def __init__(self, raw_data: List[Dict[str, Any]]) -> None:
|
||||
def __init__(
|
||||
self, raw_data: List[Dict[str, Any]], excluded: List[str] | None = None
|
||||
) -> None:
|
||||
self._cache = _FakeCache(raw_data)
|
||||
self._excluded = list(excluded or [])
|
||||
self.persist_calls = 0
|
||||
|
||||
async def get_cached_data(self) -> _FakeCache:
|
||||
return self._cache
|
||||
|
||||
def get_excluded_models(self) -> List[str]:
|
||||
return list(self._excluded)
|
||||
|
||||
async def _persist_current_cache(self) -> None:
|
||||
self.persist_calls += 1
|
||||
|
||||
|
||||
def _make_use_case(model_paths: List[str]) -> SidecarMigrationUseCase:
|
||||
scanner = _FakeScanner([{"file_path": path} for path in model_paths])
|
||||
def _make_use_case(
|
||||
model_paths: List[str], excluded: List[str] | None = None
|
||||
) -> SidecarMigrationUseCase:
|
||||
scanner = _FakeScanner(
|
||||
[{"file_path": path} for path in model_paths], excluded=excluded
|
||||
)
|
||||
|
||||
async def scanner_factory() -> _FakeScanner:
|
||||
return scanner
|
||||
@@ -504,3 +514,40 @@ async def test_migrate_root_missing_old_tree_is_noop(
|
||||
|
||||
assert summary["success"] is True
|
||||
assert summary["moved"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_covers_excluded_models(
|
||||
library_root: Path, sidecar_root: Path
|
||||
):
|
||||
"""Excluded models are absent from the cache; their sidecars still move.
|
||||
|
||||
Otherwise un-excluding a model later would leave the scanner looking for
|
||||
a sidecar in the new layout that was never migrated.
|
||||
"""
|
||||
|
||||
_set_mode("centralized")
|
||||
cached = _write_model(library_root, "cached")
|
||||
_write_sidecar(library_root, "cached", cached)
|
||||
(library_root / "cached.preview.webp").write_bytes(b"preview")
|
||||
excluded = _write_model(library_root / "hidden", "excluded")
|
||||
_write_sidecar(library_root / "hidden", "excluded", excluded, preview_ext=None)
|
||||
(library_root / "hidden" / "excluded.preview.webp").write_bytes(b"preview")
|
||||
|
||||
use_case = _make_use_case([str(cached)], excluded=[str(excluded)])
|
||||
summary = await use_case.migrate_to_centralized(force=True)
|
||||
|
||||
assert summary["success"] is True
|
||||
assert summary["models_total"] == 2
|
||||
assert summary["moved"] == 4
|
||||
assert summary["sidecar_root"] == str(sidecar_root)
|
||||
|
||||
mirror = _mirror_dir(library_root, sidecar_root, "hidden")
|
||||
assert (mirror / "excluded.metadata.json").exists()
|
||||
assert (mirror / "excluded.preview.webp").exists()
|
||||
assert not (library_root / "hidden" / "excluded.metadata.json").exists()
|
||||
assert not (library_root / "hidden" / "excluded.preview.webp").exists()
|
||||
|
||||
# The excluded model is not in the cache, so cache reconciliation is a
|
||||
# no-op for it and only the cached entry gets persisted.
|
||||
assert use_case._test_scanner.persist_calls == 1
|
||||
|
||||
Reference in New Issue
Block a user