Je vous ai déjà parlé plusieurs fois de mon nouvel environnent professionnel dans mes récents articles qui baigne dans du Microsoft à fond. Je me suis rendu copte que c’est le cas comme l’ensemble ou presque des entreprises en France lorsque l’on sort de notre bulle du pur développement. Les gens normaux utilisent Windows en entreprise et tout l’écosystème qui tourne autour avec Microsoft 365. C’est comme ça … C’est du moins le cas actuellement. On verra ce que l’avenir nous réserve, car le paysage politique et technique change rapidement. Passons, on n’est pas là pour parler de ce genre de choses ici.
C’est sûr que pour moi cela change de mon environnement Linux que j’ai toujours connu jusqu’ici dans un contexte professionnel. Pas le choix, il faut faire avec et apporter des solutions aux décideurs. On va donc s’atteler à créer un script tout simple qui s’occupe de sauvegarder l’ensemble des sites SharePoint de notre entreprise privée ou organisation publique. C’est selon, mais finalement le besoin est le même.
Si vous êtes arrivés ici, je ne vais pas vous faire l’affront de vous présenter la solution SharePoint de Microsoft et on va pouvoir passer directement à la suite. Attention, ici, dans cet article, on parle bien de la version cloud avec Microsoft 365. Avant d’aller plus loin, je vous conseille d’aller voir cet article pour créer vos clés API pour utiliser Microsoft Graph.
Pourquoi sauvegarder ses sites SharePoint Online de Microsoft 365 en local ?
On pourrait penser, à juste titre, que Microsoft s’occupe de tout. Après tout, nous payons pour un service complet. En réalité, le modèle de Microsoft est basé sur la responsabilité partagée. Ils s’assurent de la disponibilité de l’infrastructure et de la sécurité au niveau de leurs applications, mais la protection des données reste de votre responsabilité en tant que DSI.
Concrètement, Microsoft 365 propose des mécanismes de rétention et des corbeilles. Par exemple, un site SharePoint supprimé reste dans une corbeille pendant 93 jours, et un administrateur peut le restaurer. De plus, Microsoft effectue des sauvegardes de ses collections de sites toutes les 12 heures, mais celles-ci ne sont conservées que pendant 14 jours. Surtout, pour une restauration complète, il faut passer par un ticket de support, ce qui est loin d’être une solution agile en cas d’extrême urgence. Ces mécanismes sont utiles, mais ils s’apparentent plus à une politique de rétention qu’à une véritable stratégie de sauvegarde robuste. La distinction est essentielle : la rétention conserve les données pour des raisons légales ou organisationnelles, tandis que la sauvegarde crée des copies pour pouvoir les restaurer en cas d’incident. D’ailleurs, Microsoft encourage lui-même l’utilisation de solutions de sauvegarde tierces pour une protection complète.
Ne nous voilons pas la face, le principal risque se situe souvent entre la chaise et le clavier. L’erreur humaine est la cause la plus fréquente de perte de données. Une suppression accidentelle d’un fichier, d’une liste ou même d’un site entier par un utilisateur ou un administrateur est vite arrivée. Si la suppression n’est pas constatée rapidement, les données peuvent être définitivement perdues une fois les délais de rétention de la corbeille expirés.
L’autre menace, de plus en plus présente, est le ransomware. On imagine souvent que nos données dans le cloud sont à l’abri, mais c’est une erreur. Une attaque peut tout à fait chiffrer les fichiers sur un poste de travail synchronisé, et ces fichiers chiffrés seront ensuite synchronisés sur SharePoint Online et OneDrive. Des chercheurs en sécurité ont même démontré qu’il est possible pour des rançongiciels de cibler spécifiquement les fichiers hébergés sur SharePoint et OneDrive en manipulant le système de versioning pour rendre les anciennes versions inaccessibles. Dans ce scénario, sans une sauvegarde externe et déconnectée, la récupération des données devient un véritable casse-tête.
Bon, vous voyez le truc. La solution Microsoft 365, c’est très bien quand on a les moyens de payer les licences, mais ce n’est pas infaillible. Il existe de nombreuses solutions externes et entreprises qui proposent ce genre de service, mais il faut une nouvelle fois payer. Dans ce tutoriel, on va le faire nous-même à l’aide d’un script. N’oubliez pas que pour que votre script soit opérationnel, il va falloir créer votre application avec les bonnes autorisations API dans Azure via le lien que je vous ai indiqué plus haut. Au-delà de ce prérequis, les autres sont d’avoir une bonne bande passante pour récupérer le contenu et bien évidemment de l’espace de stockage en local.
Sauvegarder manuellement ses sites SharePoint Online en local avec un script Python
Il est maintenant venu le temps de passer aux choses sérieuses après tous ces avertissements. Je me permets de vous en faire un dernier. Faites attention sur la manière dont vous sauvegardez l’ensemble de ces données en local. Il ne faut pas que cela deviennent une nouvelle porte d’entrée pour les méchants qui voudraient vous voler vos précieux secrets qui étaient un minimum sécurisé dans vos sites SharePoint.
Oh mince, j’ai oublié de vous prévenir : nous allons bien évidemment utiliser Python, pour des raisons de simplicité. Ce langage de programmation fait parfaitement l’affaire pour ce besoin et fonctionne partout, tant que vos clés d’API Microsoft Graph sont valides. Il est également très portable, à condition que Python en version 3.x soit installé sur votre hôte. Comme d’habitude, la première étape consiste à créer votre environnement virtuel Python, afin de pouvoir installer toutes les dépendances sans affecter celles de votre système d’exploitation.
Comme d’habitude, il va falloir dans un premier temps créer votre environnement virtuel avec venv ou uv pour éviter d’interagir avec les dépendances Python de votre système d’exploitation. Une fois que c’est fait, nous allons installer tout ce qu’il faut avec un pip install qui va bien :
pip install requests msal python-dotenv
- requests : c’est la bibliothèque qui permet d’envoyer les requêtes HTTP à l’API Microsoft Graph pour lister les sites, les bibliothèques et télécharger les fichiers.
- msal (Microsoft Authentication Library) : elle est utilisée spécifiquement pour gérer le processus d’authentification et obtenir le jeton d’accès nécessaire pour communiquer avec l’API.
- python-dotenv : elle sert à charger les variables (vos identifiants secrets) depuis le fichier .env dans l’environnement de votre script.
Les autres modules utilisés dans le script (json, os, datetime, time, logging) font partie de la bibliothèque standard de Python et n’ont donc pas besoin d’être installés avec pip. Maintenant, que nous avons tout ce qu’il nous faut, il est temps de créer notre fichier .env qui va stocker les variables de l’API Microsoft Graph. Pour cela, un simple fichier .env sera suffisant avec ces 3 lignes :
TENANT_ID = os.getenv("TENANT_ID")
CLIENT_ID = os.getenv("CLIENT_ID")
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
Une fois que c’est fait, il est temps de passer au script à proprement parler, c’est surement ce qui vous interesse le plus et la raison de votre venue sur cet article. Pour sauvegarder l’ensemble de vos SharePoint Online de Microsoft 365, voici le script le script Python qui va le faire pour vous :
import requests import json import os import datetime import time import logging from msal import ConfidentialClientApplication from dotenv import load_dotenv # --- CONFIGURATION (CHARGÉE DEPUIS LE FICHIER .env) --- # Charge les variables d'environnement à partir du fichier .env load_dotenv() # MODIFIÉ : Récupère les identifiants depuis les variables d'environnement TENANT_ID = os.getenv("TENANT_ID") # id de l'annuaire (locataire) CLIENT_ID = os.getenv("CLIENT_ID") # id d'application (client) CLIENT_SECRET = os.getenv("CLIENT_SECRET") # valeur du secret # AJOUT : Vérification critique au démarrage if not all([TENANT_ID, CLIENT_ID, CLIENT_SECRET]): print("ERREUR CRITIQUE : Les variables d'environnement ne sont pas définies.") print("Veuillez créer un fichier .env et y renseigner TENANT_ID, CLIENT_ID, et CLIENT_SECRET.") exit() SITES_TO_EXCLUDE = [ "Designer", "Prise en main", "Pages", "My workspace", "Ideas" ] # --- 1. SETUP INITIAL (INCHANGÉ) --- # Démarrer le chronomètre global start_time = time.time() # Obtenir la date et l'heure actuelles now = datetime.datetime.now() # NOUVEAU: Obtenir le numéro de la semaine (selon la norme ISO 8601) week_number = now.isocalendar()[1] # Création du dossier de sauvegarde principal basé sur le numéro de la semaine main_backup_dir = f"sauvegarde_sharepoint_semaine_{week_number}" os.makedirs(main_backup_dir, exist_ok=True) # Création d'un nom de fichier de log unique basé sur la date et l'heure exactes du lancement log_timestamp = now.strftime("%Y%m%d_%H%M%S") log_file_path = os.path.join(main_backup_dir, f"backup_log_{log_timestamp}.log") # Configuration du logging pour écrire dans un fichier ET dans la console logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(log_file_path), logging.StreamHandler() ] ) logging.info(f"Dossier de sauvegarde principal : '{main_backup_dir}'") logging.info(f"Fichier de log : '{log_file_path}'") # --- 2. AUTHENTIFICATION (INCHANGÉ) --- AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}" SCOPE = ["https://graph.microsoft.com/.default"] app = ConfidentialClientApplication(client_id=CLIENT_ID, authority=AUTHORITY, client_credential=CLIENT_SECRET) def get_fresh_headers(): result = app.acquire_token_for_client(scopes=SCOPE) if "access_token" not in result: raise Exception("Erreur critique : Échec de l'obtention du jeton d'accès.", result.get("error_description")) return {'Authorization': f'Bearer {result["access_token"]}', 'Content-Type': 'application/json'} logging.info("\u2713 Configuration de l'authentification réussie !") # --- FONCTIONS UTILITAIRES (INCHANGÉES) --- def sanitize_name(name): invalid_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*'] for char in invalid_chars: name = name.replace(char, '_') return name def make_graph_api_request(url, max_retries=5): retries = 0 while retries < max_retries: try: headers = get_fresh_headers() response = requests.get(url, headers=headers, timeout=30) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 30)) logging.warning(f"Throttling détecté. Attente de {retry_after} secondes...") time.sleep(retry_after) retries += 1 continue response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: logging.warning(f"Erreur réseau ou HTTP : {e}. Tentative {retries + 1}/{max_retries}...") time.sleep(5 * (retries + 1)) retries += 1 logging.error(f"Échec de la requête vers {url} après {max_retries} tentatives.") return None # # --- FONCTION DE TÉLÉCHARGEMENT EN STREAMING (INCHANGÉE) --- # def download_folder_recursively(drive_id, item_id, local_path): os.makedirs(local_path, exist_ok=True) url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}/children" while url: data = make_graph_api_request(url) if not data: break items = data.get('value', []) for item in items: item_name = sanitize_name(item.get('name')) item_local_path = os.path.join(local_path, item_name) if 'folder' in item: download_folder_recursively(drive_id, item['id'], item_local_path) elif 'file' in item: download_url = item.get('@microsoft.graph.downloadUrl') if download_url: logging.info(f" - Téléchargement : {item_name}") try: with requests.get(download_url, timeout=300, stream=True) as file_response: file_response.raise_for_status() with open(item_local_path, 'wb') as f: for chunk in file_response.iter_content(chunk_size=8192): f.write(chunk) except requests.exceptions.RequestException as e: logging.warning(f" ! Échec du téléchargement pour {item_name}: {e}") url = data.get('@odata.nextLink') # --- 3. SCRIPT PRINCIPAL (INCHANGÉ) --- logging.info("\nÉtape 1/3 : Récupération de la liste de tous les sites SharePoint...") all_sites = [] sites_url = "https://graph.microsoft.com/v1.0/sites?search=*" while sites_url: data = make_graph_api_request(sites_url) if not data: logging.critical("Erreur critique : Impossible de lister les sites. Arrêt du script.") exit() all_sites.extend(data.get('value', [])) sites_url = data.get('@odata.nextLink') logging.info(f"\u2713 {len(all_sites)} sites trouvés au total.") logging.info("\nÉtape 2/3 : Démarrage du processus de sauvegarde pour chaque site...") success_count = 0 fail_count = 0 skipped_count = 0 for i, site in enumerate(all_sites): site_name = site.get('displayName') site_id = site.get('id') if site_name in SITES_TO_EXCLUDE: logging.info(f"\n--- Site {i+1}/{len(all_sites)} : '{site_name}' ignoré (présent dans la liste d'exclusion) ---") skipped_count += 1 continue logging.info(f"\n--- Traitement du site {i+1}/{len(all_sites)} : '{site_name}' ---") try: sanitized_site_name = sanitize_name(site_name) site_backup_dir = os.path.join(main_backup_dir, f"backup_{sanitized_site_name}") drives_url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives?$expand=root" drives_data = make_graph_api_request(drives_url) if not drives_data: raise Exception(f"Impossible de récupérer les bibliothèques pour le site {site_name}") drives = drives_data.get('value', []) if not drives: logging.info(" -> Ce site n'a aucune bibliothèque de documents. Passage au suivant.") success_count += 1 continue logging.info(f" -> {len(drives)} bibliothèque(s) trouvée(s).") for drive in drives: drive_name = sanitize_name(drive.get('name')) drive_id = drive.get('id') drive_root_id = drive.get('root', {}).get('id') drive_backup_path = os.path.join(site_backup_dir, drive_name) logging.info(f" -> Démarrage de la sauvegarde pour la bibliothèque '{drive_name}'...") if drive_root_id: download_folder_recursively(drive_id, drive_root_id, drive_backup_path) else: logging.warning(f" ! Avertissement : Impossible de trouver la racine de la bibliothèque '{drive_name}'.") logging.info(f" \u2713 Sauvegarde du site '{site_name}' terminée avec succès.") success_count += 1 except Exception as e: logging.error(f" \u2717 ERREUR lors de la sauvegarde du site '{site_name}': {e}") fail_count += 1 continue # --- 4. RÉSUMÉ FINAL (INCHANGÉ) --- end_time = time.time() total_seconds = end_time - start_time hours, rem = divmod(total_seconds, 3600) minutes, seconds = divmod(rem, 60) duration_str = f"{int(hours):02d}h {int(minutes):02d}m {int(seconds):02d}s" logging.info("\n--- Étape 3/3 : Processus de sauvegarde terminé ---") logging.info(f"Résumé :") logging.info(f" Sites traités avec succès : {success_count}") logging.info(f" Sites en échec : {fail_count}") logging.info(f" Sites ignorés : {skipped_count}") logging.info(f" Durée totale du script : {duration_str}") logging.info(f"Retrouvez les sauvegardes dans le dossier : '{main_backup_dir}'")
Selon la quantité de données à sauvegarder et la vitesse de votre connexion, ce sera plus ou moins long, mais cela devrait se faire sans encombre. J’ai volontairement exclu des noms de site SharePoint pour éviter de polluer ma sauvegarde de sites inutiles. Vous savez maintenant comment récupérer le contenu de vos sites SharePoint en local sans avoir besoin d’outils tiers, et ce, de manière totalement gratuite !