versione con tab

This commit is contained in:
VALLONGOL 2025-04-07 15:43:55 +02:00
parent 6081a21909
commit 7f15ca6caa
5 changed files with 1640 additions and 930 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import os
# Import dependencies # Import dependencies
from git_commands import GitCommands, GitCommandError from git_commands import GitCommands, GitCommandError
from backup_handler import BackupHandler # To perform backups from backup_handler import BackupHandler
class ActionHandler: class ActionHandler:
"""Handles the execution logic for core application actions.""" """Handles the execution logic for core application actions."""
@ -20,44 +20,47 @@ class ActionHandler:
""" """
self.logger = logger self.logger = logger
self.git_commands = git_commands self.git_commands = git_commands
self.backup_handler = backup_handler # Store backup handler instance self.backup_handler = backup_handler
def _perform_backup_if_enabled(self, svn_path, profile_name, def _perform_backup_if_enabled(self, svn_path, profile_name,
autobackup_enabled, backup_base_dir, autobackup_enabled, backup_base_dir,
excluded_extensions, excluded_dirs): excluded_extensions, excluded_dirs):
""" """
Performs backup if enabled. Performs backup if enabled. Raises IOError on backup failure.
Args: Args:
svn_path (str): Path to the repository. svn_path (str): Path to the repository.
profile_name (str): Name of the profile (for backup naming). profile_name (str): Name of the profile (for backup naming).
autobackup_enabled (bool): Flag from settings. autobackup_enabled (bool): Flag from settings.
backup_base_dir (str): Destination directory for backups. backup_base_dir (str): Destination directory for backups.
excluded_extensions (set): Set of file extensions to exclude. excluded_extensions (set): File extensions to exclude.
excluded_dirs (set): Set of directory names to exclude. excluded_dirs (set): Directory names to exclude.
Returns: Raises:
bool: True if backup succeeded or was not needed, False on failure. IOError: If the backup process fails.
""" """
if not autobackup_enabled: if not autobackup_enabled:
self.logger.debug("Autobackup disabled, skipping backup.") self.logger.debug("Autobackup disabled, skipping backup.")
return True # Not an error, just skipped return # Backup not needed, proceed
self.logger.info("Autobackup enabled. Starting backup...") self.logger.info("Autobackup enabled. Starting backup...")
try: try:
self.backup_handler.create_zip_backup( # Delegate backup creation to the BackupHandler instance
svn_path, backup_base_dir, profile_name, backup_path = self.backup_handler.create_zip_backup(
excluded_extensions, excluded_dirs svn_path,
backup_base_dir,
profile_name,
excluded_extensions,
excluded_dirs
) )
self.logger.info("Backup completed successfully.") # Log success if backup handler doesn't raise an error
return True # Indicate backup success self.logger.info(f"Backup completed successfully: {backup_path}")
# No explicit return value needed on success, failure indicated by exception
except Exception as backup_e: except Exception as backup_e:
# Log error and indicate backup failure # Log error and re-raise as IOError to signal critical failure
self.logger.error(f"Backup failed: {backup_e}", exc_info=True) self.logger.error(f"Backup failed: {backup_e}", exc_info=True)
# Let the caller decide how to handle backup failure (e.g., show error) raise IOError(f"Autobackup failed: {backup_e}") from backup_e
# For now, just return False
return False
def execute_prepare_repo(self, svn_path): def execute_prepare_repo(self, svn_path):
@ -68,17 +71,17 @@ class ActionHandler:
svn_path (str): Validated path to the repository. svn_path (str): Validated path to the repository.
Returns: Returns:
bool: True on success. bool: True on success. (Always True if no exception raised)
Raises: Raises:
ValueError: If repository is already prepared. ValueError: If repository is already prepared.
GitCommandError/IOError/Exception: If preparation fails. GitCommandError/IOError/Exception: If preparation fails.
""" """
self.logger.info(f"Executing preparation for: {svn_path}") self.logger.info(f"Executing preparation for: {svn_path}")
# Check if already prepared first # Check if already prepared first to provide specific feedback
git_dir = os.path.join(svn_path, ".git") git_dir = os.path.join(svn_path, ".git")
if os.path.exists(git_dir): if os.path.exists(git_dir):
self.logger.warning("Repository is already prepared.") self.logger.warning("Repository is already prepared.")
# Raise specific error that UI can interpret # Raise ValueError to signal this specific status to the UI layer
raise ValueError("Repository is already prepared.") raise ValueError("Repository is already prepared.")
# Attempt preparation using GitCommands # Attempt preparation using GitCommands
@ -87,8 +90,8 @@ class ActionHandler:
self.logger.info("Repository prepared successfully.") self.logger.info("Repository prepared successfully.")
return True return True
except (GitCommandError, ValueError, IOError) as e: except (GitCommandError, ValueError, IOError) as e:
# Log and re-raise known errors # Log and re-raise known errors for the UI layer to handle
self.logger.error(f"Failed to prepare repository: {e}") self.logger.error(f"Failed to prepare repository: {e}", exc_info=True)
raise raise
except Exception as e: except Exception as e:
# Log and re-raise unexpected errors # Log and re-raise unexpected errors
@ -117,44 +120,45 @@ class ActionHandler:
Returns: Returns:
str or None: Path to created bundle on success, None if empty/not created. str or None: Path to created bundle on success, None if empty/not created.
Raises: Raises:
Exception: Relays exceptions from backup, commit, or bundle creation. IOError: If backup fails.
GitCommandError/ValueError/Exception: If commit or bundle creation fails.
""" """
# --- Backup Step --- # --- Backup Step ---
if autobackup_enabled: # _perform_backup_if_enabled raises IOError on failure
backup_success = self._perform_backup_if_enabled( self._perform_backup_if_enabled(
svn_path, profile_name, True, backup_base_dir, svn_path, profile_name, autobackup_enabled, backup_base_dir,
excluded_extensions, excluded_dirs excluded_extensions, excluded_dirs
) )
if not backup_success:
# Raise specific error if backup fails? Or just log and continue?
# For now, let's raise an error to stop the process.
raise IOError("Autobackup failed. Bundle creation aborted.")
# --- Autocommit Step --- # --- Autocommit Step ---
if autocommit_enabled: if autocommit_enabled:
self.logger.info("Autocommit before bundle is enabled.") self.logger.info("Autocommit before bundle is enabled.")
try: try:
# Check for changes before attempting commit
has_changes = self.git_commands.git_status_has_changes(svn_path) has_changes = self.git_commands.git_status_has_changes(svn_path)
if has_changes: if has_changes:
self.logger.info("Changes detected, performing autocommit...") self.logger.info("Changes detected, performing autocommit...")
# Use provided message or generate default
commit_msg_to_use = commit_message if commit_message else \ commit_msg_to_use = commit_message if commit_message else \
f"Autocommit '{profile_name}' before bundle" f"Autocommit '{profile_name}' before bundle"
self.logger.debug(f"Using autocommit message: '{commit_msg_to_use}'") self.logger.debug(f"Using autocommit message: '{commit_msg_to_use}'")
# Perform commit (logs success/nothing internally) # Perform commit (raises error on failure)
self.git_commands.git_commit(svn_path, commit_msg_to_use) self.git_commands.git_commit(svn_path, commit_msg_to_use)
# Log based on return value? git_commit already logs detail.
self.logger.info("Autocommit attempt finished.")
else: else:
self.logger.info("No changes detected for autocommit.") self.logger.info("No changes detected for autocommit.")
except Exception as commit_e: except Exception as commit_e:
# Let caller handle commit errors # Log and re-raise commit error to stop the process
self.logger.error(f"Autocommit failed: {commit_e}", exc_info=True) self.logger.error(f"Autocommit failed: {commit_e}", exc_info=True)
raise commit_e # Re-raise to abort bundle creation raise commit_e
# --- Create Bundle Step --- # --- Create Bundle Step ---
self.logger.info(f"Creating bundle file: {bundle_full_path}") self.logger.info(f"Creating bundle file: {bundle_full_path}")
try: try:
# Execute command via GitCommands # Execute command (raises error on failure)
self.git_commands.create_git_bundle(svn_path, bundle_full_path) self.git_commands.create_git_bundle(svn_path, bundle_full_path)
# Check result after command execution # Check result: file exists and is not empty
bundle_exists = os.path.exists(bundle_full_path) bundle_exists = os.path.exists(bundle_full_path)
bundle_not_empty = bundle_exists and os.path.getsize(bundle_full_path) > 0 bundle_not_empty = bundle_exists and os.path.getsize(bundle_full_path) > 0
if bundle_exists and bundle_not_empty: if bundle_exists and bundle_not_empty:
@ -164,12 +168,14 @@ class ActionHandler:
# Bundle empty or not created (logged by GitCommands) # Bundle empty or not created (logged by GitCommands)
self.logger.warning("Bundle file not created or is empty.") self.logger.warning("Bundle file not created or is empty.")
if bundle_exists and not bundle_not_empty: if bundle_exists and not bundle_not_empty:
try: os.remove(bundle_full_path) # Clean up # Clean up empty file
except OSError: pass try:
os.remove(bundle_full_path)
except OSError:
self.logger.warning(f"Could not remove empty bundle file.")
return None # Indicate non-fatal issue (empty bundle) return None # Indicate non-fatal issue (empty bundle)
except Exception as bundle_e: except Exception as bundle_e:
# Let caller handle bundle creation errors # Log and re-raise bundle creation errors
self.logger.error(f"Bundle creation failed: {bundle_e}", exc_info=True) self.logger.error(f"Bundle creation failed: {bundle_e}", exc_info=True)
raise bundle_e raise bundle_e
@ -183,27 +189,25 @@ class ActionHandler:
Args: See execute_create_bundle args, excluding commit related ones. Args: See execute_create_bundle args, excluding commit related ones.
Raises: Raises:
Exception: Relays exceptions from backup or fetch/merge operations. IOError: If backup fails.
GitCommandError/Exception: If fetch or merge fails (incl. conflicts).
""" """
# --- Backup Step --- # --- Backup Step ---
if autobackup_enabled: # Raises IOError on failure
backup_success = self._perform_backup_if_enabled( self._perform_backup_if_enabled(
svn_path, profile_name, True, backup_base_dir, svn_path, profile_name, autobackup_enabled, backup_base_dir,
excluded_extensions, excluded_dirs excluded_extensions, excluded_dirs
) )
if not backup_success:
raise IOError("Autobackup failed. Fetch operation aborted.")
# --- Fetch and Merge Step --- # --- Fetch and Merge Step ---
self.logger.info(f"Fetching into '{svn_path}' from: {bundle_full_path}") self.logger.info(f"Fetching into '{svn_path}' from: {bundle_full_path}")
try: try:
# Execute fetch/merge via GitCommands # Delegate to GitCommands; it raises GitCommandError on conflict/failure
# This method might raise GitCommandError for conflicts
self.git_commands.fetch_from_git_bundle(svn_path, bundle_full_path) self.git_commands.fetch_from_git_bundle(svn_path, bundle_full_path)
self.logger.info("Fetch/merge process completed.") self.logger.info("Fetch/merge process completed successfully.")
# Return True or some status? For now, rely on exceptions for errors. # No return value needed, success indicated by no exception
except Exception as fetch_e: except Exception as fetch_e:
# Let caller handle fetch/merge errors (incl. conflicts) # Log and re-raise any error from fetch/merge
self.logger.error(f"Fetch/merge failed: {fetch_e}", exc_info=True) self.logger.error(f"Fetch/merge failed: {fetch_e}", exc_info=True)
raise fetch_e raise fetch_e
@ -214,45 +218,46 @@ class ActionHandler:
Args: Args:
svn_path (str): Validated path to the repository. svn_path (str): Validated path to the repository.
commit_message (str): The commit message (checked not empty by caller). commit_message (str): The commit message (must not be empty).
Returns: Returns:
bool: True if a commit was made, False if no changes were committed. bool: True if commit made, False if no changes.
Raises: Raises:
GitCommandError: If the commit command fails. ValueError: If commit_message is empty.
Exception: For other unexpected errors. GitCommandError/Exception: If commit command fails.
""" """
if not commit_message: if not commit_message:
# This validation should ideally happen before calling this method # Should be validated by caller (UI layer)
self.logger.error("Manual commit attempted with empty message.") self.logger.error("Manual commit attempt with empty message.")
raise ValueError("Commit message cannot be empty.") raise ValueError("Commit message cannot be empty.")
self.logger.info(f"Executing manual commit for: {svn_path}") self.logger.info(f"Executing manual commit for: {svn_path}")
try: try:
# git_commit handles staging and committing # git_commit handles staging and commit attempt
# It returns True/False and raises GitCommandError on failure
commit_made = self.git_commands.git_commit(svn_path, commit_message) commit_made = self.git_commands.git_commit(svn_path, commit_message)
if commit_made: return commit_made
self.logger.info("Manual commit successful.")
else:
self.logger.info("Manual commit: Nothing to commit.")
return commit_made # Return status
except Exception as e: except Exception as e:
# Catch and re-raise errors from git_commit
self.logger.error(f"Manual commit failed: {e}", exc_info=True) self.logger.error(f"Manual commit failed: {e}", exc_info=True)
raise # Re-raise for caller to handle raise
def execute_create_tag(self, svn_path, commit_message, tag_name, tag_message): def execute_create_tag(self, svn_path, commit_message, tag_name, tag_message):
""" """
Executes tag creation, including pre-commit using commit_message if needed. Executes tag creation, including pre-commit using commit_message if needed.
Args: See execute_manual_commit and add tag_name, tag_message. Args:
svn_path (str): Validated path to repository.
commit_message (str): Message for pre-tag commit (if needed). Can be empty.
tag_name (str): Name for the new tag.
tag_message (str): Annotation message for the new tag.
Returns: Returns:
bool: True on successful tag creation. bool: True on successful tag creation.
Raises: Raises:
ValueError: If validation fails (e.g., changes exist but no commit message). ValueError: If validation fails (e.g., changes exist, no commit message).
GitCommandError: If commit or tag command fails. GitCommandError/Exception: If commit or tag command fails.
Exception: For unexpected errors.
""" """
self.logger.info(f"Executing create tag '{tag_name}' for: {svn_path}") self.logger.info(f"Executing create tag '{tag_name}' for: {svn_path}")
@ -265,31 +270,34 @@ class ActionHandler:
# Block if changes exist but no message provided # Block if changes exist but no message provided
msg = "Changes exist. Commit message required before tagging." msg = "Changes exist. Commit message required before tagging."
self.logger.error(f"Tag creation blocked: {msg}") self.logger.error(f"Tag creation blocked: {msg}")
raise ValueError(msg) raise ValueError(msg) # Raise error for UI layer
# Perform the pre-tag commit with the provided message # Perform the pre-tag commit with the provided message
self.logger.debug(f"Performing pre-tag commit: '{commit_message}'") self.logger.debug(f"Performing pre-tag commit: '{commit_message}'")
# git_commit raises error on failure, returns bool on success/no changes
self.git_commands.git_commit(svn_path, commit_message) self.git_commands.git_commit(svn_path, commit_message)
# Log success/nothing based on return value if needed
self.logger.info("Pre-tag commit attempt finished.") self.logger.info("Pre-tag commit attempt finished.")
else: else:
self.logger.info("No uncommitted changes detected.") self.logger.info("No uncommitted changes detected before tagging.")
except Exception as e: except Exception as e:
# Catch errors during status check or commit # Catch errors during status check or commit
self.logger.error(f"Error during pre-tag commit step: {e}", exc_info=True) self.logger.error(f"Error during pre-tag commit step: {e}",
exc_info=True)
raise # Re-raise commit-related errors raise # Re-raise commit-related errors
# --- Create Tag Step --- # --- Create Tag Step ---
self.logger.info(f"Proceeding to create tag '{tag_name}'...") self.logger.info(f"Proceeding to create tag '{tag_name}'...")
try: try:
# git_commands.create_tag handles validation and execution # git_commands.create_tag handles its own validation and execution
# It raises ValueError for invalid name, GitCommandError for exists/fail
self.git_commands.create_tag(svn_path, tag_name, tag_message) self.git_commands.create_tag(svn_path, tag_name, tag_message)
self.logger.info(f"Tag '{tag_name}' created successfully.") self.logger.info(f"Tag '{tag_name}' created successfully.")
return True # Indicate success return True # Indicate success
except Exception as e: except Exception as e:
# Catch errors during tag creation (e.g., exists, invalid name) # Catch errors during tag creation
self.logger.error(f"Failed to create tag '{tag_name}': {e}", exc_info=True) self.logger.error(f"Failed to create tag '{tag_name}': {e}",
exc_info=True)
raise # Re-raise tag creation errors raise # Re-raise tag creation errors
@ -299,47 +307,155 @@ class ActionHandler:
Args: Args:
svn_path (str): Validated path to repository. svn_path (str): Validated path to repository.
tag_name (str): The tag name to check out (already validated). tag_name (str): The tag name to check out (already validated by caller).
Returns: Returns:
bool: True on successful checkout. bool: True on successful checkout.
Raises: Raises:
ValueError: If uncommitted changes exist. ValueError: If uncommitted changes exist.
GitCommandError: If status check or checkout command fails. GitCommandError/Exception: If status check or checkout fails.
Exception: For unexpected errors.
""" """
if not tag_name: if not tag_name:
# Should be validated by caller, but double-check raise ValueError("Tag name required for checkout.") # Should be caught earlier
raise ValueError("Tag name required for checkout.")
self.logger.info(f"Executing checkout for tag '{tag_name}' in: {svn_path}") self.logger.info(f"Executing checkout tag '{tag_name}' in: {svn_path}")
# --- Check for Uncommitted Changes --- # --- Check for Uncommitted Changes ---
try: try:
has_changes = self.git_commands.git_status_has_changes(svn_path) has_changes = self.git_commands.git_status_has_changes(svn_path)
if has_changes: if has_changes:
self.logger.error("Checkout blocked: Uncommitted changes exist.") msg = "Uncommitted changes exist. Commit or stash first."
# Raise specific error for UI to handle clearly self.logger.error(f"Checkout blocked: {msg}")
raise ValueError("Uncommitted changes exist. Commit or stash first.") raise ValueError(msg) # Raise specific error for UI
self.logger.debug("No uncommitted changes found.") self.logger.debug("No uncommitted changes found.")
except Exception as e: except Exception as e:
# Catch errors during status check # Catch errors during status check
self.logger.error(f"Error checking status before checkout: {e}", self.logger.error(f"Status check error before checkout: {e}",
exc_info=True) exc_info=True)
raise # Re-raise status check errors raise # Re-raise status check errors
# --- Execute Checkout --- # --- Execute Checkout ---
try: try:
# git_commands.checkout_tag raises GitCommandError on failure
checkout_success = self.git_commands.checkout_tag(svn_path, tag_name) checkout_success = self.git_commands.checkout_tag(svn_path, tag_name)
# git_commands.checkout_tag raises error on failure # If no exception, assume success
if checkout_success: self.logger.info(f"Tag '{tag_name}' checked out.")
self.logger.info(f"Tag '{tag_name}' checked out successfully.")
return True return True
else:
# This path should theoretically not be reached if check=True used
self.logger.error("Checkout command reported failure unexpectedly.")
# Raise generic error if this happens?
raise GitCommandError(f"Checkout failed for '{tag_name}' for unknown reasons.")
except Exception as e: except Exception as e:
self.logger.error(f"Failed to checkout tag '{tag_name}': {e}", exc_info=True) # Catch errors during checkout (e.g., tag not found)
self.logger.error(f"Failed to checkout tag '{tag_name}': {e}",
exc_info=True)
raise # Re-raise checkout errors raise # Re-raise checkout errors
# --- Branch Actions ---
def execute_create_branch(self, svn_path, branch_name, start_point=None):
"""
Executes branch creation.
Args: See git_commands.create_branch
Returns: True on success.
Raises: GitCommandError/ValueError/Exception on failure.
"""
self.logger.info(f"Executing create branch '{branch_name}' "
f"from '{start_point or 'HEAD'}'.")
try:
# Delegate to git_commands, raises error on failure
self.git_commands.create_branch(svn_path, branch_name, start_point)
return True
except Exception as e:
self.logger.error(f"Failed to create branch: {e}", exc_info=True)
raise
def execute_switch_branch(self, svn_path, branch_name):
"""
Executes branch switch after checking for changes.
Args: See git_commands.checkout_branch
Returns: True on success.
Raises: ValueError (changes exist), GitCommandError/Exception on failure.
"""
if not branch_name:
raise ValueError("Branch name required for switch.")
self.logger.info(f"Executing switch to branch '{branch_name}'.")
# --- Check for Uncommitted Changes ---
try:
has_changes = self.git_commands.git_status_has_changes(svn_path)
if has_changes:
msg = "Uncommitted changes exist. Commit or stash first."
self.logger.error(f"Switch blocked: {msg}")
raise ValueError(msg) # Raise specific error
self.logger.debug("No uncommitted changes found.")
except Exception as e:
self.logger.error(f"Status check error before switch: {e}",
exc_info=True)
raise # Re-raise status check errors
# --- Execute Switch ---
try:
# Delegate to git_commands, raises error on failure
success = self.git_commands.checkout_branch(svn_path, branch_name)
return success
except Exception as e:
self.logger.error(f"Failed switch to branch '{branch_name}': {e}",
exc_info=True)
raise
def execute_delete_branch(self, svn_path, branch_name, force=False):
"""
Executes branch deletion.
Args: See git_commands.delete_branch
Returns: True on success.
Raises: GitCommandError/ValueError/Exception on failure.
"""
if not branch_name:
raise ValueError("Branch name required for delete.")
# Add checks for main/master? Done in UI layer.
self.logger.info(f"Executing delete branch '{branch_name}' (force={force}).")
try:
# Delegate to git_commands, raises error on failure
success = self.git_commands.delete_branch(svn_path, branch_name, force)
return success
except Exception as e:
# Catch errors (like not fully merged if force=False)
self.logger.error(f"Failed delete branch '{branch_name}': {e}",
exc_info=True)
raise
# --- ADDED: Delete Tag Action ---
def execute_delete_tag(self, svn_path, tag_name):
"""
Executes deletion for the specified tag.
Args:
svn_path (str): Validated path to repository.
tag_name (str): The tag name to delete (validated by caller).
Returns:
bool: True on successful deletion.
Raises:
GitCommandError/ValueError/Exception: If delete fails.
"""
if not tag_name:
# Should be validated by caller (UI layer)
raise ValueError("Tag name required for deletion.")
self.logger.info(f"Executing delete tag '{tag_name}' in: {svn_path}")
try:
# Delegate deletion to GitCommands method
# This raises GitCommandError if tag not found or other git error
success = self.git_commands.delete_tag(svn_path, tag_name)
return success # Should be True if no exception
except Exception as e:
# Catch and re-raise errors from git_commands.delete_tag
self.logger.error(f"Failed to delete tag '{tag_name}': {e}",
exc_info=True)
raise # Re-raise for the UI layer to handle

