#!/usr/bin/env python3
"""
Version: 1.1.0 (2024-01-12)
Auteur: kahina et franck - Groupe 2
Contact: bigmoletos@yopmail.com
Script de sauvegarde automatique avec compression ZIP.
Ce module:
- Crée une archive ZIP datée du projet
- Génère un résumé hiérarchique des modifications
- Vérifie l'intégrité de la sauvegarde
Architecture:
------------
1. Sauvegarde:
- Compression ZIP du projet avec niveau de compression optimal
- Organisation par date/heure (format: JJ_MM_AAAA_HHhMM)
- Vérification d'intégrité avec test ZIP
2. Logs:
- Résumé hiérarchique des modifications avec emojis
- Classification par dossier avec indentation
- Horodatage des changements au format ISO
3. Structure des fichiers:
- backup_dir/
├── projet_qualite_air.zip (archive complète)
└── resume_des_modifs.log (modifications détaillées)
4. Gestion des erreurs:
- Validation des chemins source/destination
- Vérification des permissions
- Contrôle d'intégrité ZIP
- Logging détaillé des erreurs
Dépendances:
-----------
- Python 3.8+
- zipfile: Compression et vérification
- pathlib: Gestion des chemins
- logging: Journalisation des événements
"""
import os
import sys
import time
import shutil
import zipfile
import logging
from pathlib import Path
from datetime import datetime, timedelta
# Configuration du logging avec rotation des fichiers
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
filename='sauvegarde.log',
encoding='utf-8'
)
logger = logging.getLogger(__name__)
# Ajouter un handler pour la console
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
[docs]
def create_backup_folder():
"""
Crée le dossier de sauvegarde avec horodatage.
Cette fonction:
1. Génère un nom de dossier unique avec date et heure
2. Crée l'arborescence si nécessaire
3. Vérifie les permissions d'écriture
Format du dossier:
-----------------
JJ_MM_AAAA_HHhMM (exemple: 12_01_2024_15h30)
Returns:
Path: Chemin du dossier de sauvegarde créé
Raises:
OSError: Si la création du dossier échoue
PermissionError: Si les droits sont insuffisants
"""
try:
# Chemin racine des sauvegardes
backup_root = Path(r"J:\sauve_AJC_formation_oct2024")
# Génération du nom avec horodatage
current_time = datetime.now()
folder_name = current_time.strftime("%d_%m_%Y_%Hh%M")
backup_dir = backup_root / folder_name
# Création du dossier avec vérification
backup_dir.mkdir(parents=True, exist_ok=True)
# Test d'écriture
test_file = backup_dir / ".write_test"
test_file.touch()
test_file.unlink()
logger.info(f"Dossier de sauvegarde créé: {backup_dir}")
return backup_dir
except PermissionError as e:
logger.error(f"Permissions insuffisantes pour créer {backup_dir}: {e}")
raise
except OSError as e:
logger.error(f"Erreur lors de la création du dossier {backup_dir}: {e}")
raise
[docs]
def log_file_modification(file_path, action="modifié"):
"""
Enregistre une modification de fichier dans le log.
Args:
file_path (Path): Chemin du fichier modifié
action (str): Type de modification (modifié, créé, supprimé)
"""
try:
rel_path = file_path.relative_to(Path("C:/AJC_projets/projet_qualite_air"))
logger.info(f"📄 Fichier {action}: {rel_path}")
except ValueError:
logger.warning(f"Fichier hors projet: {file_path}")
[docs]
def generate_changes_summary(project_path, backup_dir):
"""
Génère un résumé des fichiers modifiés dans les 3 dernières heures.
"""
try:
summary_file = backup_dir / "resume_des_modifs.log"
three_hours_ago = datetime.now() - timedelta(hours=3)
logger.info("🔍 Analyse des modifications récentes...")
modifications_found = False
with summary_file.open('w', encoding='utf-8') as f:
f.write("=== RÉSUMÉ DES MODIFICATIONS ===\n")
f.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("================================\n\n")
for root, dirs, files in os.walk(project_path):
root_path = Path(root)
rel_path = root_path.relative_to(project_path)
# Filtrer les fichiers modifiés
modified_files = []
for file in files:
file_path = root_path / file
try:
mtime = datetime.fromtimestamp(file_path.stat().st_mtime)
if mtime > three_hours_ago:
modified_files.append((file, mtime))
log_file_modification(file_path)
modifications_found = True
except (OSError, PermissionError):
continue
# Écrire les modifications dans le résumé
if modified_files:
indent = " " * len(rel_path.parts)
f.write(f"\n{indent}📁 {rel_path}\n")
modified_files.sort(key=lambda x: x[1], reverse=True)
for file, mtime in modified_files:
f.write(f"{indent} 📄 {file}\n")
f.write(f"{indent} └─ Modifié le {mtime.strftime('%Y-%m-%d %H:%M:%S')}\n")
if not modifications_found:
msg = "Aucune modification détectée dans les 3 dernières heures"
f.write(f"\n{msg}\n")
logger.info(f"ℹ️ {msg}")
return summary_file
except Exception as e:
logger.error(f"❌ Erreur lors de la génération du résumé: {e}")
raise
[docs]
def create_zip_backup(project_path, backup_dir):
"""
Crée une archive ZIP optimisée du projet.
"""
try:
zip_path = backup_dir / "projet_qualite_air.zip"
logger.info("Début de la compression ZIP...")
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=1) as zipf:
# Utiliser un niveau de compression plus rapide (1 au lieu de 6)
for root, dirs, files in os.walk(project_path):
root_path = Path(root)
# Exclure les dossiers de cache et environnements virtuels
dirs[:] = [d for d in dirs if not d.startswith(('.', '__pycache__', 'venv'))]
for file in files:
if file.startswith('.') or file.endswith(('.pyc', '.pyo')):
continue
file_path = root_path / file
try:
arcname = file_path.relative_to(project_path)
zipf.write(file_path, arcname)
except (OSError, PermissionError):
continue
logger.info(f"Sauvegarde ZIP créée avec succès: {zip_path}")
return zip_path
except Exception as e:
logger.error(f"Erreur lors de la création du ZIP: {e}")
raise
[docs]
def verify_zip_integrity(zip_path):
"""
Vérifie l'intégrité de l'archive ZIP.
Cette fonction:
1. Teste la structure de l'archive
2. Vérifie chaque fichier compressé
3. Valide le CRC des données
4. Contrôle la présence du manifeste
Args:
zip_path (Path): Chemin de l'archive à vérifier
Returns:
bool: True si l'archive est valide et complète
Notes:
- Utilise zipfile.testzip() pour la vérification
- Vérifie aussi la présence du manifeste
- Contrôle la taille minimale attendue
"""
try:
# Vérifier que le fichier existe et n'est pas vide
if not zip_path.exists() or zip_path.stat().st_size == 0:
logger.error(f"Archive ZIP invalide ou vide: {zip_path}")
return False
with zipfile.ZipFile(zip_path, 'r') as zipf:
# Tester l'intégrité globale
result = zipf.testzip()
if result is not None:
logger.error(f"Fichier corrompu dans l'archive: {result}")
return False
# Vérifier la présence du manifeste
if "manifest.txt" not in zipf.namelist():
logger.error("Manifeste manquant dans l'archive")
return False
# Vérifier que l'archive contient des fichiers
if len(zipf.namelist()) < 2: # manifeste + au moins 1 fichier
logger.error("Archive ne contenant pas assez de fichiers")
return False
logger.info("Vérification de l'archive ZIP réussie")
return True
except zipfile.BadZipFile as e:
logger.error(f"Archive ZIP corrompue: {e}")
return False
except Exception as e:
logger.error(f"Erreur lors de la vérification du ZIP: {e}")
return False
[docs]
def main():
"""
Fonction principale de sauvegarde.
Crée une sauvegarde datée avec un résumé des modifications et une archive ZIP.
"""
try:
logger.info("Démarrage de la sauvegarde...")
# Créer le dossier de sauvegarde daté
backup_dir = create_backup_folder()
logger.info(f"Dossier de sauvegarde créé: {backup_dir}")
# Chemin du projet à sauvegarder
project_path = Path("C:/AJC_projets/projet_qualite_air")
if not project_path.exists():
raise FileNotFoundError(f"Dossier projet non trouvé: {project_path}")
# Générer le résumé des modifications récentes
logger.info("Génération du résumé des modifications des 3 dernières heures...")
generate_changes_summary(project_path, backup_dir)
# Créer l'archive ZIP
logger.info("Création de l'archive ZIP...")
zip_path = create_zip_backup(project_path, backup_dir)
# Vérifier l'intégrité de la sauvegarde
logger.info("Vérification de l'intégrité...")
if verify_zip_integrity(zip_path):
logger.info("✅ Sauvegarde terminée avec succès")
else:
logger.error("❌ Erreur d'intégrité de la sauvegarde")
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde: {e}")
raise
if __name__ == "__main__":
main()