From 099a71b2cc50f8dc2e462a8b9c065048ed106ebd Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Tue, 16 Dec 2025 22:05:40 +0800 Subject: [PATCH] feat(config): seed root symlink mappings before deep scanning Add `_seed_root_symlink_mappings` method to ensure symlinked root folders are recorded before deep scanning, preventing them from being missed during directory traversal. This ensures that root symlinks are properly captured in the path mappings. Additionally, normalize separators in relative paths for cross-platform consistency in `BaseModelService`, and update tests to verify root symlinks are preserved in the cache. --- py/config.py | 17 +++++++++++++ py/services/base_model_service.py | 2 ++ tests/config/test_symlink_cache.py | 38 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) 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