diff --git a/updater.py b/updater.py index e8833f0..f3e57ed 100644 --- a/updater.py +++ b/updater.py @@ -510,211 +510,9 @@ def _remove_readonly(func: Callable[[str], Any], path: str, exc: Any) -> None: func(path) -def _smart_selective_update(extract_path: str, target_dir: str) -> tuple[int, int]: - """ - Performs selective file replacement by comparing modification times. - - Issue #113: Smart update that only replaces changed files - - Args: - extract_path: Path to extracted new version files - target_dir: Target application directory to update - - Returns: - Tuple of (files_updated, files_skipped) - - Notes: - - Skips plugins/ directory entirely to preserve user plugins - - Only copies files that are newer in extract_path than target_dir - - Creates missing directories as needed - - Uses atomic file operations for safety - """ - import stat - - files_updated = 0 - files_skipped = 0 - - # Directories to skip (preserve user data) - skip_dirs = {'plugins', '__pycache__', '.git', 'node_modules'} - - def should_skip_dir(dirname): - return dirname in skip_dirs or dirname.startswith('.') - - def should_copy_file(src_path, dst_path): - """Return True if file should be copied (newer or doesn't exist)""" - if not os.path.exists(dst_path): - return True - src_mtime = os.path.getmtime(src_path) - dst_mtime = os.path.getmtime(dst_path) - # Copy if source is newer by at least 1 second (avoid rounding issues) - return src_mtime > dst_mtime + 1 - - # Walk through extracted files - for root, dirs, files in os.walk(extract_path): - # Filter out directories to skip - dirs[:] = [d for d in dirs if not should_skip_dir(d)] - - for filename in files: - src_path = os.path.join(root, filename) - - # Calculate relative path and destination - rel_path = os.path.relpath(src_path, extract_path) - dst_path = os.path.join(target_dir, rel_path) - - # Create destination directory if needed - dst_dir = os.path.dirname(dst_path) - os.makedirs(dst_dir, exist_ok=True) - - # Check if file needs updating - if should_copy_file(src_path, dst_path): - try: - # Atomic copy with metadata preservation - shutil.copy2(src_path, dst_path + '.tmp') - os.rename(dst_path + '.tmp', dst_path) # Atomic rename - - # Preserve permissions - src_mode = os.stat(src_path).st_mode & stat.S_IWRITE - if src_mode: - try: - os.chmod(dst_path, os.stat(src_path).st_mode) - except Exception as e: - print(f"Warning: Could not set permissions on {dst_path}: {e}") - - files_updated += 1 - except Exception as e: - print(f"Failed to copy {rel_path}: {e}") - else: - files_skipped += 1 - - return files_updated, files_skipped - - -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 (FALLBACK METHOD - rarely used now) - - 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 smart selective update (only replaces changed files). """ if not finish_update: @@ -764,12 +562,6 @@ def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, fi primary_update_folder = update_folders[0] target_updater_folder = os.path.join(primary_update_folder, f"{app_name}-darwin") if platform_name == 'darwin' else primary_update_folder - - # 2a. Perform SMART SELECTIVE UPDATE (Issue #113) - target_base = get_app_dir() - print(f"Performing selective update from: {primary_update_folder}") - files_updated, files_skipped = _smart_selective_update(primary_update_folder, target_base) - print(f"Selective update complete: {files_updated} files updated, {files_skipped} files skipped") # 3. Wait for updater process to exit print(f"Waiting for {app_name}_updater to exit...") @@ -840,8 +632,6 @@ 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}") - # Note: _restore_plugins() no longer needed - selective update preserves plugins automatically - if "--finish-update" in sys.argv: sys.argv.remove("--finish-update")