ProblĂšme d'upload de fichier volumineux

The Basic

  • Nextcloud Server version : Nextcloud AIO v11.6.0
  • Operating system and version : Ubuntu 24.04.3 LTS
  • Web server and version (e.g, Apache 2.4.25) ?
  • Reverse proxy and version _(e.g. nginx 1.27.2) : ?
  • PHP version : 8.3.2
  • Is this the first time you’ve seen this error? Non
  • When did this problem seem to first start? Depuis l’installation
  • Installation method : AIO
  • Are you using CloudfIare, mod_security, or similar? Non

Summary of the issue you are facing:

Bonjour Ă  tous,

J’ai installĂ© Nextcloud sur un serveur ubuntu sans aucun problĂšme.

Tout fonctionne sauf le client Windows mais Ă  priori il n’est pas du tout fiable (j’ai essayĂ© de passer toutes mes donnĂ©es OneDrive sur Nexcloud via le client et ça ne prenait que quelques fichiers et j’avais ensuite une ribambelle de messages d’erreur 
 bref j’ai abandonnĂ©).

Mon serveur tourne parfaitement, le cloud est accessible, je peux uploader via le navigateur tous les dossiers et fichiers que je veux SAUF les dossiers d’un certain volume et un fichier de 605Mo.

J’ai pensĂ© Ă  une limitation PHP mais celle-ci est capĂ©e Ă  16Go.

J’ai pensĂ© Ă  une limitation NextCloud mais je n’ai rien trouvĂ© Ă  ce sujet.

Pas de rupture de connexion, elle est parfaitement fonctionnelle.

Pas de sujet d’espace, j’ai 3,8To encore de libres.

Seul paramĂštre potentiel : la limite de mĂ©moire PHP de 512MB, mais celĂ  n’aurait pas de sens 


J’ai Ă  nouveau tentĂ© d’uploader ledit fichier via la page web sur Chrome (Version 141.0.7390.108 (Build officiel) (64 bits)) et voici le dĂ©tail des messages de log successifs avec un “NO APP IN CONTEXT” :

#1 :

{"reqId":"rmZNd2hoeo7UnbyPle28","level":3,"time":"2025-10-20T19:30:30+00:00","remoteAddr":"92.XXX.XX.XXX","user":"Olivier XXXXXX","app":"no app in context","method":"PUT","url":"/remote.php/dav/uploads/Olivier%20XXXXXX/web-file-upload-df6db3e88c64082f/6","message":"Taille du fichier attendue : 95485343 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 0 octet. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur.","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36","version":"31.0.8.1","exception":{"Exception":"Sabre\\DAV\\Exception\\BadRequest","Message":"Taille du fichier attendue : 95485343 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 0 octet. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur.","Code":0,"Trace":[{"file":"/var/www/html/apps/dav/lib/Connector/Sabre/Directory.php","line":116,"function":"put","class":"OCA\\DAV\\Connector\\Sabre\\File","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/apps/dav/lib/Upload/UploadFolder.php","line":28,"function":"createFile","class":"OCA\\DAV\\Connector\\Sabre\\Directory","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/3rdparty/sabre/dav/lib/DAV/Server.php","line":1098,"function":"createFile","class":"OCA\\DAV\\Upload\\UploadFolder","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/3rdparty/sabre/dav/lib/DAV/CorePlugin.php","line":504,"function":"createFile","class":"Sabre\\DAV\\Server","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/3rdparty/sabre/event/lib/WildcardEmitterTrait.php","line":89,"function":"httpPut","class":"Sabre\\DAV\\CorePlugin","type":"->","args":[{"__class__":"Sabre\\HTTP\\Request"},{"__class__":"Sabre\\HTTP\\Response"}]},{"file":"/var/www/html/3rdparty/sabre/dav/lib/DAV/Server.php","line":472,"function":"emit","class":"Sabre\\DAV\\Server","type":"->","args":["method:PUT",[{"__class__":"Sabre\\HTTP\\Request"},{"__class__":"Sabre\\HTTP\\Response"}]]},{"file":"/var/www/html/apps/dav/lib/Connector/Sabre/Server.php","line":49,"function":"invokeMethod","class":"Sabre\\DAV\\Server","type":"->","args":[{"__class__":"Sabre\\HTTP\\Request"},{"__class__":"Sabre\\HTTP\\Response"}]},{"file":"/var/www/html/apps/dav/lib/Server.php","line":401,"function":"start","class":"OCA\\DAV\\Connector\\Sabre\\Server","type":"->","args":[]},{"file":"/var/www/html/apps/dav/appinfo/v2/remote.php","line":21,"function":"exec","class":"OCA\\DAV\\Server","type":"->","args":[]},{"file":"/var/www/html/remote.php","line":145,"args":["/var/www/html/apps/dav/appinfo/v2/remote.php"],"function":"require_once"}],"File":"/var/www/html/apps/dav/lib/Connector/Sabre/File.php","Line":257,"message":"Taille du fichier attendue : 95485343 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 0 octet. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur.","exception":[],"CustomMessage":"Taille du fichier attendue : 95485343 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 0 octet. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur."},"id":"68f68e5db6cba"}

