473 lines
22 KiB
Python
473 lines
22 KiB
Python
# --- FILE: gitsync_tool/config/config_manager.py ---
|
|
|
|
import configparser
|
|
import os
|
|
import sys # Per fallback print
|
|
from typing import Optional, Dict, List, Any # Aggiunti type hints
|
|
|
|
# ---<<< MODIFICA IMPORT >>>---
|
|
# Importa usando il percorso assoluto dal pacchetto gitsync_tool
|
|
from gitutility.logging_setup import log_handler
|
|
|
|
# ---<<< FINE MODIFICA IMPORT >>>---
|
|
|
|
# --- Constants ---
|
|
CONFIG_FILE: str = "git_svn_sync.ini" # Nome file configurazione
|
|
DEFAULT_PROFILE: str = "default" # Nome del profilo di default
|
|
# Percorso di backup di default (usa os.path.join per compatibilità OS)
|
|
DEFAULT_BACKUP_DIR: str = os.path.join(
|
|
os.path.expanduser("~"), "git_sync_backups"
|
|
) # Nome cartella cambiato per chiarezza
|
|
# Nome di default per il remote Git
|
|
DEFAULT_REMOTE_NAME: str = "origin"
|
|
|
|
|
|
class ConfigManager:
|
|
"""
|
|
Manages the application's configuration file (git_svn_sync.ini).
|
|
Handles loading, saving, validating, and accessing profile settings.
|
|
Uses the centralized log_handler for logging operations.
|
|
Ensures a default profile exists and all expected keys are present
|
|
across all profiles, adding defaults where necessary.
|
|
"""
|
|
|
|
def __init__(
|
|
self, logger_ignored: Optional[Any] = None
|
|
): # Accetta argomento ma lo ignora
|
|
"""Initializes the ConfigManager and loads the configuration."""
|
|
# self.logger non è più usato
|
|
# Initialize the config parser instance
|
|
self.config: configparser.ConfigParser = configparser.ConfigParser()
|
|
|
|
# Load and validate the configuration during initialization
|
|
# Use try-except to handle critical errors during the initial load
|
|
try:
|
|
self.load_config() # This method now handles creation/validation
|
|
log_handler.log_debug(
|
|
"ConfigManager initialized and configuration loaded/validated.",
|
|
func_name="__init__",
|
|
)
|
|
except Exception as e:
|
|
# Log critical failure if initial load/validation fails completely
|
|
# Use print as a fallback since logging itself might depend on config/paths
|
|
print(
|
|
f"CRITICAL ERROR: Failed to initialize configuration: {e}",
|
|
file=sys.stderr,
|
|
)
|
|
log_handler.log_critical(
|
|
f"Failed to load/validate initial configuration: {e}",
|
|
func_name="__init__",
|
|
)
|
|
# Initialize with an empty config parser to allow the app to potentially
|
|
# start in a degraded state or prompt the user.
|
|
self.config = configparser.ConfigParser()
|
|
# Depending on app design, might want to raise RuntimeError here
|
|
# raise RuntimeError("Failed to initialize configuration manager") from e
|
|
|
|
def _get_expected_keys_with_defaults(self) -> Dict[str, str]:
|
|
"""
|
|
Returns a dictionary defining all expected config keys for a profile
|
|
and their default string values. This serves as the single source of truth.
|
|
"""
|
|
# Define all expected keys and their default values here
|
|
return {
|
|
# Core Paths & Names
|
|
"svn_working_copy_path": "", # Default to empty, user must provide
|
|
"usb_drive_path": "", # Default to empty
|
|
"bundle_name": "repository.bundle",
|
|
"bundle_name_updated": "repository_update.bundle",
|
|
# Backup Settings
|
|
"autobackup": "False", # Use string "True"/"False" for ini compatibility
|
|
"backup_dir": DEFAULT_BACKUP_DIR, # Use the defined constant
|
|
"backup_exclude_extensions": ".log, .tmp, .bak, ~*", # Example defaults
|
|
"backup_exclude_dirs": "__pycache__, .venv, .vscode, build, dist, node_modules, .git, .svn", # Extended defaults
|
|
# Commit Settings
|
|
"autocommit": "False",
|
|
"commit_message": "", # Default empty commit message
|
|
# Remote Settings
|
|
"remote_url": "", # Default to empty, user needs to configure
|
|
"remote_name": DEFAULT_REMOTE_NAME, # Use the defined constant 'origin'
|
|
}
|
|
|
|
def load_config(self) -> None:
|
|
"""
|
|
Loads configuration from CONFIG_FILE. Creates the file and/or default
|
|
profile with all expected keys if they don't exist. Validates existing
|
|
profiles to ensure all expected keys are present, adding defaults if missing.
|
|
Uses the centralized log_handler for logging.
|
|
"""
|
|
func_name: str = "load_config"
|
|
log_handler.log_debug(
|
|
f"Attempting to load configuration from '{CONFIG_FILE}'...",
|
|
func_name=func_name,
|
|
)
|
|
config_needs_saving: bool = False # Flag to track if changes require saving
|
|
|
|
try:
|
|
config_exists: bool = os.path.exists(CONFIG_FILE)
|
|
files_read: List[str] = []
|
|
|
|
if config_exists:
|
|
try:
|
|
# Attempt to read the configuration file using UTF-8
|
|
files_read = self.config.read(CONFIG_FILE, encoding="utf-8")
|
|
# Handle case where file exists but is empty or unreadable by configparser
|
|
if not files_read and config_exists:
|
|
log_handler.log_warning(
|
|
f"Configuration file '{CONFIG_FILE}' exists but is empty or could not be parsed. Will recreate with defaults.",
|
|
func_name=func_name,
|
|
)
|
|
# Reset config object to start fresh
|
|
self.config = configparser.ConfigParser()
|
|
config_exists = False # Treat as if file didn't exist
|
|
except configparser.Error as read_err:
|
|
# Handle parsing errors (e.g., invalid INI format)
|
|
log_handler.log_error(
|
|
f"Error parsing configuration file '{CONFIG_FILE}': {read_err}. Recreating file with defaults.",
|
|
func_name=func_name,
|
|
)
|
|
self.config = configparser.ConfigParser()
|
|
config_exists = False # Treat as if file didn't exist
|
|
|
|
# If file didn't exist or was invalid, create default structure
|
|
if not config_exists:
|
|
log_handler.log_info(
|
|
f"Configuration file '{CONFIG_FILE}' not found or invalid. Creating a new one with the default profile.",
|
|
func_name=func_name,
|
|
)
|
|
# Ensure config object is clean (already done above if parsing failed)
|
|
if self.config.sections(): # Should be empty, but check just in case
|
|
self.config = configparser.ConfigParser()
|
|
# Create the default profile with all expected keys
|
|
self._create_default_profile() # Helper function to add section and keys
|
|
config_needs_saving = True # Mark that the new config needs to be saved
|
|
else:
|
|
# Config file was read successfully, proceed with validation
|
|
log_handler.log_debug(
|
|
f"Successfully read configuration from '{CONFIG_FILE}'. Validating content...",
|
|
func_name=func_name,
|
|
)
|
|
|
|
# --- Content Validation ---
|
|
expected_keys_defaults: Dict[str, str] = (
|
|
self._get_expected_keys_with_defaults()
|
|
)
|
|
all_current_profiles: List[str] = list(
|
|
self.config.sections()
|
|
) # Get copy of section names
|
|
|
|
# 1. Ensure the default profile exists
|
|
if DEFAULT_PROFILE not in all_current_profiles:
|
|
log_handler.log_warning(
|
|
f"Default profile '{DEFAULT_PROFILE}' is missing in the loaded config. Adding it with default values.",
|
|
func_name=func_name,
|
|
)
|
|
self._create_default_profile() # Create the default profile
|
|
# Refresh the list of profiles if default was just added
|
|
all_current_profiles = list(self.config.sections())
|
|
config_needs_saving = True # Mark for saving
|
|
|
|
# 2. Validate keys for ALL existing profiles
|
|
log_handler.log_debug(
|
|
f"Validating keys for profiles: {all_current_profiles}",
|
|
func_name=func_name,
|
|
)
|
|
for profile_name in all_current_profiles:
|
|
for key, default_value in expected_keys_defaults.items():
|
|
# Check if the profile section is missing the expected key
|
|
if not self.config.has_option(profile_name, key):
|
|
# Use the default value defined in _get_expected_keys_with_defaults
|
|
log_handler.log_warning(
|
|
f"Key '{key}' missing in profile '{profile_name}'. Adding it with default value: '{default_value}'.",
|
|
func_name=func_name,
|
|
)
|
|
# Add the missing key with its default value
|
|
self.config.set(profile_name, key, default_value)
|
|
config_needs_saving = True # Mark for saving
|
|
|
|
# Save configuration if any changes were made during load/validation
|
|
if config_needs_saving:
|
|
log_handler.log_info(
|
|
f"Configuration was updated during load/validation. Saving changes to '{CONFIG_FILE}'.",
|
|
func_name=func_name,
|
|
)
|
|
self.save_config() # Call save method
|
|
|
|
except Exception as e:
|
|
# Catch any unexpected errors during the load/validation process
|
|
log_handler.log_exception(
|
|
f"Unexpected error loading or validating configuration: {e}",
|
|
func_name=func_name,
|
|
)
|
|
# Optionally reset config to a safe state or re-raise
|
|
self.config = configparser.ConfigParser()
|
|
# raise RuntimeError("Critical configuration error during load") from e
|
|
|
|
def _create_default_profile(self) -> None:
|
|
"""
|
|
Internal helper method to create the default profile section
|
|
and populate it with all expected keys and their default values.
|
|
"""
|
|
func_name = "_create_default_profile"
|
|
# Ensure the section exists
|
|
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 the standard keys and defaults
|
|
expected_keys_defaults: Dict[str, str] = self._get_expected_keys_with_defaults()
|
|
log_handler.log_debug(
|
|
f"Setting default keys/values for profile '{DEFAULT_PROFILE}'.",
|
|
func_name=func_name,
|
|
)
|
|
|
|
# Set each expected key with its default value in the default profile
|
|
for key, default_value in expected_keys_defaults.items():
|
|
# configparser's set() method handles adding/updating the option
|
|
self.config.set(DEFAULT_PROFILE, key, default_value)
|
|
|
|
log_handler.log_info(
|
|
f"Ensured default profile '{DEFAULT_PROFILE}' exists with all expected keys.",
|
|
func_name=func_name,
|
|
)
|
|
|
|
def save_config(self) -> None:
|
|
"""
|
|
Saves the current configuration state (self.config) to the CONFIG_FILE.
|
|
Uses UTF-8 encoding. Uses log_handler.
|
|
"""
|
|
func_name: str = "save_config"
|
|
log_handler.log_debug(
|
|
f"Attempting to save configuration to '{CONFIG_FILE}'...",
|
|
func_name=func_name,
|
|
)
|
|
|
|
# Optional: Check/Create directory if config file is specified with a path
|
|
config_dir: str = os.path.dirname(CONFIG_FILE)
|
|
# Ensure directory exists only if CONFIG_FILE includes a directory part
|
|
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 save attempt (might succeed in current dir)
|
|
log_handler.log_error(
|
|
f"Could not create directory '{config_dir}': {e}. Save attempt might fail.",
|
|
func_name=func_name,
|
|
)
|
|
|
|
try:
|
|
# Write the config object to the file using 'w' mode and 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 I/O 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 IOError(f"Failed to save config: {e}") from e
|
|
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
|
|
)
|
|
# raise RuntimeError(f"Unexpected error saving config: {e}") from e
|
|
|
|
def get_profile_sections(self) -> List[str]:
|
|
"""
|
|
Returns a sorted list of all profile section names present in the configuration.
|
|
"""
|
|
# Sort alphabetically for consistent display order in UI elements
|
|
return sorted(self.config.sections())
|
|
|
|
def get_profile_option(
|
|
self, profile: str, option: str, fallback: Optional[str] = None
|
|
) -> Optional[str]:
|
|
"""
|
|
Retrieves a specific option's value from a profile section as a string.
|
|
Handles missing sections or options gracefully using the fallback. Uses log_handler.
|
|
|
|
Args:
|
|
profile (str): Name of the profile section (e.g., 'default').
|
|
option (str): Name of the option key (e.g., 'remote_url').
|
|
fallback (Optional[str]): Value to return if the option or section is not found.
|
|
|
|
Returns:
|
|
Optional[str]: The option value as a string if found, otherwise the fallback value.
|
|
Returns None if fallback is None and option is missing.
|
|
"""
|
|
func_name: str = "get_profile_option"
|
|
try:
|
|
# Check if the profile section exists first for better logging
|
|
if not self.config.has_section(profile):
|
|
log_handler.log_warning(
|
|
f"Profile '{profile}' not found. Cannot get option '{option}'. Returning fallback: {repr(fallback)}",
|
|
func_name=func_name,
|
|
)
|
|
return fallback # Return the original fallback value
|
|
|
|
# Use configparser's get method with its built-in fallback mechanism
|
|
# Configparser always returns strings or the fallback if the fallback itself is provided.
|
|
value_str: Optional[str] = self.config.get(
|
|
profile, option, fallback=fallback
|
|
)
|
|
|
|
# Log if fallback was potentially used (more informative debug log)
|
|
# Note: This check isn't perfect if fallback could be the actual value,
|
|
# but it helps in debugging missing keys.
|
|
if value_str == fallback and not self.config.has_option(profile, option):
|
|
log_handler.log_debug(
|
|
f"Option '{option}' not found in profile '{profile}'. Used fallback value: {repr(fallback)}",
|
|
func_name=func_name,
|
|
)
|
|
|
|
# Return the value obtained (which will be a string or the original fallback)
|
|
return value_str
|
|
|
|
except Exception as e:
|
|
# Log unexpected errors during option retrieval
|
|
log_handler.log_exception(
|
|
f"Unexpected error getting option '{option}' for profile '{profile}': {e}",
|
|
func_name=func_name,
|
|
)
|
|
# Return the fallback value in case of any error
|
|
return fallback
|
|
|
|
def set_profile_option(self, profile: str, option: str, value: Any) -> None:
|
|
"""
|
|
Sets an option in a specific profile section. Creates the section if needed.
|
|
The provided value is automatically converted to a string for storage in the INI file.
|
|
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 its string representation).
|
|
None will be stored as an empty string.
|
|
"""
|
|
func_name: str = "set_profile_option"
|
|
try:
|
|
# Ensure the profile section exists, creating it if necessary
|
|
if not self.config.has_section(profile):
|
|
self.config.add_section(profile)
|
|
log_handler.log_info(
|
|
f"Created new profile section '{profile}' while setting option '{option}'.",
|
|
func_name=func_name,
|
|
)
|
|
|
|
# Convert the value to string for storage (handle None case)
|
|
value_str: str = str(value) if value is not None else ""
|
|
|
|
# Set the option using configparser's set method
|
|
self.config.set(profile, option, value_str)
|
|
log_handler.log_debug(
|
|
f"Set option in profile '{profile}': {option} = '{value_str}'",
|
|
func_name=func_name,
|
|
)
|
|
except configparser.Error as e:
|
|
# Log specific configparser errors (e.g., invalid section/option names)
|
|
log_handler.log_error(
|
|
f"ConfigParser error setting option '{option}' in profile '{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 '{profile}': {e}",
|
|
func_name=func_name,
|
|
)
|
|
|
|
def remove_profile_section(self, profile: str) -> bool:
|
|
"""
|
|
Removes a profile section entirely from the configuration.
|
|
Prevents removal of the default profile. Uses log_handler.
|
|
|
|
Args:
|
|
profile (str): Name of the profile section to remove.
|
|
|
|
Returns:
|
|
bool: True if the section was successfully removed, False otherwise
|
|
(e.g., profile not found or is the default profile).
|
|
"""
|
|
func_name: str = "remove_profile_section"
|
|
|
|
# Prevent removal of the required default profile
|
|
if profile == DEFAULT_PROFILE:
|
|
log_handler.log_warning(
|
|
f"Attempt to remove the default profile ('{DEFAULT_PROFILE}') was denied.",
|
|
func_name=func_name,
|
|
)
|
|
return False # Indicate failure
|
|
|
|
# Check if the profile section actually exists
|
|
if self.config.has_section(profile):
|
|
try:
|
|
# Attempt to remove the section using configparser
|
|
removed: bool = self.config.remove_section(profile)
|
|
if removed:
|
|
log_handler.log_info(
|
|
f"Successfully removed profile section: '{profile}'",
|
|
func_name=func_name,
|
|
)
|
|
return True # Indicate success
|
|
else:
|
|
# This case should be rare if has_section was True, but log it
|
|
log_handler.log_warning(
|
|
f"ConfigParser reported failure trying to remove existing section '{profile}'.",
|
|
func_name=func_name,
|
|
)
|
|
return False # Indicate failure
|
|
except Exception as e:
|
|
# Log any errors that occur during the removal process
|
|
log_handler.log_exception(
|
|
f"Error removing profile section '{profile}': {e}",
|
|
func_name=func_name,
|
|
)
|
|
return False # Indicate failure
|
|
else:
|
|
# Profile section was not found
|
|
log_handler.log_warning(
|
|
f"Profile section '{profile}' not found. Cannot remove.",
|
|
func_name=func_name,
|
|
)
|
|
return False # Indicate failure
|
|
|
|
def add_section(self, section_name: str) -> bool:
|
|
"""
|
|
Adds a new section (profile) to the configuration if it doesn't already exist.
|
|
|
|
Args:
|
|
section_name (str): The name of the section (profile) to add.
|
|
|
|
Returns:
|
|
bool: True if the section was added, False if it already existed.
|
|
"""
|
|
# Helper function, mainly used internally by add_profile logic in controller
|
|
if not self.config.has_section(section_name):
|
|
self.config.add_section(section_name)
|
|
log_handler.log_info(
|
|
f"Added new configuration section: '{section_name}'",
|
|
func_name="add_section",
|
|
)
|
|
return True # Section was added
|
|
else:
|
|
log_handler.log_debug(
|
|
f"Section '{section_name}' already exists. No action taken.",
|
|
func_name="add_section",
|
|
)
|
|
return False # Section already existed
|
|
|
|
|
|
# --- END OF FILE gitsync_tool/config/config_manager.py ---
|