View File

@ -4,10 +4,6 @@ import datetime
import zipfile import zipfile
import logging import logging
# Note: Assumes ConfigManager is available via dependency injection or other means
# if needed for settings beyond exclusions passed directly.
# For now, it only needs exclusions passed to the create method.
class BackupHandler: class BackupHandler:
"""Handles the creation of ZIP backups with exclusions.""" """Handles the creation of ZIP backups with exclusions."""
@ -20,6 +16,9 @@ class BackupHandler:
""" """
self.logger = logger self.logger = logger
# Note: _parse_exclusions was moved into GitUtilityApp as it needs direct access
# to ConfigManager based on the current profile selected in the UI.
# The create_zip_backup method now receives the parsed exclusions directly.
def create_zip_backup(self, source_repo_path, backup_base_dir, def create_zip_backup(self, source_repo_path, backup_base_dir,
profile_name, excluded_extensions, excluded_dirs_base): profile_name, excluded_extensions, excluded_dirs_base):
@ -54,20 +53,21 @@ class BackupHandler:
if not backup_base_dir: if not backup_base_dir:
raise ValueError("Backup base directory cannot be empty.") raise ValueError("Backup base directory cannot be empty.")
# Ensure backup directory exists # Ensure backup directory exists, create if necessary
if not os.path.isdir(backup_base_dir): if not os.path.isdir(backup_base_dir):
self.logger.info(f"Creating backup base directory: {backup_base_dir}") self.logger.info(f"Creating backup base directory: {backup_base_dir}")
try: try:
# exist_ok=True prevents error if directory already exists
os.makedirs(backup_base_dir, exist_ok=True) os.makedirs(backup_base_dir, exist_ok=True)
except OSError as e: except OSError as e:
self.logger.error(f"Cannot create backup directory: {e}", self.logger.error(f"Cannot create backup directory: {e}",
exc_info=True) exc_info=True)
# Re-raise as IOError for the caller # Re-raise as IOError for the caller to potentially handle differently
raise IOError(f"Could not create backup directory: {e}") from e raise IOError(f"Could not create backup directory: {e}") from e
# --- 2. Construct Backup Filename --- # --- 2. Construct Backup Filename ---
now_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") now_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# Sanitize profile name for use in filename # Sanitize profile name for use in filename (remove potentially invalid chars)
safe_profile = "".join(c for c in profile_name safe_profile = "".join(c for c in profile_name
if c.isalnum() or c in '_-').rstrip() or "profile" if c.isalnum() or c in '_-').rstrip() or "profile"
backup_filename = f"{now_str}_backup_{safe_profile}.zip" backup_filename = f"{now_str}_backup_{safe_profile}.zip"
@ -78,20 +78,25 @@ class BackupHandler:
files_added = 0 files_added = 0
files_excluded = 0 files_excluded = 0
dirs_excluded = 0 dirs_excluded = 0
zip_f = None # Initialize zip file object zip_f = None # Initialize zip file object outside try block
try: try:
# Open ZIP file with appropriate settings # Open ZIP file with settings for compression and large files
zip_f = zipfile.ZipFile(backup_full_path, 'w', zip_f = zipfile.ZipFile(backup_full_path, 'w',
compression=zipfile.ZIP_DEFLATED, compression=zipfile.ZIP_DEFLATED,
allowZip64=True) # Support large archives allowZip64=True)
# Walk through the source directory # Walk through the source directory tree
for root, dirs, files in os.walk(source_repo_path, topdown=True): for root, dirs, files in os.walk(source_repo_path, topdown=True):
# --- Directory Exclusion --- # --- Directory Exclusion ---
original_dirs = list(dirs) # Copy before modifying # Keep a copy of original dirs list before modifying it in-place
# Exclude based on base name (case-insensitive) original_dirs = list(dirs)
# Filter the dirs list: keep only those NOT in excluded_dirs_base
# Compare lowercase names for case-insensitivity
dirs[:] = [d for d in dirs if d.lower() not in excluded_dirs_base] dirs[:] = [d for d in dirs if d.lower() not in excluded_dirs_base]
# Log excluded directories for this level
# Log excluded directories for this level if any were removed
excluded_dirs_now = set(original_dirs) - set(dirs) excluded_dirs_now = set(original_dirs) - set(dirs)
if excluded_dirs_now: if excluded_dirs_now:
dirs_excluded += len(excluded_dirs_now) dirs_excluded += len(excluded_dirs_now)
@ -115,36 +120,37 @@ class BackupHandler:
# If not excluded, add file to ZIP # If not excluded, add file to ZIP
file_full_path = os.path.join(root, filename) file_full_path = os.path.join(root, filename)
# Store with relative path inside ZIP archive # Calculate relative path for storage inside ZIP archive
archive_name = os.path.relpath(file_full_path, source_repo_path) archive_name = os.path.relpath(file_full_path, source_repo_path)
try: try:
# Write the file to the ZIP archive
zip_f.write(file_full_path, arcname=archive_name) zip_f.write(file_full_path, arcname=archive_name)
files_added += 1 files_added += 1
# Log progress occasionally for large backups # Log progress periodically for large backups
if files_added % 500 == 0: if files_added % 500 == 0:
self.logger.debug(f"Added {files_added} files...") self.logger.debug(f"Added {files_added} files...")
except Exception as write_e: except Exception as write_e:
# Log error writing specific file but allow backup to continue # Log error writing a specific file but allow backup to continue
self.logger.error( self.logger.error(
f"Error writing file '{file_full_path}' to ZIP: {write_e}", f"Error writing file '{file_full_path}' to ZIP: {write_e}",
exc_info=True exc_info=True
) )
# Mark backup potentially incomplete? For now, just log. # Consider marking the backup as potentially incomplete
# Log final summary after successful walk # Log final summary after successful walk and write attempts
self.logger.info(f"Backup ZIP creation process finished: {backup_full_path}") self.logger.info(f"Backup ZIP creation finished: {backup_full_path}")
self.logger.info( self.logger.info(
f"Summary - Added: {files_added}, Excl Files: {files_excluded}, " f"Summary - Added: {files_added}, Excl Files: {files_excluded}, "
f"Excl Dirs: {dirs_excluded}" f"Excl Dirs: {dirs_excluded}"
) )
# Return the path of the created zip file on success # Return the full path of the created ZIP file on success
return backup_full_path return backup_full_path
except (OSError, zipfile.BadZipFile) as e: except (OSError, zipfile.BadZipFile) as e:
# Handle OS errors and specific ZIP errors # Handle OS errors (permissions, disk space) and ZIP format errors
self.logger.error(f"Error creating backup ZIP: {e}", exc_info=True) self.logger.error(f"Error creating backup ZIP: {e}", exc_info=True)
# Re-raise as specific types or a general IOError # Re-raise as IOError for the caller to potentially handle specifically
raise IOError(f"Failed to create backup ZIP: {e}") from e raise IOError(f"Failed to create backup ZIP: {e}") from e
except Exception as e: except Exception as e:
# Catch any other unexpected error during the process # Catch any other unexpected error during the process
@ -152,17 +158,18 @@ class BackupHandler:
# Re-raise the original exception # Re-raise the original exception
raise raise
finally: finally:
# Ensure the ZIP file is always closed # Ensure the ZIP file is always closed, even if errors occurred
if zip_f: if zip_f:
zip_f.close() zip_f.close()
self.logger.debug(f"ZIP file '{backup_full_path}' closed.") self.logger.debug(f"ZIP file '{backup_full_path}' closed.")
# Clean up potentially empty/failed ZIP file # Clean up potentially empty or failed ZIP file
zip_exists = os.path.exists(backup_full_path) zip_exists = os.path.exists(backup_full_path)
# Check if zip exists but no files were added # Check if zip exists but no files were actually added
if zip_exists and files_added == 0: if zip_exists and files_added == 0:
self.logger.warning(f"Backup ZIP is empty: {backup_full_path}") self.logger.warning(f"Backup ZIP is empty: {backup_full_path}")
try: try:
# Attempt to remove the empty zip file
os.remove(backup_full_path) os.remove(backup_full_path)
self.logger.info("Removed empty backup ZIP file.") self.logger.info("Removed empty backup ZIP file.")
except OSError as rm_e: except OSError as rm_e:
@ -170,5 +177,6 @@ class BackupHandler:
self.logger.error(f"Failed remove empty backup ZIP: {rm_e}") self.logger.error(f"Failed remove empty backup ZIP: {rm_e}")
elif not zip_exists and files_added > 0: elif not zip_exists and files_added > 0:
# This case indicates an issue if files were supposedly added # This case indicates an issue if files were supposedly added
# but the zip file doesn't exist at the end (perhaps deleted?)
self.logger.error("Backup process finished but ZIP file missing.") self.logger.error("Backup process finished but ZIP file missing.")
# Consider raising an error here? # Consider raising an error here if this state is critical

File diff suppressed because it is too large Load Diff

699
gui.py

File diff suppressed because it is too large Load Diff