#2 :

{"reqId":"KD33o6bYOJI7x8mZWE3N","level":3,"time":"2025-10-20T19:31:19+00:00","remoteAddr":"92.XXX.XX.XXX","user":"Olivier XXXXXX","app":"no app in context","method":"PUT","url":"/remote.php/dav/uploads/Olivier%20XXXXXX/web-file-upload-df6db3e88c64082f/5","message":"Taille du fichier attendue : 104857600 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 4005888 octets. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur.","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36","version":"31.0.8.1","exception":{"Exception":"Sabre\\DAV\\Exception\\BadRequest","Message":"Taille du fichier attendue : 104857600 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 4005888 octets. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur.","Code":0,"Trace":[{"file":"/var/www/html/apps/dav/lib/Connector/Sabre/Directory.php","line":116,"function":"put","class":"OCA\\DAV\\Connector\\Sabre\\File","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/apps/dav/lib/Upload/UploadFolder.php","line":28,"function":"createFile","class":"OCA\\DAV\\Connector\\Sabre\\Directory","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/3rdparty/sabre/dav/lib/DAV/Server.php","line":1098,"function":"createFile","class":"OCA\\DAV\\Upload\\UploadFolder","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/3rdparty/sabre/dav/lib/DAV/CorePlugin.php","line":504,"function":"createFile","class":"Sabre\\DAV\\Server","type":"->","args":["*** sensitive parameters replaced ***"]},{"file":"/var/www/html/3rdparty/sabre/event/lib/WildcardEmitterTrait.php","line":89,"function":"httpPut","class":"Sabre\\DAV\\CorePlugin","type":"->","args":[{"__class__":"Sabre\\HTTP\\Request"},{"__class__":"Sabre\\HTTP\\Response"}]},{"file":"/var/www/html/3rdparty/sabre/dav/lib/DAV/Server.php","line":472,"function":"emit","class":"Sabre\\DAV\\Server","type":"->","args":["method:PUT",[{"__class__":"Sabre\\HTTP\\Request"},{"__class__":"Sabre\\HTTP\\Response"}]]},{"file":"/var/www/html/apps/dav/lib/Connector/Sabre/Server.php","line":49,"function":"invokeMethod","class":"Sabre\\DAV\\Server","type":"->","args":[{"__class__":"Sabre\\HTTP\\Request"},{"__class__":"Sabre\\HTTP\\Response"}]},{"file":"/var/www/html/apps/dav/lib/Server.php","line":401,"function":"start","class":"OCA\\DAV\\Connector\\Sabre\\Server","type":"->","args":[]},{"file":"/var/www/html/apps/dav/appinfo/v2/remote.php","line":21,"function":"exec","class":"OCA\\DAV\\Server","type":"->","args":[]},{"file":"/var/www/html/remote.php","line":145,"args":["/var/www/html/apps/dav/appinfo/v2/remote.php"],"function":"require_once"}],"File":"/var/www/html/apps/dav/lib/Connector/Sabre/File.php","Line":257,"message":"Taille du fichier attendue : 104857600 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 4005888 octets. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur.","exception":[],"CustomMessage":"Taille du fichier attendue : 104857600 octets mais taille du fichier lue (depuis le client Nextcloud) et Ă©crit (dans le stockage Nextcloud) : 4005888 octets. Cela peut ĂȘtre un problĂšme de rĂ©seau au niveau du client ou un problĂšme de stockage au niveau du serveur."},"id":"68f68e5db6c9b"}

