From d02a0611d369a8345e6b3b259143e4e1996b6403 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sun, 31 May 2026 15:06:15 +0800 Subject: [PATCH] fix(update): close SQLite connection and protect cache dir during ZIP update On Windows, shutil.rmtree() fails when deleting a directory that contains an open SQLite database file. The ZIP update path in _download_and_replace_zip() calls _clean_plugin_folder() which tries to delete the cache/ directory, but downloaded_versions.sqlite is held open by DownloadedVersionHistoryService. Fix: - Add close() method to DownloadedVersionHistoryService to release the persistent SQLite connection - Call close() before _clean_plugin_folder() in the ZIP update flow - Add 'cache' to the skip_files list so the runtime cache directory is never deleted during plugin updates --- py/routes/update_routes.py | 16 ++++++++++++++-- .../downloaded_version_history_service.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/py/routes/update_routes.py b/py/routes/update_routes.py index 7628f334..95038838 100644 --- a/py/routes/update_routes.py +++ b/py/routes/update_routes.py @@ -11,6 +11,7 @@ from typing import Dict, List from ..utils.settings_paths import ensure_settings_file from ..services.downloader import get_downloader +from ..services.service_registry import ServiceRegistry logger = logging.getLogger(__name__) @@ -212,8 +213,19 @@ class UpdateRoutes: zip_path = tmp_zip_path - # Skip both settings.json, civitai and model cache folder - UpdateRoutes._clean_plugin_folder(plugin_root, skip_files=['settings.json', 'civitai', 'model_cache']) + # Close the downloaded-versions SQLite connection before cleaning, + # so that shutil.rmtree() does not fail on Windows (the process + # cannot delete a file with an outstanding open handle). + try: + history_svc = ServiceRegistry._services.get("downloaded_version_history_service") + if history_svc is not None: + history_svc.close() + logger.info("Closed downloaded-version history database connection") + except Exception: + logger.debug("Could not close downloaded-version history database", exc_info=True) + + # Skip settings.json, civitai, model cache and runtime cache folders + UpdateRoutes._clean_plugin_folder(plugin_root, skip_files=['settings.json', 'civitai', 'model_cache', 'cache']) # Extract ZIP to temp dir with tempfile.TemporaryDirectory() as tmp_dir: diff --git a/py/services/downloaded_version_history_service.py b/py/services/downloaded_version_history_service.py index 00d0bc7d..083d8fb9 100644 --- a/py/services/downloaded_version_history_service.py +++ b/py/services/downloaded_version_history_service.py @@ -96,6 +96,21 @@ class DownloadedVersionHistoryService: def get_database_path(self) -> str: return self._db_path + def close(self) -> None: + """Close the persistent SQLite connection, if open. + + This is called before plugin update operations to release the + database file lock on Windows, allowing ``shutil.rmtree()`` to + succeed when the cache resides inside the plugin directory. + """ + if self._conn is not None: + try: + self._conn.close() + except Exception: + pass + finally: + self._conn = None + def _get_active_library_name(self) -> str | None: try: value = self._settings.get_active_library_name()