From 28e7c04b378596ab5a78eac92b0239cb918412b3 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 29 Jun 2026 21:40:37 +0800 Subject: [PATCH] fix(settings): migrate all settings subdirectories on portable mode switch --- py/services/settings_manager.py | 78 ++++++++++++---------- tests/services/test_settings_manager.py | 89 +++++++++++++++++++------ 2 files changed, 109 insertions(+), 58 deletions(-) diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index c76270bb..64a0320e 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -1568,7 +1568,7 @@ class SettingsManager: previous_dir = os.path.dirname(previous_path) or target_dir if os.path.abspath(previous_path) != os.path.abspath(target_path): - self._copy_model_cache_directory(previous_dir, target_dir) + self._migrate_settings_directory_content(previous_dir, target_dir) logger.info("Switching settings file to: %s", target_path) self._pending_portable_switch = {"other_path": other_path} @@ -1603,46 +1603,52 @@ class SettingsManager: finally: self._pending_portable_switch = None - def _copy_model_cache_directory(self, source_dir: str, target_dir: str) -> None: - """Copy model_cache artifacts when switching storage locations.""" + def _migrate_settings_directory_content( + self, source_dir: str, target_dir: str + ) -> None: + """Migrate settings directory subdirectories when switching storage locations. + + Copies the canonical subdirectories (cache, backups, logs, stats, wildcards) + from the old settings directory to the new one. Legacy cache artifacts + (model_cache, recipe_cache, etc.) are migrated lazily by + ``resolve_cache_path_with_migration`` on first access. + + Args: + source_dir: The previous settings directory path. + target_dir: The new settings directory path. + """ if not source_dir or not target_dir: return - source_cache_dir = os.path.join(source_dir, "model_cache") - target_cache_dir = os.path.join(target_dir, "model_cache") - if os.path.isdir(source_cache_dir) and os.path.abspath( - source_cache_dir - ) != os.path.abspath(target_cache_dir): - try: - shutil.copytree( - source_cache_dir, - target_cache_dir, - dirs_exist_ok=True, - ignore=shutil.ignore_patterns("*.sqlite-shm", "*.sqlite-wal"), - ) - except Exception as exc: - logger.warning( - "Failed to copy model_cache directory from %s to %s: %s", - source_cache_dir, - target_cache_dir, - exc, - ) + def _copy_dir(name: str) -> None: + source = os.path.join(source_dir, name) + target = os.path.join(target_dir, name) + if os.path.isdir(source) and os.path.abspath(source) != os.path.abspath( + target + ): + try: + shutil.copytree( + source, + target, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns("*.sqlite-shm", "*.sqlite-wal"), + ) + except Exception as exc: + logger.warning( + "Failed to copy directory %s from %s to %s: %s", + name, + source, + target, + exc, + ) - source_cache_file = os.path.join(source_dir, "model_cache.sqlite") - target_cache_file = os.path.join(target_dir, "model_cache.sqlite") - if os.path.isfile(source_cache_file) and os.path.abspath( - source_cache_file - ) != os.path.abspath(target_cache_file): - try: - shutil.copy2(source_cache_file, target_cache_file) - except Exception as exc: - logger.warning( - "Failed to copy model_cache.sqlite from %s to %s: %s", - source_cache_file, - target_cache_file, - exc, - ) + # Managed subdirectories under settings_dir + _copy_dir("cache") + _copy_dir("backups") + _copy_dir("logs") + _copy_dir("stats") + _copy_dir("wildcards") def _get_user_config_directory(self) -> str: """Return the user configuration directory, falling back to ~/.config.""" diff --git a/tests/services/test_settings_manager.py b/tests/services/test_settings_manager.py index c5561520..93605320 100644 --- a/tests/services/test_settings_manager.py +++ b/tests/services/test_settings_manager.py @@ -200,52 +200,97 @@ def _setup_storage_paths(tmp_path, monkeypatch): return project_root, user_dir, user_settings_path -def _populate_cache(root_dir, marker_name, db_text): - cache_dir = root_dir / "model_cache" - cache_dir.mkdir(exist_ok=True) - marker_file = cache_dir / marker_name - marker_file.write_text(marker_name, encoding="utf-8") - (root_dir / "model_cache.sqlite").write_text(db_text, encoding="utf-8") +def _populate_settings_dir(root_dir): + """Create test data for all managed subdirectories under a settings directory.""" + (root_dir / "cache" / "symlink").mkdir(parents=True, exist_ok=True) + (root_dir / "cache" / "symlink" / "symlink_map.json").write_text( + '{"migrated": true}', encoding="utf-8" + ) + (root_dir / "backups").mkdir(parents=True, exist_ok=True) + (root_dir / "backups" / "backup_test.zip").write_text( + "backup", encoding="utf-8" + ) + (root_dir / "logs").mkdir(parents=True, exist_ok=True) + (root_dir / "logs" / "session.log").write_text("log", encoding="utf-8") + (root_dir / "stats").mkdir(parents=True, exist_ok=True) + (root_dir / "stats" / "stats.json").write_text( + '{"stats": true}', encoding="utf-8" + ) + (root_dir / "wildcards").mkdir(parents=True, exist_ok=True) + (root_dir / "wildcards" / "test.txt").write_text("wildcard", encoding="utf-8") -def test_switch_to_portable_mode_copies_cache(tmp_path, monkeypatch): +def test_switch_to_portable_mode_copies_subdirectories(tmp_path, monkeypatch): project_root, user_dir, user_settings = _setup_storage_paths(tmp_path, monkeypatch) - _populate_cache(user_dir, "user_marker.txt", "user_db") + _populate_settings_dir(user_dir) manager = SettingsManager() manager.set("use_portable_settings", True) assert manager.settings_file == str(project_root / "settings.json") - marker_copy = project_root / "model_cache" / "user_marker.txt" - assert marker_copy.read_text(encoding="utf-8") == "user_marker.txt" - assert (project_root / "model_cache.sqlite").read_text( + # Managed subdirectories should all be migrated + assert ( + project_root / "cache" / "symlink" / "symlink_map.json" + ).read_text(encoding="utf-8") == '{"migrated": true}' + assert ( + project_root / "backups" / "backup_test.zip" + ).read_text(encoding="utf-8") == "backup" + assert (project_root / "logs" / "session.log").read_text( encoding="utf-8" - ) == "user_db" + ) == "log" + assert (project_root / "stats" / "stats.json").read_text( + encoding="utf-8" + ) == '{"stats": true}' + assert (project_root / "wildcards" / "test.txt").read_text( + encoding="utf-8" + ) == "wildcard" assert user_settings.exists() -def test_switching_back_to_user_config_moves_cache(tmp_path, monkeypatch): +def test_switching_back_to_user_config_moves_subdirectories(tmp_path, monkeypatch): project_root, user_dir, user_settings = _setup_storage_paths(tmp_path, monkeypatch) - _populate_cache(user_dir, "user_marker.txt", "user_db") + _populate_settings_dir(user_dir) manager = SettingsManager() manager.set("use_portable_settings", True) - project_cache_dir = project_root / "model_cache" - project_cache_dir.mkdir(exist_ok=True) - (project_cache_dir / "project_marker.txt").write_text( - "project_marker", encoding="utf-8" + # Populate project-root managed subdirectories + (project_root / "cache" / "model").mkdir(parents=True, exist_ok=True) + (project_root / "cache" / "model" / "default.sqlite").write_text( + "project_db", encoding="utf-8" + ) + (project_root / "backups" / "project_backup.zip").write_text( + "project_backup", encoding="utf-8" + ) + (project_root / "logs" / "project.log").write_text( + "project_log", encoding="utf-8" + ) + (project_root / "stats" / "project_stats.json").write_text( + '{"project": true}', encoding="utf-8" + ) + (project_root / "wildcards" / "project.txt").write_text( + "project_wildcard", encoding="utf-8" ) - (project_root / "model_cache.sqlite").write_text("project_db", encoding="utf-8") manager.set("use_portable_settings", False) assert manager.settings_file == str(user_settings) - assert (user_dir / "model_cache" / "project_marker.txt").read_text( + assert (user_dir / "cache" / "model" / "default.sqlite").read_text( encoding="utf-8" - ) == "project_marker" - assert (user_dir / "model_cache.sqlite").read_text(encoding="utf-8") == "project_db" + ) == "project_db" + assert (user_dir / "backups" / "project_backup.zip").read_text( + encoding="utf-8" + ) == "project_backup" + assert (user_dir / "logs" / "project.log").read_text( + encoding="utf-8" + ) == "project_log" + assert (user_dir / "stats" / "project_stats.json").read_text( + encoding="utf-8" + ) == '{"project": true}' + assert (user_dir / "wildcards" / "project.txt").read_text( + encoding="utf-8" + ) == "project_wildcard" def test_download_path_template_parses_json_string(manager):