from PySide6.QtCore import QObject, QStandardPaths, Signal from pathlib import Path import json import os import logging import threading import datetime from copy import deepcopy import pickle from src.datatypes import ConfType from src.utils import RestrictedUnpickler class ConfManager(QObject): """ Gestionnaire de configuration avec persistance automatique via pickle sécurisé. Maintient un haut niveau de journalisation pour le suivi et le débogage. """ conf_changed = Signal(ConfType) download_location_changed = Signal(str) files_changed = Signal(list) workers_changed = Signal(int) token_changed = Signal(dict) def __init__(self, parent=None): super().__init__(parent) # Configuration du logger avec niveau détaillé self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.DEBUG) # Niveau DEBUG pour capturer tous les messages self.logger.info("Initialisation de ConfManager") # Verrou pour les opérations de fichier (thread-safety) self._lock = threading.RLock() self.logger.debug("Verrou RLock initialisé") # Préparation du répertoire de configuration self.app_config_path = Path(QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppConfigLocation)) self.logger.debug(f"Répertoire de configuration: {self.app_config_path}") # cache web self.web_cache_path = self.app_config_path / "web_cache" if not self.app_config_path.exists(): try: self.app_config_path.mkdir(parents=True, exist_ok=True) self.logger.info(f"Répertoire de configuration créé: {self.app_config_path}") except Exception as e: self.logger.error(f"Erreur lors de la création du répertoire de configuration: {e}", exc_info=True) # Configuration du fichier de sauvegarde self.conf_file = self.app_config_path / "config.pickle" self.logger.info(f"Fichier de configuration défini: {self.conf_file}") # Initialisation avec les valeurs par défaut self.conf = ConfType() self.logger.debug("Objet ConfType initialisé avec valeurs par défaut") # Chargement de la configuration if self.load_conf(): self.logger.info("Configuration chargée avec succès") else: self.logger.warning("Échec du chargement de la configuration, utilisation des valeurs par défaut") def load_conf(self) -> bool: """ Charge la configuration depuis le fichier pickle en utilisant un désérialiseur sécurisé. Returns: bool: True si la configuration a été chargée avec succès, False sinon. """ with self._lock: self.logger.debug("Début du chargement de la configuration") try: if self.conf_file.exists(): self.logger.debug(f"Le fichier de configuration existe: {self.conf_file}") with open(self.conf_file, "rb") as f: self.logger.debug("Fichier de configuration ouvert pour lecture") # Utilisation du RestrictedUnpickler pour la sécurité unpickler = RestrictedUnpickler(f) self.logger.debug("Désérialisation avec RestrictedUnpickler") # Désérialisation avec gestion d'erreurs spécifiques try: loaded_conf = unpickler.load() self.logger.debug("Données désérialisées avec succès") # Vérification du type pour s'assurer de la conformité if isinstance(loaded_conf, ConfType): self.conf = loaded_conf self.logger.info("Configuration chargée et vérifiée avec succès") return True else: self.logger.error(f"Le type chargé n'est pas valide: {type(loaded_conf)}") return False except pickle.UnpicklingError as ue: self.logger.error(f"Erreur de désérialisation restrictive: {ue}", exc_info=True) self._backup_corrupted_config() return False else: self.logger.warning(f"Fichier de configuration non trouvé, création par défaut: {self.conf_file}") self.save_conf() return True except Exception as e: self.logger.error(f"Erreur inattendue lors du chargement de la configuration: {e}", exc_info=True) return False def save_conf(self) -> bool: """ Sauvegarde la configuration actuelle dans le fichier pickle. Returns: bool: True si la sauvegarde a réussi, False sinon. """ with self._lock: self.logger.debug("Début de la sauvegarde de la configuration") try: # Création d'une copie pour éviter les modifications pendant la sérialisation conf_to_save = deepcopy(self.conf) self.logger.debug("Copie profonde de la configuration créée pour la sauvegarde") # Sérialisation et écriture du fichier with open(self.conf_file, 'wb') as f: self.logger.debug("Fichier de configuration ouvert pour écriture") pickle.dump(conf_to_save, f, protocol=pickle.HIGHEST_PROTOCOL) self.logger.debug(f"Sérialisation effectuée avec le protocole {pickle.HIGHEST_PROTOCOL}") self.logger.info("Configuration sauvegardée avec succès") return True except Exception as e: self.logger.error(f"Erreur lors de la sauvegarde de la configuration: {e}", exc_info=True) return False def _backup_corrupted_config(self): """ Crée une sauvegarde horodatée du fichier de configuration corrompu. """ if self.conf_file.exists(): try: # Création d'un nom de fichier avec date et heure timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") backup_path = self.conf_file.with_name(f"{self.conf_file.stem}_{timestamp}_corrupted.bak") self.logger.debug(f"Préparation de la sauvegarde du fichier corrompu vers: {backup_path}") # Copie du fichier with open(self.conf_file, 'rb') as src_file: corrupted_data = src_file.read() with open(backup_path, 'wb') as backup_file: backup_file.write(corrupted_data) self.logger.warning(f"Configuration corrompue sauvegardée dans {backup_path}") # Journaliser les détails du fichier corrompu self.logger.debug(f"Taille du fichier corrompu: {os.path.getsize(backup_path)} octets") except Exception as e: self.logger.error(f"Échec de la sauvegarde du fichier corrompu: {e}", exc_info=True) def get_value(self, key, default=None): """ Récupère une valeur de la configuration par son nom de clé. Args : key : Nom de la propriété à récupérer Returns : La valeur associée à la clé ou None si la clé n'existe pas """ with self._lock: self.logger.debug(f"Récupération de la valeur pour la clé: {key}") if hasattr(self.conf, key): value = getattr(self.conf, key) self.logger.debug(f"Valeur récupérée pour {key}: {value}") return value else: self.logger.warning(f"Tentative d'accès à une clé inexistante: {key}") return default def set_value(self, key, value): """ Définit une valeur dans la configuration et émet le signal approprié. Args: key: Nom de la propriété à définir value: Valeur à affecter Returns: bool: True si la valeur a été définie avec succès, False sinon """ with self._lock: self.logger.debug(f"Tentative de définition de la valeur pour la clé: {key}") if hasattr(self.conf, key): try: # Stockage de l'ancienne valeur pour la journalisation old_value = getattr(self.conf, key) # Définition de la nouvelle valeur setattr(self.conf, key, value) self.logger.info(f"Valeur de {key} modifiée: {old_value} -> {value}") # Émission des signaux appropriés if key == "download_location": self.download_location_changed.emit(value) self.logger.debug(f"Signal download_location_changed émis avec {value}") elif key == "files": self.files_changed.emit(list(value.values())) self.logger.debug(f"Signal files_changed émis avec {len(value)} fichiers") elif key == "workers": self.workers_changed.emit(value) self.logger.debug(f"Signal workers_changed émis avec {value}") elif key == "token": self.token_changed.emit(value) self.logger.debug("Signal token_changed émis") # Signal global indiquant que la configuration a changé self.conf_changed.emit(self.conf) self.logger.debug("Signal conf_changed émis") # Sauvegarde automatique de la configuration self.save_conf() return True except Exception as e: self.logger.error(f"Erreur lors de la définition de {key}: {e}", exc_info=True) return False else: self.logger.warning(f"Tentative de définition d'une clé inexistante: {key}") return False def reset_to_defaults(self): """ Réinitialise la configuration aux valeurs par défaut. Returns: bool: True si la réinitialisation a réussi, False sinon """ with self._lock: try: self.logger.info("Début de la réinitialisation de la configuration aux valeurs par défaut") old_conf = deepcopy(self.conf) # Création d'une nouvelle instance avec les valeurs par défaut self.conf = ConfType() self.logger.debug("Configuration réinitialisée aux valeurs par défaut") # Émission de tous les signaux self.download_location_changed.emit(self.conf.download_location) self.files_changed.emit(list(self.conf.files.values())) self.workers_changed.emit(self.conf.workers) self.token_changed.emit(self.conf.token) self.conf_changed.emit(self.conf) self.logger.debug("Tous les signaux émis après réinitialisation") # Sauvegarde des nouvelles valeurs if self.save_conf(): self.logger.info("Réinitialisation terminée avec succès") return True else: # En cas d'échec, restauration de l'ancienne configuration self.conf = old_conf self.logger.error( "Échec de la sauvegarde après réinitialisation, restauration de l'ancienne configuration") return False except Exception as e: self.logger.error(f"Erreur lors de la réinitialisation: {e}", exc_info=True) return False