J’ai le mĂȘme problĂšme sous EDGE (Version 141.0.3537.85 (Version officielle) (64 bits))

Ni la documentation NextCloud, ni ChatGPT, ni Perplexity n’ont pu en venir à bout, je sollicite donc votre aide pour diagnostiquer ce sujet afin que je puisse le traiter 


Par avance, un grand merci !

ComplĂ©ment d’information : je suis passĂ© par WinSCP / Webdave pour essayer d’uploader le fichier et ça fonctionne correctement, les 605Mo sont passĂ©s comme une lettre Ă  la poste 
 auriez-vous une idĂ©e de l’origine du problĂšme par l’interface Web ?

Bonsoir @Olivier0, pour les téléversement de fichier nextcloud utilise soit la ram disponible ou un dossier temporaire pour stocker le fichier sous forme de plusieurs fichiers (chunks) puis reconstitue le fichier final dans le dossier de stocke par défaut.

Assurez vous que le disque / partition stockant votre conteneur nextcloud AIO dispose assez de place pour la taille de fichier que vous souhaitez transférer.

Bonjour Mageunic,

Merci pour votre retour.

ParallĂšlement, puisque mon client Nextcloud plantait Ă©galement les uploads et en vue d’automatiser la synchronisation, j’ai dĂ©veloppĂ© un bout de code python pour assurer l’upload de gros fichiers et la synchronisation en permanence et cela fonctionne.

Je le partage ici au cas oĂč quelqu’un serait intĂ©ressĂ© :

import os
import json
import hashlib
import time  # Nécessaire pour time.sleep()
from urllib.parse import urljoin, quote
import requests
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

# --- Configuration Globale (Identifiants WebDAV) ---
USERNAME = "XXXXXX"  # Votre nom d'utilisateur WebDAV
PASSWORD = "YYYYYY"  # Votre mot de passe WebDAV
AUTH = (USERNAME, PASSWORD)
REQUESTS_TIMEOUT = 300  # Timeout pour les requĂȘtes HTTP

# --- Configuration des PAIRES de Synchronisation ---
SYNC_PAIRS = [
    {
        "local_path": r"C:\OneDrive\Dossier 01",
        "cloud_url": "https://serveur_nextcloud.fr/remote.php/dav/files/user/Dossier_01/",
        "state_file": "sync_state_01.json"
    },
    {
        "local_path": r"C:\Quelque part\Dossier 02",
        "cloud_url": "https://serveur_nextcloud.fr/remote.php/dav/files/user/Dossier_02/",
        "state_file": "sync_state_02.json"
    },
    {
        "local_path": r"C:\Ailleurs\Dossier 03",
        "cloud_url": "https://serveur_nextcloud.fr/remote.php/dav/files/user/Dossier_03/",
        "state_file": "sync_state_03.json"
    }
]

# --- Fonctions Utilitaires ---

def get_webdav_url(cloud_url, path=""):
    """Construit l'URL WebDAV encodée pour un chemin donné."""
    encoded_path = '/'.join(quote(part) for part in path.split('/'))
    return urljoin(cloud_url, encoded_path)

def load_state(state_file):
    """Charge l'état de la derniÚre synchronisation depuis un fichier JSON."""
    if os.path.exists(state_file):
        with open(state_file, 'r') as f:
            try:
                return json.load(f)
            except json.JSONDecodeError:
                return {}
    return {}

def save_state(state, state_file):
    """Sauvegarde l'état actuel de la synchronisation dans un fichier JSON."""
    with open(state_file, 'w') as f:
        json.dump(state, f, indent=4)

