# --- START OF FILE config_manager.py --- # config_manager.py import configparser import os # Rimosso import logging import sys # Per fallback print # Importa il nuovo gestore della coda log import log_handler # Constants CONFIG_FILE = "git_svn_sync.ini" DEFAULT_PROFILE = "default" # Single source of truth for the default backup directory path DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "git_svn_backup") class ConfigManager: """ Manages the configuration file (git_svn_sync.ini) for the tool. Handles loading, saving, and accessing profile settings using log_handler. Ensures default profile and all expected keys exist across profiles. """ # Rimosso logger da __init__ def __init__(self, logger_ignored=None): # Accetta argomento ma lo ignora """Initializes the ConfigManager.""" # Non c'è più self.logger # Inizializza il parser self.config = configparser.ConfigParser() # Carica la configurazione all'inizializzazione # Usa try-except per gestire errori critici durante il caricamento iniziale try: self.load_config() # Ensure config is loaded and validated log_handler.log_debug( "ConfigManager initialized and config loaded.", func_name="__init__" ) except Exception as e: # Log critico se il caricamento iniziale fallisce completamente # Usa print perché il logging potrebbe non essere ancora pronto print( f"CRITICAL ERROR: Failed to load initial configuration: {e}", file=sys.stderr, ) log_handler.log_critical( f"Failed to load initial configuration: {e}", func_name="__init__" ) # Inizializza con un config vuoto per permettere all'app di partire (forse) self.config = configparser.ConfigParser() # Potrebbe essere necessario sollevare l'eccezione qui per fermare l'app # raise RuntimeError("Failed to initialize configuration manager") from e def _get_expected_keys_with_defaults(self): """ Returns a dictionary of expected config keys and their default values. This is the single source of truth for profile keys. """ return { "svn_working_copy_path": "/path/to/your/repo", # Aggiornato default "usb_drive_path": "/path/to/your/usb", # Aggiornato default "bundle_name": "my_repo.bundle", "bundle_name_updated": "my_repo_update.bundle", "autobackup": "False", "backup_dir": DEFAULT_BACKUP_DIR, # Usa costante definita sopra "autocommit": "False", "commit_message": "", "backup_exclude_extensions": ".log,.tmp,.bak,~*", # Esempio default "backup_exclude_dirs": "__pycache__,.venv,.vscode,build,dist,node_modules,.git,.svn", # Default esteso } def load_config(self): """ Loads config from CONFIG_FILE, creates/validates defaults and keys. Uses log_handler for logging. """ func_name = "load_config" log_handler.log_debug( f"Attempting to load configuration from '{CONFIG_FILE}'...", func_name=func_name, ) needs_save = False # Flag to track if config needs saving after load/check try: config_exists = os.path.exists(CONFIG_FILE) files_read = [] if config_exists: # Try reading the existing file try: # Specify UTF-8 encoding for consistency files_read = self.config.read(CONFIG_FILE, encoding="utf-8") if not files_read and config_exists: # File exists but configparser couldn't read it (e.g., empty, corrupted format) log_handler.log_warning( f"Config file '{CONFIG_FILE}' exists but is empty or unreadable. Will recreate.", func_name=func_name, ) self.config = configparser.ConfigParser() # Reset parser config_exists = False # Force default creation logic except configparser.Error as read_err: log_handler.log_error( f"Error parsing config file '{CONFIG_FILE}': {read_err}. Recreating with defaults.", func_name=func_name, ) self.config = configparser.ConfigParser() # Reset parser config_exists = False # Force default creation logic # --- Create or Validate Configuration --- if not config_exists: # File doesn't exist or failed to parse log_handler.log_info( f"Config file '{CONFIG_FILE}' not found or invalid. Creating with default profile.", func_name=func_name, ) # Ensure clean parser state if recreating self.config = configparser.ConfigParser() self._create_default_profile() # Create the default section needs_save = True # Mark for saving the new default profile else: # File exists and was read successfully log_handler.log_debug( f"Successfully read from '{CONFIG_FILE}'. Validating content...", func_name=func_name, ) # --- Ensure Default Profile Exists and Has All Keys --- if DEFAULT_PROFILE not in self.config.sections(): log_handler.log_warning( f"Default profile '{DEFAULT_PROFILE}' missing. Adding with defaults.", func_name=func_name, ) self._create_default_profile() # Creates section and adds all keys needs_save = True else: # Default profile exists, check/add missing keys expected_keys = self._get_expected_keys_with_defaults() for key, default_value in expected_keys.items(): if not self.config.has_option(DEFAULT_PROFILE, key): log_handler.log_warning( f"Key '{key}' missing in profile '{DEFAULT_PROFILE}'. Adding default: '{default_value}'.", func_name=func_name, ) self.config.set(DEFAULT_PROFILE, key, default_value) needs_save = True # --- Ensure ALL Profiles Have ALL Expected Keys --- expected_keys = self._get_expected_keys_with_defaults() all_current_profiles = ( self.config.sections() ) # Get potentially updated list for profile_name in all_current_profiles: for key, default_value in expected_keys.items(): if not self.config.has_option(profile_name, key): log_handler.log_warning( f"Key '{key}' missing in profile '{profile_name}'. Adding default: '{default_value}'.", func_name=func_name, ) self.config.set(profile_name, key, default_value) needs_save = True # Mark for saving # Save configuration only if changes were made during loading/validation if needs_save: log_handler.log_info( f"Configuration updated. Saving changes to '{CONFIG_FILE}'.", func_name=func_name, ) self.save_config() # Call save method except Exception as e: # Catch unexpected errors during the load/validation process log_handler.log_exception( f"Unexpected error loading/validating config: {e}", func_name=func_name ) # Reset to a safe state (empty config) in case of severe issues self.config = configparser.ConfigParser() # Optionally re-raise or handle this critical failure def _create_default_profile(self): """Internal helper: Creates the default profile section with default keys/values.""" func_name = "_create_default_profile" if DEFAULT_PROFILE not in self.config.sections(): log_handler.log_debug( f"Adding default profile section '{DEFAULT_PROFILE}'.", func_name=func_name, ) self.config.add_section(DEFAULT_PROFILE) # Get expected keys and set them for the default profile expected_keys = self._get_expected_keys_with_defaults() log_handler.log_debug( f"Setting default keys for profile '{DEFAULT_PROFILE}'.", func_name=func_name, ) for key, default_value in expected_keys.items(): # configparser's set() handles both adding and updating options self.config.set(DEFAULT_PROFILE, key, default_value) log_handler.log_info( f"Ensured default profile '{DEFAULT_PROFILE}' exists with default keys.", func_name=func_name, ) def save_config(self): """Saves the current configuration state to the CONFIG_FILE.""" func_name = "save_config" log_handler.log_debug( f"Attempting to save configuration to '{CONFIG_FILE}'...", func_name=func_name, ) # Check/Create directory if config file is in a subdirectory (optional) config_dir = os.path.dirname(CONFIG_FILE) # Ensure directory exists only if CONFIG_FILE specifies one if config_dir and not os.path.exists(config_dir): try: os.makedirs(config_dir) log_handler.log_info( f"Created directory for config file: {config_dir}", func_name=func_name, ) except OSError as e: # Log error but proceed with saving attempt (might work in current dir) log_handler.log_error( f"Could not create directory '{config_dir}': {e}. Save might fail.", func_name=func_name, ) try: # Write the config object to the file using UTF-8 encoding with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: self.config.write(configfile) log_handler.log_info( f"Configuration saved successfully to '{CONFIG_FILE}'.", func_name=func_name, ) except IOError as e: # Log specific IO errors during save log_handler.log_error( f"IOError writing config file '{CONFIG_FILE}': {e}", func_name=func_name ) # Optionally re-raise depending on desired application behavior # raise except Exception as e: # Log any other unexpected errors during save log_handler.log_exception( f"Unexpected error saving configuration: {e}", func_name=func_name ) # Optionally re-raise # raise def get_profile_sections(self): """Returns a sorted list of all profile section names.""" # Sort alphabetically for consistent display order in UI return sorted(self.config.sections()) def get_profile_option(self, profile, option, fallback=None): """ Retrieves a specific option from a profile section, handling errors. Args: profile (str): Name of the profile section. option (str): Name of the option key. fallback (any, optional): Value to return if option/section not found. Returns: str | any: The option value as a string if found, otherwise the original fallback. """ func_name = "get_profile_option" try: # Check section existence first for clearer logging if not self.config.has_section(profile): log_handler.log_warning( f"Profile '{profile}' not found. Cannot get option '{option}'. Using fallback: {fallback}", func_name=func_name, ) return fallback # Return original fallback type # Use configparser's built-in fallback mechanism if option missing # Convert fallback to string for configparser, handling None str_fallback = str(fallback) if fallback is not None else None value_str = self.config.get(profile, option, fallback=str_fallback) # Check if the fallback was actually used because the option was missing option_was_missing = not self.config.has_option(profile, option) # Careful comparison needed if fallback could be None or empty string if value_str == str_fallback and option_was_missing: log_handler.log_debug( f"Option '{option}' not found in profile '{profile}'. Used fallback value.", func_name=func_name, ) return fallback # Return original fallback type if option was truly missing # Return the value obtained (always a string from configparser) return value_str except Exception as e: # Log unexpected errors during option retrieval log_handler.log_exception( f"Error getting option '{option}' for profile '{profile}': {e}", func_name=func_name, ) # Return the fallback value on any error return fallback def set_profile_option(self, profile, option, value): """ Sets an option in a profile section, creating the section if needed. Value is converted to string for storage. Uses log_handler. Args: profile (str): Name of the profile section. option (str): Name of the option key. value (any): Value to set (will be converted to string). """ func_name = "set_profile_option" try: # Create section if it doesn't exist if not self.config.has_section(profile): self.config.add_section(profile) log_handler.log_info( f"Created new profile section during set: '{profile}'", func_name=func_name, ) # Convert value to string for storage (handle None) value_str = str(value) if value is not None else "" # Set the option using configparser self.config.set(profile, option, value_str) log_handler.log_debug( f"Set option '{option}' = '{value_str}' in profile '{profile}'.", func_name=func_name, ) except configparser.Error as e: # Log specific configparser errors log_handler.log_error( f"ConfigParser error setting option '{option}' in '{profile}': {e}", func_name=func_name, ) except Exception as e: # Log other unexpected errors log_handler.log_exception( f"Unexpected error setting option '{option}' in '{profile}': {e}", func_name=func_name, ) def remove_profile_section(self, profile): """ Removes a profile section (cannot remove the default profile). Uses log_handler. Args: profile (str): Name of the profile section to remove. Returns: bool: True if removed successfully, False otherwise. """ func_name = "remove_profile_section" # Prevent removal of the default profile if profile == DEFAULT_PROFILE: log_handler.log_warning( f"Attempt to remove default profile '{DEFAULT_PROFILE}' denied.", func_name=func_name, ) return False # Check if the profile section exists if self.config.has_section(profile): try: # Attempt to remove the section removed = self.config.remove_section(profile) if removed: log_handler.log_info( f"Removed profile section: '{profile}'", func_name=func_name ) return True else: # Should not happen if has_section was True, but log anyway log_handler.log_warning( f"ConfigParser failed to remove existing section '{profile}'.", func_name=func_name, ) return False except Exception as e: # Log errors during removal log_handler.log_exception( f"Error removing profile section '{profile}': {e}", func_name=func_name, ) return False else: # Profile section not found log_handler.log_warning( f"Profile section '{profile}' not found, cannot remove.", func_name=func_name, ) return False def add_section(self, section_name): """Adds a new section if it doesn't exist.""" # Helper function, might be useful for add_profile logic if not self.config.has_section(section_name): self.config.add_section(section_name) log_handler.log_info( f"Added new config section: '{section_name}'", func_name="add_section" ) return True return False # Already existed # --- END OF FILE config_manager.py ---