revert 618ae3c5a2
Test Suite / test (push) Canceled after 0s
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:
1 parent
e2cb49ceb9
commit
5ba3b30575
1 file changed
-210
-210
@@ -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")
|
||||
|
||||
|
||||
Reference in new issue
Block a user