Fix #113: Preserve plugins during application updates
Test Suite / test (push) Canceled after 0s

Add backup/restore mechanism to prevent plugin loss on update:
- Backs up plugins to temp location before update extraction
- Restores plugins after cleanup completes
- Each plugin preserved as-is (no uninstallation logic needed)
- Temp backups cleaned up automatically after successful restore
This commit is contained in:
Hermes committed 2026-11-07 17:24:59 -07:00
1 parent 895695148b
commit 24cbaaa615
1 file changed
+131
+131
View File
@@ -494,9 +494,132 @@ def _remove_readonly(func: Callable[[str], Any], path: str, exc: Any) -> None:
func(path)
def _backup_plugins(platform_name: str, app_name: str, source_dir: str, dest_dir: Optional[str] = None) -> bool:
"""
Backs up plugins from source directory to a temporary location.
Issue #113: Preserve plugins during updates
Returns True if backup was successful, False otherwise.
"""
plugins_src = os.path.join(source_dir, "plugins")
if not os.path.exists(plugins_src):
print("No plugins directory to backup.")
return False
# Create a temporary backup location (outside app dir so it won't be wiped)
import tempfile
backup_temp = tempfile.mkdtemp(prefix=f"{app_name}_plugins_backup_")
plugins_dest = os.path.join(backup_temp, "plugins")
try:
shutil.copytree(plugins_src, plugins_dest)
plugin_count = sum(1 for f in os.listdir(plugins_dest))
print(f"Backed up {plugin_count} plugin(s) to: {plugins_dest}")
# Store backup path in a marker file for restoration
marker_path = os.path.join(backup_temp, ".plugins_backup_marker")
with open(marker_path, "w") as f:
f.write(f"plugins_dir={plugins_dest}\n")
f.write(f"target_dir={source_dir}\n")
return True
except Exception as e:
print(f"Failed to backup plugins: {e}")
# Cleanup failed backup attempt
try:
shutil.rmtree(backup_temp, onexc=_remove_readonly)
except Exception:
pass
return False
def _restore_plugins(platform_name: str, app_name: str, source_dir: str) -> bool:
"""
Restores plugins from temporary backup to the application directory.
Issue #113: Preserve plugins during updates
Returns True if restoration was successful, False otherwise.
"""
import tempfile
# Find all plugin backup temp directories
temp_dir = tempfile.gettempdir()
backups_found = []
try:
for entry in os.listdir(temp_dir):
if entry.startswith(f"{app_name}_plugins_backup_"):
backup_path = os.path.join(temp_dir, entry)
marker_path = os.path.join(backup_path, ".plugins_backup_marker")
if os.path.exists(marker_path):
backups_found.append((backup_path, entry))
except Exception as e:
print(f"Error searching for plugin backups: {e}")
if not backups_found:
print("No plugin backups found to restore.")
return False
# Restore from all found backups (in case multiple exist)
plugins_restored = 0
for backup_path, entry_name in sorted(backups_found):
try:
marker_path = os.path.join(backup_path, ".plugins_backup_marker")
# Parse marker file to get original paths
backup_info = {}
with open(marker_path, "r") as f:
for line in f:
if "=" in line:
key, value = line.strip().split("=", 1)
backup_info[key] = value
plugins_src = backup_info.get("plugins_dir", os.path.join(backup_path, "plugins"))
# Create plugins directory if it doesn't exist
target_plugins_dir = os.path.join(source_dir, "plugins")
os.makedirs(target_plugins_dir, exist_ok=True)
# Copy each plugin to target
if os.path.exists(plugins_src):
for item in os.listdir(plugins_src):
src_item = os.path.join(plugins_src, item)
dst_item = os.path.join(target_plugins_dir, item)
if os.path.isdir(src_item):
if os.path.exists(dst_item):
shutil.rmtree(dst_item, onexc=_remove_readonly)
shutil.copytree(src_item, dst_item)
else:
shutil.copy2(src_item, dst_item)
plugins_restored += 1
# Cleanup backup after successful restore
try:
shutil.rmtree(backup_path, onexc=_remove_readonly)
print(f"Cleaned up backup directory: {entry_name}")
except Exception as e:
print(f"Warning: Could not cleanup backup: {e}")
except Exception as e:
print(f"Failed to restore plugins from {entry_name}: {e}")
if plugins_restored > 0:
print(f"Restored {plugins_restored} plugin(s) to: {os.path.join(source_dir, 'plugins')}")
return True
return False
def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, finish_update: bool) -> None:
"""
Completes a pending application update if '--finish-update' is present in the command-line arguments.
Issue #113: Added plugin preservation during updates.
"""
if not finish_update:
@@ -504,6 +627,11 @@ def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, fi
print("Finishing update...")
# 0. Backup plugins BEFORE we do anything (Issue #113)
target_base = get_app_dir()
if os.path.exists(target_base):
_backup_plugins(platform_name, app_name, target_base, None)
# 1. Reset welcome dialog config flag
update_cfg = configparser.ConfigParser()
try:
@@ -611,6 +739,9 @@ def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, fi
else:
print(f"Failed to delete update folder '{folder}' after 3 attempts: {e}")
# 6. Restore plugins after update (Issue #113)
_restore_plugins(platform_name, app_name, target_base)
if "--finish-update" in sys.argv:
sys.argv.remove("--finish-update")