def get_file_hash(filepath):
    """Calcule le hachage d'un fichier pour une comparaison de contenu fiable."""
    hasher = hashlib.sha256()
    try:
        with open(filepath, 'rb') as file:
            for chunk in iter(lambda: file.read(4096), b""):
                hasher.update(chunk)
        return hasher.hexdigest()
    except Exception:
        return None

# --- SCANNER LE DOSSIER LOCAL ---

def scan_local_folder(local_folder):
    """Scanne le dossier local et retourne un dictionnaire d'état."""
    local_state = {}
    print(f"Scanning local folder: {local_folder}")
    
    if not os.path.exists(local_folder):
        os.makedirs(local_folder)
        print(f"Local folder created: {local_folder}")
        return local_state
        
    for root, dirs, files in os.walk(local_folder):
        
        # 1. Traiter les DOSSIERS
        for name in dirs:
            full_path = os.path.join(root, name)
            rel_path = os.path.relpath(full_path, local_folder).replace('\\', '/')
            local_state[rel_path] = {
                "type": "dir",
                "modified": int(os.path.getmtime(full_path)),
            }
            
        # 2. Traiter les FICHIERS
        for name in files:
            full_path = os.path.join(root, name)
            rel_path = os.path.relpath(full_path, local_folder).replace('\\', '/')
            stat_info = os.stat(full_path)
            
            local_state[rel_path] = {
                "type": "file",
                "size": stat_info.st_size,
                "modified": int(stat_info.st_mtime), 
                "hash": get_file_hash(full_path)
            }
            
    print(f"Local scan complete. Found {len(local_state)} items.")
    return local_state

# --- Opérations WebDAV ---

def upload_file(config, rel_path, full_path, max_retries=5, delay=1):
    """Téléverse/met à jour un fichier vers le cloud avec logique de réessai."""
    cloud_path = get_webdav_url(config["cloud_url"], rel_path)
    print(f"  âŹ†ïž Uploading/Updating file: {rel_path}")
    
    for attempt in range(max_retries):
        try:
            # Tente d'ouvrir le fichier. Si PermissionError, l'exception est levée.
            with open(full_path, 'rb') as f:
                response = requests.put(cloud_path, data=f, auth=AUTH, timeout=REQUESTS_TIMEOUT)
                response.raise_for_status()
            
            # Si l'opération réussit aprÚs potentiellement plusieurs tentatives
            print(f"  ✅ Uploaded (Attempt {attempt + 1}): {rel_path}")
            return True
            
        except PermissionError:
            if attempt < max_retries - 1:
                # La PermissionError indique que le fichier est verrouillé par une autre application.
                print(f"  ⚠ PermissionError on {rel_path}. Retrying in {delay}s...")
                time.sleep(delay)
            else:
                # Échec aprùs toutes les tentatives
                print(f"  ❌ Failed to open file after {max_retries} attempts (Permission denied). Skipping: {rel_path}")
                return False
                
        except requests.exceptions.RequestException as e:
            # Gestion des erreurs WebDAV/réseau (si l'erreur n'est pas une PermissionError)
            print(f"  ❌ Error uploading {rel_path}: {e}")
            return False
            
    return False # Devrait ĂȘtre atteint uniquement si l'erreur n'est pas gĂ©rĂ©e dans les blocs except

def create_directory(config, rel_path):
    """Crée un dossier sur le cloud."""
    cloud_path = get_webdav_url(config["cloud_url"], rel_path)
    print(f"  ➕ Creating directory: {rel_path}")
    try:
        response = requests.request("MKCOL", cloud_path, auth=AUTH, timeout=REQUESTS_TIMEOUT)
        if response.status_code in [201, 405]:
            print(f"  ✅ Created/Exists: {rel_path}")
            return True
        response.raise_for_status()
    except requests.exceptions.RequestException as e:
        print(f"  ❌ Error creating directory {rel_path}: {e}")
        return False

