diff --git a/updater.py b/updater.py index 050e783..6910f1d 100644 --- a/updater.py +++ b/updater.py @@ -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")