mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-26 21:44:09 -03:00
fix: address review — injective mirror roots, root relocation, full preview coverage, EXDEV-safe rollback
Codex review on #1124: - P1: mirror layout root component is now <basename>-<roothash> (sha256 of the normalized root path), so two roots sharing a basename no longer map to the same mirror directory and overwrite each other's sidecars - P1: changing sidecar_storage_path while centralized no longer strands assets in the old root — new relocate_root migration direction moves the whole mirror tree, rewrites preview_url prefixes inside sidecars, reconciles scanner caches, and prunes the emptied old tree; the settings UI detects the path change and offers the relocation - P2: migration enumerates the same preview candidates as find_preview_file — case-insensitive variants (model.WEBP) and the legacy .example.0.jpeg suffix — instead of exact lowercase PREVIEW_EXTENSIONS only - P2: _rollback_model_staging restores staged files with the EXDEV-tolerant mover, so a failed undoable-delete staging no longer strands a cross-filesystem centralized sidecar copy Tests: same-basename root injectivity, mixed-case/example preview migration, relocate_root happy path + guards + route 400, frontend relocation prompt flow. Verified end-to-end in a sandboxed standalone server: uppercase/legacy previews migrate, root relocation moves the tree and the list API serves the new locations immediately without a rescan.
This commit is contained in:
@@ -22,6 +22,7 @@ from py.utils.sidecar_paths import (
|
||||
resolve_centralized_dir,
|
||||
resolve_centralized_dir_for_dir,
|
||||
resolve_metadata_path,
|
||||
root_mirror_component,
|
||||
sanitize_path_component,
|
||||
)
|
||||
|
||||
@@ -117,16 +118,48 @@ class TestCentralizedMode:
|
||||
def test_mirror_layout(self, model_roots: dict, centralized: Path):
|
||||
model = model_roots["loras"] / "styles" / "anime" / "model.safetensors"
|
||||
library = get_settings_manager().get_active_library_name()
|
||||
root_component = root_mirror_component(str(model_roots["loras"]))
|
||||
|
||||
metadata_path = get_metadata_path(str(model))
|
||||
|
||||
expected = os.path.join(
|
||||
str(centralized), library, "loras", "styles", "anime", "model" + METADATA_SUFFIX
|
||||
str(centralized), library, root_component, "styles", "anime", "model" + METADATA_SUFFIX
|
||||
)
|
||||
assert metadata_path == expected
|
||||
assert get_preview_dir(str(model)) == os.path.dirname(expected)
|
||||
assert is_centralized()
|
||||
|
||||
def test_same_basename_roots_get_distinct_mirrors(
|
||||
self, model_roots: dict, centralized: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
from py.config import config
|
||||
|
||||
# Two roots sharing the basename "loras" must not share a mirror dir.
|
||||
other_parent = tmp_path / "elsewhere"
|
||||
other_root = other_parent / "loras"
|
||||
other_root.mkdir(parents=True)
|
||||
monkeypatch.setattr(
|
||||
config,
|
||||
"loras_roots",
|
||||
[str(model_roots["loras"]), str(other_root)],
|
||||
raising=False,
|
||||
)
|
||||
|
||||
model_a = model_roots["loras"] / "model.safetensors"
|
||||
model_b = other_root / "model.safetensors"
|
||||
|
||||
dir_a = resolve_centralized_dir(str(model_a))
|
||||
dir_b = resolve_centralized_dir(str(model_b))
|
||||
assert dir_a is not None and dir_b is not None
|
||||
assert dir_a != dir_b
|
||||
assert root_mirror_component(str(model_roots["loras"])) != root_mirror_component(
|
||||
str(other_root)
|
||||
)
|
||||
# Same root always maps to the same component (stable hash).
|
||||
assert root_mirror_component(str(model_roots["loras"])) == root_mirror_component(
|
||||
str(model_roots["loras"]) + os.sep
|
||||
)
|
||||
|
||||
def test_longest_root_wins(self, model_roots: dict, centralized: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
from py.config import config
|
||||
|
||||
@@ -142,7 +175,7 @@ class TestCentralizedMode:
|
||||
|
||||
model = nested / "model.safetensors"
|
||||
assert get_metadata_path(str(model)) == os.path.join(
|
||||
str(centralized), library, "nested", "model" + METADATA_SUFFIX
|
||||
str(centralized), library, root_mirror_component(str(nested)), "model" + METADATA_SUFFIX
|
||||
)
|
||||
|
||||
def test_outside_roots_falls_back_to_alongside(
|
||||
@@ -174,7 +207,7 @@ class TestCentralizedMode:
|
||||
library = get_settings_manager().get_active_library_name()
|
||||
|
||||
assert resolve_centralized_dir_for_dir(str(model_roots["loras"])) == os.path.join(
|
||||
str(centralized), library, "loras"
|
||||
str(centralized), library, root_mirror_component(str(model_roots["loras"]))
|
||||
)
|
||||
|
||||
def test_empty_path_uses_default_sidecar_root(self, model_roots: dict, tmp_path: Path):
|
||||
@@ -223,7 +256,9 @@ class TestModeIndependentResolution:
|
||||
assert resolve_centralized_dir_for_dir(str(model_dir)) is None
|
||||
assert resolve_centralized_dir_for_dir(
|
||||
str(model_dir), sidecar_root=str(sidecar_root)
|
||||
) == os.path.join(str(sidecar_root), library, "loras", "sub")
|
||||
) == os.path.join(
|
||||
str(sidecar_root), library, root_mirror_component(str(model_roots["loras"])), "sub"
|
||||
)
|
||||
|
||||
|
||||
class TestSettingsValidation:
|
||||
|
||||
Reference in New Issue
Block a user