def delete_element(config, rel_path):
    """Supprime un fichier ou un dossier sur le cloud."""
    cloud_path = get_webdav_url(config["cloud_url"], rel_path)
    print(f"  đŸ—‘ïž Deleting cloud element: {rel_path}")
    try:
        response = requests.delete(cloud_path, auth=AUTH, timeout=REQUESTS_TIMEOUT)
        if response.status_code in [204, 404]:
            print(f"  ✅ Deleted/Not Found: {rel_path}")
            return True
        response.raise_for_status()
    except requests.exceptions.RequestException as e:
        print(f"  ❌ Error deleting {rel_path}: {e}")
        return False

# --- Classe Gestionnaire d'ÉvĂ©nements Watchdog ---

class WebDAVSyncHandler(FileSystemEventHandler):
    """
    GÚre les événements du systÚme de fichiers (création, modification, suppression)
    et déclenche la synchronisation WebDAV pour les éléments concernés.
    """
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.local_folder = config["local_path"]
        self.state_file = config["state_file"]

    def _get_rel_path(self, path):
        """Calcule le chemin relatif pour WebDAV."""
        rel_path = os.path.relpath(path, self.local_folder).replace('\\', '/')
        return rel_path if rel_path != '.' else ''

    def on_created(self, event):
        if not event.is_directory:
            self._sync_action(event.src_path, action="create_file")
        else:
            self._sync_action(event.src_path, action="create_dir")

    def on_deleted(self, event):
        self._sync_action(event.src_path, action="delete")

    def on_modified(self, event):
        if not event.is_directory:
            self._sync_action(event.src_path, action="modify")

    def on_moved(self, event):
        self._sync_action(event.src_path, action="delete") # L'ancienne source est supprimée
        self._sync_action(event.dest_path, action="create_file") # La nouvelle destination est créée

    def _sync_action(self, full_path, action):
        """Exécute l'opération WebDAV basée sur l'événement."""
        rel_path = self._get_rel_path(full_path)
        
        # Ignorer les chemins non valides (ex: fichiers temporaires/systĂšme)
        if not rel_path or rel_path.startswith('.') or rel_path.endswith('~'):
            return

        print(f"\n[EVENT] {action.upper()} detected on: {rel_path}")
        
        state = load_state(self.state_file)
        success = False
        
# --- Actions d'Upload/Création ---
        if action in ["create_file", "modify", "create_dir"]:
            
            if not os.path.exists(full_path) or os.path.isdir(full_path):
                # Ignorer si le fichier disparaßt ou si c'est un dossier (sauf création de dossier)
                if action == "create_dir" and os.path.isdir(full_path):
                    success = create_directory(self.config, rel_path)
                return

            # --- NOUVELLE LOGIQUE DE VÉRIFICATION (POUR MODIFICATION) ---
            current_hash = get_file_hash(full_path)
            previous_data = state.get(rel_path)
            
            if action == "modify" and previous_data and previous_data.get("hash") == current_hash:
                print(f"  ⏭ Ignored modification: Content hash is unchanged (st_atime update only?).")
                # Mettre à jour les autres métadonnées (comme la taille et l'horodatage mtime) dans l'état
                # pour éviter des re-vérifications futures inutiles.
                stat_info = os.stat(full_path)
                previous_data["size"] = stat_info.st_size
                previous_data["modified"] = int(stat_info.st_mtime)
                save_state(state, self.state_file)
                return # ArrĂȘte le traitement, pas d'upload.
            # --- FIN DE LA NOUVELLE LOGIQUE ---

            # Code d'upload existant (s'exécute si c'est une création OU si le hachage a changé)
            
            # 1. Assurer la création du dossier parent
            parent_dir = os.path.dirname(rel_path)
            if parent_dir:
                create_directory(self.config, parent_dir)
            
            # 2. Upload
            success = upload_file(self.config, rel_path, full_path)
            
            # 3. Mise à jour de l'état
            if success:
                new_data = {
                    "type": "file",
                    "size": os.path.getsize(full_path),
                    "modified": int(os.path.getmtime(full_path)),
                    "hash": current_hash, # Utiliser le hachage déjà calculé
                }
                state[rel_path] = new_data
                save_state(state, self.state_file)

        # --- Action de Suppression ---
        elif action == "delete":
            
            success = delete_element(self.config, rel_path)
            
            if success and rel_path in state:
                del state[rel_path]
                save_state(state, self.state_file)


