316 lines
14 KiB
Python
316 lines
14 KiB
Python
# config_manager.py
|
|
import configparser
|
|
import os
|
|
import logging
|
|
|
|
# Constants
|
|
CONFIG_FILE = "git_svn_sync.ini"
|
|
DEFAULT_PROFILE = "default"
|
|
# This is the 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 for the Git SVN Sync tool.
|
|
Handles loading, saving, and accessing configuration settings.
|
|
Ensures default profile and all keys exist in all profiles.
|
|
"""
|
|
|
|
def __init__(self, logger):
|
|
"""
|
|
Initializes the ConfigManager with a logger instance.
|
|
Args:
|
|
logger (logging.Logger or logging.LoggerAdapter): Logger instance.
|
|
"""
|
|
if not isinstance(logger, (logging.Logger, logging.LoggerAdapter)):
|
|
raise TypeError("logger must be a Logger or LoggerAdapter instance")
|
|
self.logger = logger
|
|
self.config = configparser.ConfigParser()
|
|
# Carica la configurazione all'inizializzazione
|
|
self.load_config() # Ensure config is loaded
|
|
|
|
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/svn/working/copy",
|
|
"usb_drive_path": "/media/usb",
|
|
"bundle_name": "my_bundle.bundle",
|
|
"bundle_name_updated": "my_updated_bundle.bundle",
|
|
"autobackup": "False",
|
|
"backup_dir": DEFAULT_BACKUP_DIR, # Use the global constant
|
|
"autocommit": "False",
|
|
"commit_message": "",
|
|
"backup_exclude_extensions": ".log,.tmp", # Example default from original
|
|
# --- MODIFICA: Aggiunta Nuova Chiave ---
|
|
# Key to store comma-separated list of directory names to exclude from backup.
|
|
"backup_exclude_dirs": "__pycache__,.venv,build,dist,node_modules",
|
|
# --- FINE MODIFICA ---
|
|
}
|
|
|
|
def load_config(self):
|
|
"""
|
|
Loads the configuration from CONFIG_FILE.
|
|
Creates the file with defaults if it doesn't exist.
|
|
Ensures all profiles have all expected keys, adding defaults if missing.
|
|
"""
|
|
needs_save = (
|
|
False # Flag to track if config needs saving after loading/checking
|
|
)
|
|
try:
|
|
config_exists = os.path.exists(CONFIG_FILE)
|
|
files_read = []
|
|
if config_exists:
|
|
# Use try-except specifically for reading to handle potential encoding/parsing issues
|
|
try:
|
|
files_read = self.config.read(CONFIG_FILE, encoding="utf-8")
|
|
if not files_read and config_exists:
|
|
# File exists but was empty or unreadable by configparser
|
|
self.logger.warning(
|
|
f"Config file '{CONFIG_FILE}' exists but could not be read or is empty. Will recreate structure."
|
|
)
|
|
# Reset parser and treat as if file didn't exist for default creation logic
|
|
self.config = configparser.ConfigParser()
|
|
config_exists = False # Force default profile creation below
|
|
except configparser.Error as read_err:
|
|
self.logger.error(
|
|
f"Error parsing config file '{CONFIG_FILE}': {read_err}. Will attempt to recreate with defaults.",
|
|
exc_info=True,
|
|
)
|
|
self.config = configparser.ConfigParser()
|
|
config_exists = False # Force default profile creation below
|
|
|
|
if not config_exists:
|
|
# File doesn't exist or failed to parse/was empty
|
|
self.logger.info(
|
|
f"Config file '{CONFIG_FILE}' not found or invalid. Creating with default profile and settings."
|
|
)
|
|
self.config = configparser.ConfigParser() # Ensure clean parser state
|
|
self._create_default_profile()
|
|
needs_save = True # Need to save the newly created default profile
|
|
else:
|
|
self.logger.debug(
|
|
f"Successfully read configuration from '{CONFIG_FILE}'. Validating profiles..."
|
|
)
|
|
# File exists and was read, ensure default profile and all keys are present in all profiles
|
|
|
|
# --- Ensure Default Profile Exists and Has All Keys ---
|
|
if DEFAULT_PROFILE not in self.config.sections():
|
|
self.logger.warning(
|
|
f"Default profile '{DEFAULT_PROFILE}' missing. Adding it with default values."
|
|
)
|
|
self._create_default_profile() # Creates section and adds all keys
|
|
needs_save = True
|
|
else:
|
|
# Default profile exists, check for 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):
|
|
self.logger.warning(
|
|
f"Key '{key}' missing in profile '{DEFAULT_PROFILE}'. Adding default: '{default_value}'."
|
|
)
|
|
self.config.set(DEFAULT_PROFILE, key, default_value)
|
|
needs_save = True
|
|
|
|
# --- Ensure ALL Profiles Have ALL Keys (including the new backup_exclude_dirs) ---
|
|
expected_keys = self._get_expected_keys_with_defaults()
|
|
all_current_profiles = (
|
|
self.config.sections()
|
|
) # Get possibly updated list
|
|
for profile_name in all_current_profiles:
|
|
# Skip default, already checked
|
|
if profile_name == DEFAULT_PROFILE:
|
|
continue
|
|
# Check each expected key for this profile
|
|
for key, default_value in expected_keys.items():
|
|
if not self.config.has_option(profile_name, key):
|
|
self.logger.warning(
|
|
f"Key '{key}' missing in profile '{profile_name}'. Adding default: '{default_value}'."
|
|
)
|
|
self.config.set(profile_name, key, default_value)
|
|
needs_save = True # Mark for saving
|
|
|
|
# Save the configuration file only if changes were made
|
|
if needs_save:
|
|
self.logger.info(
|
|
f"Configuration updated with missing profiles/keys. Saving to '{CONFIG_FILE}'."
|
|
)
|
|
self.save_config()
|
|
|
|
except Exception as e:
|
|
# Catch unexpected errors during the load/validation process
|
|
self.logger.error(
|
|
f"Unexpected error loading or validating configuration: {e}",
|
|
exc_info=True,
|
|
)
|
|
# Reset to a safe state (empty config) in case of severe issues
|
|
self.config = configparser.ConfigParser()
|
|
|
|
def _create_default_profile(self):
|
|
"""Creates the default profile section or ensures it has all default keys."""
|
|
if DEFAULT_PROFILE not in self.config.sections():
|
|
self.logger.debug(f"Adding default profile section '{DEFAULT_PROFILE}'.")
|
|
self.config.add_section(DEFAULT_PROFILE)
|
|
|
|
# Get expected keys and set them for the default profile
|
|
expected_keys = self._get_expected_keys_with_defaults()
|
|
for key, default_value in expected_keys.items():
|
|
self.config.set(
|
|
DEFAULT_PROFILE, key, default_value
|
|
) # set() handles adding/updating
|
|
|
|
self.logger.info(
|
|
f"Ensured default profile '{DEFAULT_PROFILE}' exists with default keys/values."
|
|
)
|
|
|
|
def save_config(self):
|
|
"""Saves the current configuration state to the CONFIG_FILE."""
|
|
# Check/Create directory if needed (from previous version, good practice)
|
|
config_dir = os.path.dirname(CONFIG_FILE)
|
|
if config_dir and not os.path.exists(config_dir):
|
|
try:
|
|
os.makedirs(config_dir)
|
|
self.logger.info(f"Created directory for config file: {config_dir}")
|
|
except OSError as e:
|
|
self.logger.error(
|
|
f"Could not create directory '{config_dir}': {e}. Save might fail.",
|
|
exc_info=True,
|
|
)
|
|
try:
|
|
# Write the config object to the file
|
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
|
self.config.write(configfile)
|
|
self.logger.debug(f"Configuration saved to '{CONFIG_FILE}'.")
|
|
except IOError as e:
|
|
self.logger.error(
|
|
f"IOError writing configuration file '{CONFIG_FILE}': {e}",
|
|
exc_info=True,
|
|
)
|
|
# Consider raising the error depending on desired behavior
|
|
except Exception as e:
|
|
self.logger.error(
|
|
f"Unexpected error saving configuration: {e}", exc_info=True
|
|
)
|
|
|
|
def get_profile_sections(self):
|
|
"""Returns a sorted list of all profile section names."""
|
|
# Sort alphabetically for consistent display 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 or error.
|
|
|
|
Returns:
|
|
str or any: The option value as a string if found, otherwise the original fallback.
|
|
"""
|
|
try:
|
|
# Check if the profile section exists first for clearer logging
|
|
if not self.config.has_section(profile):
|
|
self.logger.warning(
|
|
f"Profile '{profile}' not found. Cannot get option '{option}'. Using fallback: {fallback}"
|
|
)
|
|
return fallback # Return original fallback type
|
|
|
|
# Use configparser's built-in fallback mechanism
|
|
str_fallback = str(fallback) if fallback is not None else None
|
|
value_str = self.config.get(profile, option, fallback=str_fallback)
|
|
|
|
# Check if fallback was used because the option was missing
|
|
option_was_missing = not self.config.has_option(profile, option)
|
|
# Careful: if fallback is None, str_fallback is None. config.get might return None if option value is empty.
|
|
# We need to distinguish between "option missing" and "option exists but is empty".
|
|
if value_str == str_fallback and option_was_missing:
|
|
self.logger.debug(
|
|
f"Option '{option}' not found in profile '{profile}'. Used fallback: {fallback}"
|
|
)
|
|
return fallback # Return original fallback type
|
|
|
|
# Return the value obtained (always a string from configparser)
|
|
return value_str
|
|
|
|
except Exception as e:
|
|
self.logger.error(
|
|
f"Error getting option '{option}' for profile '{profile}': {e}",
|
|
exc_info=True,
|
|
)
|
|
return fallback # Return fallback on error
|
|
|
|
def set_profile_option(self, profile, option, value):
|
|
"""
|
|
Sets an option in a profile section, creating the section if needed.
|
|
Stores the value as a string.
|
|
|
|
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).
|
|
"""
|
|
try:
|
|
# Create section if it doesn't exist
|
|
if not self.config.has_section(profile):
|
|
self.config.add_section(profile)
|
|
self.logger.info(f"Created new profile section during set: '{profile}'")
|
|
|
|
# Convert value to string for storage
|
|
value_str = str(value) if value is not None else ""
|
|
self.config.set(profile, option, value_str)
|
|
self.logger.debug(
|
|
f"Set option '{option}' = '{value_str}' in profile '{profile}'."
|
|
)
|
|
except configparser.Error as e:
|
|
self.logger.error(
|
|
f"ConfigParser error setting option '{option}' in profile '{profile}': {e}",
|
|
exc_info=True,
|
|
)
|
|
except Exception as e:
|
|
self.logger.error(
|
|
f"Unexpected error setting option '{option}' in profile '{profile}': {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
def remove_profile_section(self, profile):
|
|
"""
|
|
Removes a profile section (cannot remove the default profile).
|
|
|
|
Args:
|
|
profile (str): Name of the profile section to remove.
|
|
|
|
Returns:
|
|
bool: True if removed successfully, False otherwise.
|
|
"""
|
|
if profile == DEFAULT_PROFILE:
|
|
self.logger.warning(
|
|
f"Attempt to remove default profile '{DEFAULT_PROFILE}' denied."
|
|
)
|
|
return False
|
|
if self.config.has_section(profile):
|
|
try:
|
|
removed = self.config.remove_section(profile)
|
|
if removed:
|
|
self.logger.info(f"Removed profile section: '{profile}'")
|
|
return True
|
|
else: # Should not happen if has_section was True
|
|
self.logger.warning(
|
|
f"ConfigParser failed to remove section '{profile}' despite existence."
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(
|
|
f"Error removing profile section '{profile}': {e}", exc_info=True
|
|
)
|
|
return False
|
|
else:
|
|
self.logger.warning(
|
|
f"Profile section '{profile}' not found, cannot remove."
|
|
)
|
|
return False
|