209 lines
10 KiB
Python
209 lines
10 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 keys exist.
|
|
"""
|
|
|
|
def __init__(self, logger):
|
|
"""
|
|
Initializes the ConfigManager with a logger instance.
|
|
Args:
|
|
logger (logging.Logger): Logger instance for logging messages.
|
|
"""
|
|
self.logger = logger
|
|
self.config = configparser.ConfigParser()
|
|
self.load_config()
|
|
|
|
def load_config(self):
|
|
"""
|
|
Loads the configuration from the CONFIG_FILE.
|
|
If the file doesn't exist or is missing sections/keys, it creates/adds them with default values.
|
|
"""
|
|
try:
|
|
config_exists = os.path.exists(CONFIG_FILE)
|
|
files_read = self.config.read(CONFIG_FILE, encoding='utf-8') # Specify encoding
|
|
|
|
if not config_exists or not files_read:
|
|
if config_exists and not files_read: # File exists but was empty or unreadable
|
|
self.logger.warning(f"Config file '{CONFIG_FILE}' exists but could not be read or is empty. Recreating with defaults.")
|
|
else: # File doesn't exist
|
|
self.logger.info(f"Config file '{CONFIG_FILE}' not found. Creating with defaults.")
|
|
self.config = configparser.ConfigParser() # Ensure clean parser state
|
|
self._create_default_profile()
|
|
self.save_config()
|
|
else:
|
|
self.logger.debug(f"Successfully read configuration from '{CONFIG_FILE}'.")
|
|
# File exists and was read, ensure default profile and all keys are present
|
|
profile_needs_save = False
|
|
if DEFAULT_PROFILE not in self.config.sections():
|
|
self.logger.warning(f"Default profile '{DEFAULT_PROFILE}' missing. Adding it.")
|
|
self._create_default_profile() # Creates section and adds all keys
|
|
profile_needs_save = True
|
|
else:
|
|
# Check if default profile is missing new keys (for backward compatibility)
|
|
default_section = self.config[DEFAULT_PROFILE]
|
|
# Define expected keys and their defaults here for check
|
|
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): # More robust check
|
|
self.logger.warning(f"Key '{key}' missing in profile '{DEFAULT_PROFILE}'. Adding default value: '{default_value}'.")
|
|
self.config.set(DEFAULT_PROFILE, key, default_value)
|
|
profile_needs_save = True
|
|
|
|
if profile_needs_save:
|
|
self.logger.info(f"Updating configuration file '{CONFIG_FILE}' with missing defaults.")
|
|
self.save_config()
|
|
|
|
except configparser.Error as e:
|
|
self.logger.error(f"Error parsing configuration file '{CONFIG_FILE}': {e}", exc_info=True)
|
|
self.logger.info("Attempting to create a default configuration due to parsing error.")
|
|
self.config = configparser.ConfigParser()
|
|
self._create_default_profile()
|
|
self.save_config() # Save the new default config
|
|
except Exception as e:
|
|
self.logger.error(f"Unexpected error loading configuration: {e}", exc_info=True)
|
|
# Consider re-raising or exiting depending on severity
|
|
|
|
def _get_expected_keys_with_defaults(self):
|
|
"""Returns a dictionary of expected config keys and their default values."""
|
|
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,
|
|
"autocommit": "False",
|
|
"commit_message": "", # Added
|
|
"backup_exclude_extensions": ".log,.tmp" # Added with example default
|
|
}
|
|
|
|
def _create_default_profile(self):
|
|
"""Creates the default profile section with default values for all keys."""
|
|
if DEFAULT_PROFILE not in self.config.sections():
|
|
self.config.add_section(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)
|
|
|
|
self.logger.info(f"Ensured default profile '{DEFAULT_PROFILE}' exists with default values.")
|
|
|
|
|
|
def save_config(self):
|
|
"""
|
|
Saves the current configuration to the CONFIG_FILE.
|
|
"""
|
|
try:
|
|
with open(CONFIG_FILE, "w", encoding='utf-8') as configfile: # Specify encoding
|
|
self.config.write(configfile)
|
|
self.logger.debug(f"Configuration saved to '{CONFIG_FILE}'.")
|
|
except IOError as e:
|
|
self.logger.error(f"Error writing configuration file '{CONFIG_FILE}': {e}", exc_info=True)
|
|
except Exception as e:
|
|
self.logger.error(f"Unexpected error saving configuration: {e}", exc_info=True)
|
|
|
|
def get_profile_sections(self):
|
|
"""
|
|
Returns a list of all profile sections in the configuration file.
|
|
Returns:
|
|
list: List of profile section names.
|
|
"""
|
|
return self.config.sections()
|
|
|
|
def get_profile_option(self, profile, option, fallback=None):
|
|
"""
|
|
Retrieves a specific option from a profile section.
|
|
|
|
Args:
|
|
profile (str): Name of the profile section.
|
|
option (str): Name of the option to retrieve.
|
|
fallback (any, optional): Default value to return if the option or profile
|
|
is not found. Defaults to None.
|
|
|
|
Returns:
|
|
str: Value of the option or the fallback value if not found.
|
|
Returns fallback also if the profile itself doesn't exist.
|
|
"""
|
|
# Use configparser's built-in fallback mechanism
|
|
# The 'fallback' parameter in config.get() handles NoSectionError and NoOptionError
|
|
try:
|
|
# Ensure fallback is a string if not None, as configparser expects strings
|
|
str_fallback = str(fallback) if fallback is not None else None
|
|
|
|
# Check if section exists first for better logging
|
|
if not self.config.has_section(profile):
|
|
self.logger.warning(f"Profile '{profile}' not found. Cannot get option '{option}'. Using fallback.")
|
|
return fallback # Return original fallback type
|
|
|
|
value = self.config.get(profile, option, fallback=str_fallback)
|
|
|
|
# Log if fallback was used specifically because the option was missing
|
|
if value == str_fallback and not self.config.has_option(profile, option):
|
|
self.logger.warning(f"Option '{option}' not found in profile '{profile}'. Using fallback.")
|
|
|
|
return value # Return the retrieved value (or stringified fallback)
|
|
except Exception as e:
|
|
# Catch unexpected errors during retrieval
|
|
self.logger.error(f"Error getting option '{option}' for profile '{profile}': {e}", exc_info=True)
|
|
return fallback # Return the original fallback type
|
|
|
|
def set_profile_option(self, profile, option, value):
|
|
"""
|
|
Sets a specific option in a profile section. Creates the section if it doesn't exist.
|
|
Args:
|
|
profile (str): Name of the profile section.
|
|
option (str): Name of the option to set.
|
|
value (any): Value to set for the option (will be converted to string).
|
|
"""
|
|
try:
|
|
if not self.config.has_section(profile):
|
|
self.config.add_section(profile)
|
|
self.logger.info(f"Created new profile section: '{profile}'")
|
|
# Ensure keys are stored in lowercase if needed for case-insensitivity later
|
|
# configparser handles sections case-insensitively by default, keys case-sensitively
|
|
self.config.set(profile, option, str(value)) # Ensure value is string
|
|
self.logger.debug(f"Set option '{option}' = '{value}' in profile '{profile}'.")
|
|
except Exception as e:
|
|
self.logger.error(f"Error setting option '{option}' in profile '{profile}': {e}", exc_info=True)
|
|
|
|
def remove_profile_section(self, profile):
|
|
"""
|
|
Removes a profile section from the configuration.
|
|
Args:
|
|
profile (str): Name of the profile section to remove.
|
|
Returns:
|
|
bool: True if the section was removed successfully, False otherwise.
|
|
"""
|
|
if profile == DEFAULT_PROFILE:
|
|
self.logger.warning(f"Attempted to remove the default profile '{DEFAULT_PROFILE}', which is not allowed.")
|
|
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, but good to log
|
|
self.logger.warning(f"ConfigParser reported failure removing section '{profile}' even though it existed.")
|
|
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 |