Files
oxapp25/src/conf.py
2025-04-17 13:56:26 +02:00

283 lines
12 KiB
Python

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