mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-06 22:10:14 -03:00
fix(update): stage preserved items outside repo before git/ZIP update
Move settings.json, civitai/, wildcards/, backups/, stats/, logs/, cache/, and model_cache/ to a temp directory before git reset/clean or ZIP replacement, then restore them in a try/finally block. This prevents data loss on Windows where git clean -e exclusion patterns can fail due to path-separator mismatches or where file locks (open SQLite/log handles) cause the restore step to be skipped on failure. Also unifies three hardcoded skip lists (_clean_plugin_folder, skip_items, skip_tracked) to derive from the single _PRESERVE_DIRS constant, fixing drift where logs/ was missing from the ZIP path.
This commit is contained in:
@@ -38,6 +38,84 @@ def _clean_excludes() -> List[str]:
|
|||||||
return excludes
|
return excludes
|
||||||
|
|
||||||
|
|
||||||
|
def _stage_preserved_items(plugin_root: str) -> tuple[str, list[str]]:
|
||||||
|
"""Move preserved user-data items to a temp directory outside *plugin_root*.
|
||||||
|
|
||||||
|
This ensures that ``git reset --hard``, ``git clean -fd``, and ZIP-based
|
||||||
|
replacement cannot touch these files even when ``-e`` exclusion patterns
|
||||||
|
are mishandled (e.g. on Windows where forward-slash patterns may not
|
||||||
|
match backslash-prefixed paths in some Git builds, or where file locks
|
||||||
|
prevent deletion/recreation).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``(backup_root, staged_names)``: the temp directory path and the
|
||||||
|
list of item names that were successfully moved.
|
||||||
|
"""
|
||||||
|
backup_root = tempfile.mkdtemp(prefix='lora_manager_update_')
|
||||||
|
staged: list[str] = []
|
||||||
|
for name in _PRESERVE_DIRS:
|
||||||
|
src = os.path.join(plugin_root, name)
|
||||||
|
if not os.path.lexists(src):
|
||||||
|
continue
|
||||||
|
dst = os.path.join(backup_root, name)
|
||||||
|
try:
|
||||||
|
shutil.move(src, dst)
|
||||||
|
staged.append(name)
|
||||||
|
logger.debug("Staged '%s' for update safety", name)
|
||||||
|
except OSError:
|
||||||
|
# ``shutil.move`` may fail on Windows if a file handle inside
|
||||||
|
# the directory is still open (e.g. a SQLite WAL file). Fall
|
||||||
|
# back to copy-then-remove.
|
||||||
|
logger.debug("Move failed for '%s', falling back to copy", name)
|
||||||
|
try:
|
||||||
|
if os.path.isdir(src) and not os.path.islink(src):
|
||||||
|
shutil.copytree(src, dst, symlinks=True)
|
||||||
|
shutil.rmtree(src, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
os.remove(src)
|
||||||
|
staged.append(name)
|
||||||
|
logger.info("Copied (then removed) '%s' for update safety", name)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"Could not stage '%s': %s (will rely on git -e / skip lists)", name, exc
|
||||||
|
)
|
||||||
|
return backup_root, staged
|
||||||
|
|
||||||
|
|
||||||
|
def _restore_preserved_items(plugin_root: str, backup_root: str, staged: list[str]) -> None:
|
||||||
|
"""Move staged items back from *backup_root* into *plugin_root*.
|
||||||
|
|
||||||
|
Any leftover placeholder at the destination (created by git checkout or
|
||||||
|
ZIP extraction) is removed before the move.
|
||||||
|
"""
|
||||||
|
for name in staged:
|
||||||
|
src = os.path.join(backup_root, name)
|
||||||
|
dst = os.path.join(plugin_root, name)
|
||||||
|
try:
|
||||||
|
if os.path.lexists(dst):
|
||||||
|
if os.path.isdir(dst) and not os.path.islink(dst):
|
||||||
|
shutil.rmtree(dst, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
os.remove(dst)
|
||||||
|
shutil.move(src, dst)
|
||||||
|
logger.debug("Restored '%s' after update", name)
|
||||||
|
except OSError:
|
||||||
|
logger.debug("Move failed restoring '%s', falling back to copy", name)
|
||||||
|
try:
|
||||||
|
if os.path.isdir(src) and not os.path.islink(src):
|
||||||
|
shutil.copytree(src, dst, symlinks=True, dirs_exist_ok=True)
|
||||||
|
shutil.rmtree(src, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
os.remove(src)
|
||||||
|
logger.info("Copied '%s' back after update", name)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Failed to restore '%s': %s", name, exc)
|
||||||
|
shutil.rmtree(backup_root, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoutes:
|
class UpdateRoutes:
|
||||||
"""Routes for handling plugin update checks"""
|
"""Routes for handling plugin update checks"""
|
||||||
|
|
||||||
@@ -173,20 +251,22 @@ class UpdateRoutes:
|
|||||||
if os.path.exists(settings_path):
|
if os.path.exists(settings_path):
|
||||||
with open(settings_path, 'r', encoding='utf-8') as f:
|
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||||
settings_backup = f.read()
|
settings_backup = f.read()
|
||||||
logger.info("Backed up settings.json")
|
logger.info("Backed up settings.json (%d bytes)", len(settings_backup))
|
||||||
|
|
||||||
git_folder = os.path.join(plugin_root, '.git')
|
staged_backup_dir, staged_items = _stage_preserved_items(plugin_root)
|
||||||
if os.path.exists(git_folder):
|
try:
|
||||||
# Git update
|
git_folder = os.path.join(plugin_root, '.git')
|
||||||
success, new_version = await UpdateRoutes._perform_git_update(plugin_root, nightly)
|
if os.path.exists(git_folder):
|
||||||
else:
|
success, new_version = await UpdateRoutes._perform_git_update(plugin_root, nightly)
|
||||||
# Fallback: Download ZIP and replace files
|
else:
|
||||||
success, new_version = await UpdateRoutes._download_and_replace_zip(plugin_root)
|
success, new_version = await UpdateRoutes._download_and_replace_zip(plugin_root)
|
||||||
|
finally:
|
||||||
|
_restore_preserved_items(plugin_root, staged_backup_dir, staged_items)
|
||||||
|
|
||||||
if settings_backup and success:
|
if settings_backup and success:
|
||||||
with open(settings_path, 'w', encoding='utf-8') as f:
|
with open(settings_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(settings_backup)
|
f.write(settings_backup)
|
||||||
logger.info("Restored settings.json")
|
logger.info("Restored settings.json content")
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
@@ -417,8 +497,7 @@ class UpdateRoutes:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Could not close downloaded-version history database", exc_info=True)
|
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=list(_PRESERVE_DIRS))
|
||||||
UpdateRoutes._clean_plugin_folder(plugin_root, skip_files=['settings.json', 'civitai', 'model_cache', 'cache', 'wildcards', 'backups', 'stats'])
|
|
||||||
|
|
||||||
# Extract ZIP to temp dir
|
# Extract ZIP to temp dir
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
@@ -428,7 +507,7 @@ class UpdateRoutes:
|
|||||||
extracted_root = next(os.scandir(tmp_dir)).path
|
extracted_root = next(os.scandir(tmp_dir)).path
|
||||||
|
|
||||||
# Copy files, skipping user data that should be preserved
|
# Copy files, skipping user data that should be preserved
|
||||||
skip_items = {'settings.json', 'civitai', 'wildcards', 'backups', 'stats'}
|
skip_items = set(_PRESERVE_DIRS)
|
||||||
for item in os.listdir(extracted_root):
|
for item in os.listdir(extracted_root):
|
||||||
if item in skip_items:
|
if item in skip_items:
|
||||||
continue
|
continue
|
||||||
@@ -445,7 +524,7 @@ class UpdateRoutes:
|
|||||||
# for ComfyUI Manager to work properly
|
# for ComfyUI Manager to work properly
|
||||||
tracking_info_file = os.path.join(plugin_root, '.tracking')
|
tracking_info_file = os.path.join(plugin_root, '.tracking')
|
||||||
tracking_files = []
|
tracking_files = []
|
||||||
skip_tracked = {'civitai', 'wildcards', 'backups', 'stats'}
|
skip_tracked = set(_PRESERVE_DIRS) - {'settings.json'}
|
||||||
for root, dirs, files in os.walk(extracted_root):
|
for root, dirs, files in os.walk(extracted_root):
|
||||||
# Skip user data directories and their contents
|
# Skip user data directories and their contents
|
||||||
rel_root = os.path.relpath(root, extracted_root)
|
rel_root = os.path.relpath(root, extracted_root)
|
||||||
|
|||||||
Reference in New Issue
Block a user