283 lines
12 KiB
Python
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
|
|
|
|
|