mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-27 05:54:08 -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:
@@ -23,6 +23,7 @@ from py.routes.handlers.misc_handlers import (
|
||||
NodeRegistryHandler,
|
||||
ServiceRegistryAdapter,
|
||||
SettingsHandler,
|
||||
SidecarMigrationHandler,
|
||||
_collect_comfyui_session_logs,
|
||||
_is_wsl,
|
||||
_wsl_to_windows_path,
|
||||
@@ -2557,3 +2558,86 @@ async def test_get_model_versions_status_supported_type_stays_interactive():
|
||||
"hasBeenDownloaded": False,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class DummySidecarMigrationUseCase:
|
||||
def __init__(self, result):
|
||||
self.result = result
|
||||
self.calls = []
|
||||
|
||||
async def execute_with_error_handling(self, *, direction, progress_cb=None, force=False):
|
||||
self.calls.append({"direction": direction, "force": force})
|
||||
return self.result
|
||||
|
||||
|
||||
def _sidecar_migration_handler(result):
|
||||
use_case = DummySidecarMigrationUseCase(result)
|
||||
handler = SidecarMigrationHandler(
|
||||
use_case_factory=lambda: use_case,
|
||||
progress_callback_factory=lambda: None,
|
||||
)
|
||||
return handler, use_case
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidecar_migration_handler_runs_to_centralized():
|
||||
result = {"success": True, "direction": "to_centralized", "moved": 3}
|
||||
handler, use_case = _sidecar_migration_handler(result)
|
||||
|
||||
response = await handler.migrate_sidecars(
|
||||
FakeRequest(json_data={"direction": "to_centralized", "force": True}) # pyright: ignore[reportArgumentType]
|
||||
)
|
||||
payload = _json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["success"] is True
|
||||
assert payload["moved"] == 3
|
||||
assert use_case.calls == [{"direction": "to_centralized", "force": True}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidecar_migration_handler_rejects_bad_direction():
|
||||
handler, use_case = _sidecar_migration_handler({"success": True})
|
||||
|
||||
response = await handler.migrate_sidecars(
|
||||
FakeRequest(json_data={"direction": "sideways"}) # pyright: ignore[reportArgumentType]
|
||||
)
|
||||
payload = _json_payload(response)
|
||||
|
||||
assert response.status == 400
|
||||
assert payload["success"] is False
|
||||
assert use_case.calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidecar_migration_handler_accepts_get_query_params():
|
||||
result = {"success": True, "direction": "to_alongside", "moved": 0}
|
||||
handler, use_case = _sidecar_migration_handler(result)
|
||||
|
||||
response = await handler.migrate_sidecars(
|
||||
FakeRequest( # pyright: ignore[reportArgumentType]
|
||||
query={"direction": "to_alongside", "force": "true"},
|
||||
method="GET",
|
||||
)
|
||||
)
|
||||
payload = _json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["success"] is True
|
||||
assert use_case.calls == [{"direction": "to_alongside", "force": True}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidecar_migration_handler_guard_refusal_is_400():
|
||||
result = {"success": False, "error": "sidecar storage is already centralized"}
|
||||
handler, use_case = _sidecar_migration_handler(result)
|
||||
|
||||
response = await handler.migrate_sidecars(
|
||||
FakeRequest(json_data={"direction": "to_centralized"}) # pyright: ignore[reportArgumentType]
|
||||
)
|
||||
payload = _json_payload(response)
|
||||
|
||||
assert response.status == 400
|
||||
assert payload["success"] is False
|
||||
assert "already centralized" in payload["error"]
|
||||
assert use_case.calls == [{"direction": "to_centralized", "force": False}]
|
||||
|
||||
Reference in New Issue
Block a user