diff --git a/py/config.py b/py/config.py index bdd9a591..c34952d5 100644 --- a/py/config.py +++ b/py/config.py @@ -355,6 +355,7 @@ class Config: start = time.perf_counter() # Reset mappings before rescanning to avoid stale entries self._path_mappings.clear() + self._seed_root_symlink_mappings() visited_dirs: Set[str] = set() for root in self._symlink_roots(): self._scan_directory_links(root, visited_dirs) @@ -458,6 +459,22 @@ class Config: self._preview_root_paths.update(self._expand_preview_root(normalized_target)) self._preview_root_paths.update(self._expand_preview_root(normalized_link)) + def _seed_root_symlink_mappings(self) -> None: + """Ensure symlinked root folders are recorded before deep scanning.""" + + for root in self._symlink_roots(): + if not root: + continue + try: + if not self._is_link(root): + continue + target_path = os.path.realpath(root) + if not os.path.isdir(target_path): + continue + self.add_path_mapping(root, target_path) + except Exception as exc: + logger.debug("Skipping root symlink %s: %s", root, exc) + def _expand_preview_root(self, path: str) -> Set[Path]: """Return normalized ``Path`` objects representing a preview root.""" diff --git a/py/services/base_model_service.py b/py/services/base_model_service.py index 84db592b..7f1d9bb6 100644 --- a/py/services/base_model_service.py +++ b/py/services/base_model_service.py @@ -716,6 +716,8 @@ class BaseModelService(ABC): if normalized_file.startswith(normalized_root): # Remove root and leading separator to get relative path relative_path = normalized_file[len(normalized_root):].lstrip(os.sep) + # Normalize separators so results are stable across platforms + relative_path = relative_path.replace(os.sep, "/") break if not relative_path: diff --git a/tests/config/test_symlink_cache.py b/tests/config/test_symlink_cache.py index caf50195..5ba9820b 100644 --- a/tests/config/test_symlink_cache.py +++ b/tests/config/test_symlink_cache.py @@ -1,3 +1,4 @@ +import json import os import pytest @@ -143,3 +144,40 @@ def test_background_rescan_refreshes_cache(monkeypatch: pytest.MonkeyPatch, tmp_ new_real = _normalize(os.path.realpath(new_target)) assert second_cfg._path_mappings.get(new_real) == _normalize(str(dir_link)) assert second_cfg.map_path_to_link(str(new_target)) == _normalize(str(dir_link)) + + +def test_symlink_roots_are_preserved(monkeypatch: pytest.MonkeyPatch, tmp_path): + settings_dir = tmp_path / "settings" + real_loras = tmp_path / "loras_real" + real_loras.mkdir() + loras_link = tmp_path / "loras_link" + loras_link.symlink_to(real_loras, target_is_directory=True) + + checkpoints_dir = tmp_path / "checkpoints" + checkpoints_dir.mkdir() + embedding_dir = tmp_path / "embeddings" + embedding_dir.mkdir() + + def fake_get_folder_paths(kind: str): + mapping = { + "loras": [str(loras_link)], + "checkpoints": [str(checkpoints_dir)], + "unet": [], + "embeddings": [str(embedding_dir)], + } + return mapping.get(kind, []) + + monkeypatch.setattr(config_module.folder_paths, "get_folder_paths", fake_get_folder_paths) + monkeypatch.setattr(config_module, "standalone_mode", True) + monkeypatch.setattr(config_module, "get_settings_dir", lambda create=True: str(settings_dir)) + monkeypatch.setattr(config_module.Config, "_schedule_symlink_rescan", lambda self: None) + + cfg = config_module.Config() + + normalized_real = _normalize(os.path.realpath(real_loras)) + normalized_link = _normalize(str(loras_link)) + assert cfg._path_mappings[normalized_real] == normalized_link + + cache_path = settings_dir / "cache" / "symlink_map.json" + payload = json.loads(cache_path.read_text(encoding="utf-8")) + assert payload["path_mappings"][normalized_real] == normalized_link