revert 618ae3c5a2
Test Suite / test (push) Canceled after 0s

revert Fix #113: Smart selective update (preserves plugins, minimizes I/O)

Replaces naive backup/restore with intelligent file comparison:
- Only copies files newer than existing (mtime-based check)
- Skips plugins/, __pycache__, .git/ directories entirely
- Atomic file operations (copy to .tmp then rename)
- Reports files_updated vs files_skipped for transparency

Benefits over previous approach:
  ✓ Zero plugin backup/restore overhead
  ✓ Minimal disk I/O (unchanged files ignored)
  ✓ Plugin preservation is automatic, not post-process
  ✓ Faster update times, especially with many unchanged files
This commit is contained in:
tyler committed 2026-11-07 22:37:47 -08:00
1 parent e2cb49ceb9
commit 5ba3b30575
1 file changed
-210
-210
View File
@@ -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")