change gui with notebook tab

This commit is contained in:
VALLONGOL 2025-04-14 09:02:34 +02:00
parent 23a5c593ed
commit a982c35c7d
7 changed files with 2682 additions and 2681 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +1,17 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis( a = Analysis(scripts=['C:\\src\\____GitProjects\\GitUtility\\GitUtility.py'], pathex=['C:\\src\\____GitProjects\\GitUtility'], binaries=[], datas=[('C:\\src\\____GitProjects\\GitUtility\\git_svn_sync.ini', '.')], hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=None, noarchive=False,)
['GitUtility.py'],
pathex=[],
binaries=[],
datas=[('git_svn_sync.ini', '.')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE( pyz = PYZ(a.pure, a.zipped_data, cipher=None)
pyz,
a.scripts, exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='GitUtility',
[], debug=False,
exclude_binaries=True, bootloader_ignore_signals=False,
name='GitUtility', strip=False,
debug=False, upx=True,
bootloader_ignore_signals=False, upx_exclude=[],
strip=False, runtime_tmpdir=None,
upx=True, console=False,)
console=False, coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='GitUtility',)
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='GitUtility',
)

View File

@ -6,10 +6,13 @@ import os
from git_commands import GitCommands, GitCommandError from git_commands import GitCommands, GitCommandError
from backup_handler import BackupHandler 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."""
def __init__(self, logger, git_commands: GitCommands, backup_handler: BackupHandler): def __init__(
self, logger, git_commands: GitCommands, backup_handler: BackupHandler
):
""" """
Initializes the ActionHandler. Initializes the ActionHandler.
@ -22,10 +25,15 @@ class ActionHandler:
self.git_commands = git_commands self.git_commands = git_commands
self.backup_handler = backup_handler self.backup_handler = backup_handler
def _perform_backup_if_enabled(
def _perform_backup_if_enabled(self, svn_path, profile_name, self,
autobackup_enabled, backup_base_dir, svn_path,
excluded_extensions, excluded_dirs): profile_name,
autobackup_enabled,
backup_base_dir,
excluded_extensions,
excluded_dirs,
):
""" """
Performs backup if enabled. Raises IOError on backup failure. Performs backup if enabled. Raises IOError on backup failure.
@ -42,7 +50,7 @@ class ActionHandler:
""" """
if not autobackup_enabled: if not autobackup_enabled:
self.logger.debug("Autobackup disabled, skipping backup.") self.logger.debug("Autobackup disabled, skipping backup.")
return # Backup not needed, proceed successfully return # Backup not needed, proceed successfully
self.logger.info("Autobackup enabled. Starting backup...") self.logger.info("Autobackup enabled. Starting backup...")
try: try:
@ -53,18 +61,17 @@ class ActionHandler:
backup_base_dir, backup_base_dir,
profile_name, profile_name,
excluded_extensions, excluded_extensions,
excluded_dirs excluded_dirs,
) )
# Log success if backup handler doesn't raise an error # Log success if backup handler doesn't raise an error
self.logger.info(f"Backup completed successfully: {backup_path}") self.logger.info(f"Backup completed successfully: {backup_path}")
# No explicit return value needed on success # No explicit return value needed on success
except Exception as backup_e: except Exception as backup_e:
# Log error and re-raise as IOError to signal critical 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)
# Standardize on IOError for backup failures passed up # Standardize on IOError for backup failures passed up
raise IOError(f"Autobackup failed: {backup_e}") from backup_e raise IOError(f"Autobackup failed: {backup_e}") from backup_e
def execute_prepare_repo(self, svn_path): def execute_prepare_repo(self, svn_path):
""" """
@ -98,11 +105,18 @@ class ActionHandler:
self.logger.error(f"Failed to prepare repository: {e}", exc_info=True) self.logger.error(f"Failed to prepare repository: {e}", exc_info=True)
raise raise
def execute_create_bundle(
def execute_create_bundle(self, svn_path, bundle_full_path, profile_name, self,
autobackup_enabled, backup_base_dir, svn_path,
autocommit_enabled, commit_message, bundle_full_path,
excluded_extensions, excluded_dirs): profile_name,
autobackup_enabled,
backup_base_dir,
autocommit_enabled,
commit_message,
excluded_extensions,
excluded_dirs,
):
""" """
Executes 'create bundle', including backup and commit logic. Executes 'create bundle', including backup and commit logic.
@ -126,8 +140,12 @@ class ActionHandler:
# --- Backup Step --- # --- Backup Step ---
# _perform_backup_if_enabled raises IOError on failure # _perform_backup_if_enabled raises IOError on failure
self._perform_backup_if_enabled( self._perform_backup_if_enabled(
svn_path, profile_name, autobackup_enabled, backup_base_dir, svn_path,
excluded_extensions, excluded_dirs profile_name,
autobackup_enabled,
backup_base_dir,
excluded_extensions,
excluded_dirs,
) )
# --- Autocommit Step --- # --- Autocommit Step ---
@ -139,9 +157,14 @@ class ActionHandler:
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 # Use provided message or generate default
commit_msg_to_use = commit_message if commit_message else \ commit_msg_to_use = (
f"Autocommit '{profile_name}' before bundle" commit_message
self.logger.debug(f"Using autocommit message: '{commit_msg_to_use}'") if commit_message
else f"Autocommit '{profile_name}' before bundle"
)
self.logger.debug(
f"Using autocommit message: '{commit_msg_to_use}'"
)
# Perform commit (raises error on failure, returns bool) # Perform commit (raises error on failure, returns bool)
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. # Log based on return value? git_commit already logs detail.
@ -162,27 +185,33 @@ class ActionHandler:
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:
self.logger.info("Git bundle created successfully.") self.logger.info("Git bundle created successfully.")
return bundle_full_path # Return path on success return bundle_full_path # Return path on success
else: else:
# 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:
# Clean up empty file # Clean up empty file
try: try:
os.remove(bundle_full_path) os.remove(bundle_full_path)
except OSError: except OSError:
self.logger.warning("Could not remove empty bundle file.") self.logger.warning("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:
# Log and re-raise 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
def execute_fetch_bundle(
def execute_fetch_bundle(self, svn_path, bundle_full_path, profile_name, self,
autobackup_enabled, backup_base_dir, svn_path,
excluded_extensions, excluded_dirs): bundle_full_path,
profile_name,
autobackup_enabled,
backup_base_dir,
excluded_extensions,
excluded_dirs,
):
""" """
Executes the 'fetch bundle' action, including backup logic. Executes the 'fetch bundle' action, including backup logic.
@ -195,8 +224,12 @@ class ActionHandler:
# --- Backup Step --- # --- Backup Step ---
# Raises IOError on failure # Raises IOError on failure
self._perform_backup_if_enabled( self._perform_backup_if_enabled(
svn_path, profile_name, autobackup_enabled, backup_base_dir, svn_path,
excluded_extensions, excluded_dirs profile_name,
autobackup_enabled,
backup_base_dir,
excluded_extensions,
excluded_dirs,
) )
# --- Fetch and Merge Step --- # --- Fetch and Merge Step ---
@ -207,10 +240,9 @@ class ActionHandler:
self.logger.info("Fetch/merge process completed successfully.") self.logger.info("Fetch/merge process completed successfully.")
# No return needed, success indicated by no exception # No return needed, success indicated by no exception
except Exception as fetch_e: except Exception as fetch_e:
# Log and re-raise any error from fetch/merge # 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
def execute_manual_commit(self, svn_path, commit_message): def execute_manual_commit(self, svn_path, commit_message):
""" """
@ -227,9 +259,9 @@ class ActionHandler:
GitCommandError/Exception: If commit command fails. GitCommandError/Exception: If commit command fails.
""" """
if not commit_message: if not commit_message:
# This validation should ideally happen before calling this method # This validation should ideally happen before calling this method
self.logger.error("Manual commit attempt 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:
@ -242,7 +274,6 @@ class ActionHandler:
self.logger.error(f"Manual commit failed: {e}", exc_info=True) self.logger.error(f"Manual commit failed: {e}", exc_info=True)
raise 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.
@ -270,22 +301,23 @@ 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 error for UI layer 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 otherwise # git_commit raises error on failure, returns bool otherwise
commit_made = self.git_commands.git_commit(svn_path, commit_message) commit_made = self.git_commands.git_commit(svn_path, commit_message)
# Log based on return value? git_commit logs details. # Log based on return value? git_commit logs details.
self.logger.info(f"Pre-tag commit attempt finished (made={commit_made}).") self.logger.info(
f"Pre-tag commit attempt finished (made={commit_made})."
)
else: else:
self.logger.info("No uncommitted changes detected before tagging.") 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}", self.logger.error(f"Error during pre-tag commit step: {e}", exc_info=True)
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}'...")
@ -294,13 +326,11 @@ class ActionHandler:
# It raises ValueError for invalid name, GitCommandError for exists/fail # 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 # Catch errors during tag creation
self.logger.error(f"Failed to create tag '{tag_name}': {e}", self.logger.error(f"Failed to create tag '{tag_name}': {e}", exc_info=True)
exc_info=True) raise # Re-raise tag creation errors
raise # Re-raise tag creation errors
def execute_checkout_tag(self, svn_path, tag_name): def execute_checkout_tag(self, svn_path, tag_name):
""" """
@ -327,31 +357,30 @@ class ActionHandler:
if has_changes: if has_changes:
msg = "Uncommitted changes exist. Commit or stash first." msg = "Uncommitted changes exist. Commit or stash first."
self.logger.error(f"Checkout blocked: {msg}") self.logger.error(f"Checkout blocked: {msg}")
raise ValueError(msg) # Raise specific error for UI 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"Status check error 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 error on failure # git_commands.checkout_tag raises error on failure
checkout_success = self.git_commands.checkout_tag(svn_path, tag_name) checkout_success = self.git_commands.checkout_tag(svn_path, tag_name)
if checkout_success: if checkout_success:
self.logger.info(f"Tag '{tag_name}' checked out.") self.logger.info(f"Tag '{tag_name}' checked out.")
return True return True
else: else:
# This path should theoretically not be reached # This path should theoretically not be reached
self.logger.error("Checkout command reported failure unexpectedly.") self.logger.error("Checkout command reported failure unexpectedly.")
raise GitCommandError("Checkout failed for unknown reason.") raise GitCommandError("Checkout failed for unknown reason.")
except Exception as e: except Exception as e:
# Catch errors during checkout (e.g., tag not found) # Catch errors during checkout (e.g., tag not found)
self.logger.error(f"Failed to checkout tag '{tag_name}': {e}", self.logger.error(
exc_info=True) f"Failed to checkout tag '{tag_name}': {e}", exc_info=True
raise # Re-raise checkout errors )
raise # Re-raise checkout errors
# --- Branch Actions --- # --- Branch Actions ---
def execute_create_branch(self, svn_path, branch_name, start_point=None): def execute_create_branch(self, svn_path, branch_name, start_point=None):
@ -363,8 +392,10 @@ class ActionHandler:
Returns: True on success. Returns: True on success.
Raises: GitCommandError/ValueError/Exception on failure. Raises: GitCommandError/ValueError/Exception on failure.
""" """
self.logger.info(f"Executing create branch '{branch_name}' " self.logger.info(
f"from '{start_point or 'HEAD'}'.") f"Executing create branch '{branch_name}' "
f"from '{start_point or 'HEAD'}'."
)
try: try:
# Delegate to git_commands, raises error on failure # Delegate to git_commands, raises error on failure
self.git_commands.create_branch(svn_path, branch_name, start_point) self.git_commands.create_branch(svn_path, branch_name, start_point)
@ -374,7 +405,6 @@ class ActionHandler:
self.logger.error(f"Failed to create branch: {e}", exc_info=True) self.logger.error(f"Failed to create branch: {e}", exc_info=True)
raise raise
def execute_switch_branch(self, svn_path, branch_name): def execute_switch_branch(self, svn_path, branch_name):
""" """
Executes branch switch after checking for changes. Executes branch switch after checking for changes.
@ -394,13 +424,12 @@ class ActionHandler:
if has_changes: if has_changes:
msg = "Uncommitted changes exist. Commit or stash first." msg = "Uncommitted changes exist. Commit or stash first."
self.logger.error(f"Switch blocked: {msg}") self.logger.error(f"Switch blocked: {msg}")
raise ValueError(msg) # Raise specific error for UI 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"Status check error before switch: {e}", self.logger.error(f"Status check error before switch: {e}", exc_info=True)
exc_info=True) raise # Re-raise status check errors
raise # Re-raise status check errors
# --- Execute Switch --- # --- Execute Switch ---
try: try:
@ -409,11 +438,11 @@ class ActionHandler:
return success return success
except Exception as e: except Exception as e:
# Catch errors during switch (e.g., branch not found) # Catch errors during switch (e.g., branch not found)
self.logger.error(f"Failed switch to branch '{branch_name}': {e}", self.logger.error(
exc_info=True) f"Failed switch to branch '{branch_name}': {e}", exc_info=True
)
raise raise
def execute_delete_branch(self, svn_path, branch_name, force=False): def execute_delete_branch(self, svn_path, branch_name, force=False):
""" """
Executes branch deletion. Executes branch deletion.
@ -434,11 +463,11 @@ class ActionHandler:
return success return success
except Exception as e: except Exception as e:
# Catch errors (like not merged, needs force) # Catch errors (like not merged, needs force)
self.logger.error(f"Failed delete branch '{branch_name}': {e}", self.logger.error(
exc_info=True) f"Failed delete branch '{branch_name}': {e}", exc_info=True
)
raise raise
def execute_delete_tag(self, svn_path, tag_name): def execute_delete_tag(self, svn_path, tag_name):
""" """
Executes deletion for the specified tag. Executes deletion for the specified tag.
@ -461,9 +490,8 @@ class ActionHandler:
# Delegate deletion to GitCommands method # Delegate deletion to GitCommands method
# This raises GitCommandError if tag not found or other git error # This raises GitCommandError if tag not found or other git error
success = self.git_commands.delete_tag(svn_path, tag_name) success = self.git_commands.delete_tag(svn_path, tag_name)
return success # Should be True if no exception return success # Should be True if no exception
except Exception as e: except Exception as e:
# Catch and re-raise errors from git_commands.delete_tag # Catch and re-raise errors from git_commands.delete_tag
self.logger.error(f"Failed to delete tag '{tag_name}': {e}", self.logger.error(f"Failed to delete tag '{tag_name}': {e}", exc_info=True)
exc_info=True) raise # Re-raise for the UI layer to handle
raise # Re-raise for the UI layer to handle

View File

@ -4,6 +4,7 @@ import datetime
import zipfile import zipfile
import logging import logging
class BackupHandler: class BackupHandler:
"""Handles the creation of ZIP backups with exclusions.""" """Handles the creation of ZIP backups with exclusions."""
@ -20,8 +21,14 @@ class BackupHandler:
# to ConfigManager based on the current profile selected in the UI. # to ConfigManager based on the current profile selected in the UI.
# The create_zip_backup method now receives the parsed exclusions directly. # 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(
profile_name, excluded_extensions, excluded_dirs_base): self,
source_repo_path,
backup_base_dir,
profile_name,
excluded_extensions,
excluded_dirs_base,
):
""" """
Creates a timestamped ZIP backup of the source repository directory, Creates a timestamped ZIP backup of the source repository directory,
respecting provided exclusions. respecting provided exclusions.
@ -60,16 +67,17 @@ class BackupHandler:
# exist_ok=True prevents error if directory already exists # 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 to potentially handle differently # 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 (remove potentially invalid chars) # Sanitize profile name for use in filename (remove potentially invalid chars)
safe_profile = "".join(c for c in profile_name safe_profile = (
if c.isalnum() or c in '_-').rstrip() or "profile" "".join(c for c in profile_name 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"
backup_full_path = os.path.join(backup_base_dir, backup_filename) backup_full_path = os.path.join(backup_base_dir, backup_filename)
self.logger.info(f"Target backup ZIP file: {backup_full_path}") self.logger.info(f"Target backup ZIP file: {backup_full_path}")
@ -78,13 +86,13 @@ 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 outside try block zip_f = None # Initialize zip file object outside try block
try: try:
# Open ZIP file with settings for compression and large files # Open ZIP file with settings for compression and large files
zip_f = zipfile.ZipFile(backup_full_path, 'w', zip_f = zipfile.ZipFile(
compression=zipfile.ZIP_DEFLATED, backup_full_path, "w", compression=zipfile.ZIP_DEFLATED, allowZip64=True
allowZip64=True) )
# Walk through the source directory tree # 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):
@ -101,8 +109,8 @@ class BackupHandler:
if excluded_dirs_now: if excluded_dirs_now:
dirs_excluded += len(excluded_dirs_now) dirs_excluded += len(excluded_dirs_now)
for ex_dir in excluded_dirs_now: for ex_dir in excluded_dirs_now:
path_excluded = os.path.join(root, ex_dir) path_excluded = os.path.join(root, ex_dir)
self.logger.debug(f"Excluding directory: {path_excluded}") self.logger.debug(f"Excluding directory: {path_excluded}")
# --- File Exclusion and Addition --- # --- File Exclusion and Addition ---
for filename in files: for filename in files:
@ -111,12 +119,14 @@ class BackupHandler:
file_ext_lower = file_ext.lower() file_ext_lower = file_ext.lower()
# Check exclusion rules (case-insensitive filename or extension) # Check exclusion rules (case-insensitive filename or extension)
if filename.lower() in excluded_dirs_base or \ if (
file_ext_lower in excluded_extensions: filename.lower() in excluded_dirs_base
or file_ext_lower in excluded_extensions
):
path_excluded = os.path.join(root, filename) path_excluded = os.path.join(root, filename)
self.logger.debug(f"Excluding file: {path_excluded}") self.logger.debug(f"Excluding file: {path_excluded}")
files_excluded += 1 files_excluded += 1
continue # Skip this file continue # Skip this file
# 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)
@ -129,14 +139,14 @@ class BackupHandler:
files_added += 1 files_added += 1
# Log progress periodically 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 a 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,
) )
# Consider marking the backup as potentially incomplete # Consider marking the backup as potentially incomplete
# Log final summary after successful walk and write attempts # Log final summary after successful walk and write attempts
self.logger.info(f"Backup ZIP creation finished: {backup_full_path}") self.logger.info(f"Backup ZIP creation finished: {backup_full_path}")
@ -148,15 +158,15 @@ class BackupHandler:
return backup_full_path return backup_full_path
except (OSError, zipfile.BadZipFile) as e: except (OSError, zipfile.BadZipFile) as e:
# Handle OS errors (permissions, disk space) and ZIP format 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 IOError for the caller to potentially handle specifically # 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
self.logger.exception(f"Unexpected error during ZIP backup: {e}") self.logger.exception(f"Unexpected error during ZIP backup: {e}")
# Re-raise the original exception # Re-raise the original exception
raise raise
finally: finally:
# Ensure the ZIP file is always closed, even if errors occurred # Ensure the ZIP file is always closed, even if errors occurred
if zip_f: if zip_f:
@ -167,16 +177,16 @@ class BackupHandler:
zip_exists = os.path.exists(backup_full_path) zip_exists = os.path.exists(backup_full_path)
# Check if zip exists but no files were actually 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 # 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:
# Log error if removal fails, but don't stop execution # Log error if removal fails, but don't stop execution
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?) # 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 if this state is critical # Consider raising an error here if this state is critical

File diff suppressed because it is too large Load Diff

1747
gui.py

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,10 @@
# profile_handler.py # profile_handler.py
import logging import logging
# Assuming ConfigManager handles its own constants now # Assuming ConfigManager handles its own constants now
from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR
class ProfileHandler: class ProfileHandler:
"""Handles loading, saving, adding, removing profiles via ConfigManager.""" """Handles loading, saving, adding, removing profiles via ConfigManager."""
@ -50,15 +52,16 @@ class ProfileHandler:
# Convert loaded string values back to appropriate types if needed # Convert loaded string values back to appropriate types if needed
# Example: Booleans # Example: Booleans
profile_data["autocommit"] = \ profile_data["autocommit"] = (
str(profile_data.get("autocommit", "False")).lower() == "true" str(profile_data.get("autocommit", "False")).lower() == "true"
profile_data["autobackup"] = \ )
profile_data["autobackup"] = (
str(profile_data.get("autobackup", "False")).lower() == "true" str(profile_data.get("autobackup", "False")).lower() == "true"
)
self.logger.debug(f"Loaded data for '{profile_name}': {profile_data}") self.logger.debug(f"Loaded data for '{profile_name}': {profile_data}")
return profile_data return profile_data
def save_profile_data(self, profile_name, profile_data): def save_profile_data(self, profile_name, profile_data):
""" """
Saves the provided data dictionary to the specified profile name. Saves the provided data dictionary to the specified profile name.
@ -90,21 +93,19 @@ class ProfileHandler:
# Assume other types can be directly converted to string # Assume other types can be directly converted to string
value_to_save = str(value) if value is not None else "" value_to_save = str(value) if value is not None else ""
self.config_manager.set_profile_option( self.config_manager.set_profile_option(profile_name, key, value_to_save)
profile_name, key, value_to_save
)
# Persist changes to the configuration file # Persist changes to the configuration file
self.config_manager.save_config() self.config_manager.save_config()
self.logger.info(f"Profile '{profile_name}' saved successfully.") self.logger.info(f"Profile '{profile_name}' saved successfully.")
return True return True
except Exception as e: except Exception as e:
self.logger.error(f"Error saving profile '{profile_name}': {e}", self.logger.error(
exc_info=True) f"Error saving profile '{profile_name}': {e}", exc_info=True
)
# Let the caller (main app) show the error message # Let the caller (main app) show the error message
return False return False
def add_new_profile(self, profile_name): def add_new_profile(self, profile_name):
""" """
Adds a new profile with default settings. Adds a new profile with default settings.
@ -129,13 +130,13 @@ class ProfileHandler:
defaults = self.config_manager._get_expected_keys_with_defaults() defaults = self.config_manager._get_expected_keys_with_defaults()
defaults["bundle_name"] = f"{profile_name}_repo.bundle" defaults["bundle_name"] = f"{profile_name}_repo.bundle"
defaults["bundle_name_updated"] = f"{profile_name}_update.bundle" defaults["bundle_name_updated"] = f"{profile_name}_update.bundle"
defaults["svn_working_copy_path"] = "" # Start empty defaults["svn_working_copy_path"] = "" # Start empty
defaults["usb_drive_path"] = "" # Start empty defaults["usb_drive_path"] = "" # Start empty
# Set all options for the new profile section # Set all options for the new profile section
# ConfigManager.set_profile_option creates the section if needed # ConfigManager.set_profile_option creates the section if needed
for key, value in defaults.items(): for key, value in defaults.items():
self.config_manager.set_profile_option(profile_name, key, value) self.config_manager.set_profile_option(profile_name, key, value)
# Save the configuration file # Save the configuration file
self.config_manager.save_config() self.config_manager.save_config()
@ -143,11 +144,11 @@ class ProfileHandler:
return True return True
except Exception as e: except Exception as e:
# Log unexpected errors during profile addition # Log unexpected errors during profile addition
self.logger.error(f"Error adding profile '{profile_name}': {e}", self.logger.error(
exc_info=True) f"Error adding profile '{profile_name}': {e}", exc_info=True
)
return False return False
def remove_existing_profile(self, profile_name): def remove_existing_profile(self, profile_name):
""" """
Removes an existing profile (cannot remove the default profile). Removes an existing profile (cannot remove the default profile).
@ -180,10 +181,13 @@ class ProfileHandler:
return True return True
else: else:
# ConfigManager should log the reason for failure # ConfigManager should log the reason for failure
self.logger.error(f"ConfigManager reported failure removing '{profile_name}'.") self.logger.error(
f"ConfigManager reported failure removing '{profile_name}'."
)
return False return False
except Exception as e: except Exception as e:
# Log unexpected errors during removal # Log unexpected errors during removal
self.logger.error(f"Error removing profile '{profile_name}': {e}", self.logger.error(
exc_info=True) f"Error removing profile '{profile_name}': {e}", exc_info=True
return False )
return False