# --- Logique de Synchronisation Globale (Mode Temps Réel) ---

def run_initial_sync_and_start_watchdog():
    """
    1. Effectue la synchronisation complĂšte initiale (pour rattraper les changements hors-ligne).
    2. Démarre l'observateur watchdog pour la surveillance en temps réel.
    """
    
    if not SYNC_PAIRS:
        print("Avertissement: Aucune paire de synchronisation définie dans SYNC_PAIRS.")
        return

    observer = Observer()
    
    print("--- Démarrage de la synchronisation initiale (ratrappage) ---")
    
    def initial_full_sync(config):
        local_folder = config["local_path"]
        state_file = config["state_file"]
        print(f"\n[INITIAL] Processing: {local_folder}")
        
        previous_state = load_state(state_file)
        current_local_state = scan_local_folder(local_folder)
        new_state = {}
        
        # Phase 1: Créations/Mises à Jour
        sorted_local_paths = sorted(current_local_state.keys())
        for rel_path in sorted_local_paths:
            current_data = current_local_state[rel_path]
            previous_data = previous_state.get(rel_path)
            full_path = os.path.join(local_folder, rel_path)
            
            is_new = (previous_data is None)
            is_modified = (
                current_data.get("type") == "file" and 
                previous_data and previous_data.get("type") == "file" and 
                current_data.get("hash") != previous_data.get("hash")
            )
            
            success = True
            if is_new or is_modified:
                if current_data["type"] == "dir":
                    success = create_directory(config, rel_path)
                else: 
                    parent_dir = os.path.dirname(rel_path)
                    if parent_dir: create_directory(config, parent_dir)
                    # Utilise la fonction d'upload AVEC réessai
                    success = upload_file(config, rel_path, full_path)
            
            if success:
                new_state[rel_path] = current_data
            elif previous_state:
                new_state[rel_path] = previous_state.get(rel_path)

        # Phase 2: Suppressions (Anti-Orphelin)
        deleted_paths = [rel_path for rel_path in previous_state if rel_path not in current_local_state]
        deleted_paths.sort(reverse=True)
        for rel_path in deleted_paths:
            delete_element(config, rel_path)
        
        save_state(new_state, state_file)
        print(f"✅ Initial Sync for {local_folder} complete.")
        
    for config in SYNC_PAIRS:
        try:
            initial_full_sync(config)
            
            # Démarrer l'observateur pour chaque dossier aprÚs la synchro initiale
            event_handler = WebDAVSyncHandler(config)
            observer.schedule(event_handler, config["local_path"], recursive=True)
            
        except Exception as e:
            print(f"!!! ÉCHEC de la synchro initiale ou du dĂ©marrage de l'observateur pour {config['local_path']}: {e} !!!")

    # --- Démarrer l'Observateur Watchdog ---
    print("\n========================================================")
    print("✹ DÉMARRAGE DU MODE SURVEILLANCE EN TEMPS RÉEL (Watchdog)")
    print("   Laissez cette fenĂȘtre ouverte pour maintenir la synchro.")
    print("========================================================")

    observer.start()
    
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
        print("\nArrĂȘt de la surveillance demandĂ© par l'utilisateur.")
        
    observer.join()
    print("Programme terminé.")


# --- Point d'entrée de l'application ---

if __name__ == "__main__":
    try:
        import requests
        import watchdog
    except ImportError:
        print("Erreur: Les librairies 'requests' et 'watchdog' ne sont pas installées.")
        print("Veuillez exécuter: pip install requests watchdog")
        exit(1)
        
    try:
        run_initial_sync_and_start_watchdog()
    except Exception as e:
        print(f"\n!!! Erreur fatale au niveau de l'application: {e} !!!")

Je me suis aidé de Gemini pour arriver trÚs rapidement à quelque-chose de viable.

Si cela peut servir à quelqu’un.

Excellente journée à vous Mageunic.

Bien cordialement.

Olivier.