'folder', 'name' => 'root', 'path' => '', 'children' => [] ]; // Trier les fichiers par chemin pour un traitement cohérent usort($filesList, function($a, $b) { return strcmp($a['name'], $b['name']); }); foreach ($filesList as $file) { $path = $file['name']; $size = $file['size']; $isDir = $file['is_dir']; // Découper le chemin en segments $segments = explode('/', trim($path, '/')); // Naviguer/créer l'arborescence self::addToTree($tree, $segments, $size, $isDir, $path); } Config::log("Arborescence construite : " . self::countNodes($tree) . " nœuds"); return $tree; } /** * Ajoute un fichier/dossier à l'arborescence * * @param array &$node Nœud actuel (passé par référence) * @param array $segments Segments du chemin restants * @param int $size Taille du fichier * @param bool $isDir Si c'est un dossier * @param string $fullPath Chemin complet * @param array $processedSegments Segments déjà traités (pour construire le path des dossiers intermédiaires) */ private static function addToTree(array &$node, array $segments, int $size, bool $isDir, string $fullPath, array $processedSegments = []): void { if (empty($segments)) { return; } $currentSegment = array_shift($segments); // Si c'est vide (cas des chemins se terminant par /), ignorer if ($currentSegment === '') { return; } // Ajouter le segment actuel aux segments traités $processedSegments[] = $currentSegment; // Chercher si ce segment existe déjà dans les enfants $existingChild = null; $existingIndex = null; foreach ($node['children'] as $index => $child) { if ($child['name'] === $currentSegment) { $existingChild = &$node['children'][$index]; $existingIndex = $index; break; } } // Si c'est le dernier segment if (empty($segments)) { // C'est le fichier/dossier final if ($existingChild === null) { $node['children'][] = [ 'type' => $isDir ? 'folder' : 'file', 'name' => $currentSegment, 'path' => $fullPath, 'size' => $size, 'children' => $isDir ? [] : null ]; } else { // Le nœud existe déjà (cas des dossiers déclarés implicitement) // Mettre à jour avec les vraies infos $node['children'][$existingIndex]['type'] = $isDir ? 'folder' : 'file'; $node['children'][$existingIndex]['path'] = $fullPath; $node['children'][$existingIndex]['size'] = $size; if (!$isDir) { $node['children'][$existingIndex]['children'] = null; } } } else { // Il reste des segments, c'est un dossier intermédiaire if ($existingChild === null) { // Construire le chemin du dossier intermédiaire $intermediatePath = implode('/', $processedSegments) . '/'; // Créer un dossier intermédiaire $node['children'][] = [ 'type' => 'folder', 'name' => $currentSegment, 'path' => $intermediatePath, 'size' => 0, 'children' => [] ]; // Récursion sur le nouveau nœud self::addToTree($node['children'][count($node['children']) - 1], $segments, $size, $isDir, $fullPath, $processedSegments); } else { // Le dossier existe, continuer la récursion // Mettre à jour le path s'il était vide if ($node['children'][$existingIndex]['path'] === '') { $node['children'][$existingIndex]['path'] = implode('/', $processedSegments) . '/'; } self::addToTree($node['children'][$existingIndex], $segments, $size, $isDir, $fullPath, $processedSegments); } } } /** * Compte le nombre total de nœuds dans l'arborescence * * @param array $node Nœud racine * @return int Nombre de nœuds */ private static function countNodes(array $node): int { $count = 1; // Le nœud lui-même if (isset($node['children']) && is_array($node['children'])) { foreach ($node['children'] as $child) { $count += self::countNodes($child); } } return $count; } /** * Détecte les conflits entre 2 listes de fichiers * Un conflit = même chemin existe dans les 2 listes * * @param array $leftFiles Liste fichiers ZIP gauche * @param array $rightFiles Liste fichiers ZIP droite * @return array Liste des conflits avec détails */ public static function detectConflicts(array $leftFiles, array $rightFiles): array { $conflicts = []; // Créer un index des fichiers de droite pour recherche rapide $rightIndex = []; foreach ($rightFiles as $file) { $rightIndex[$file['name']] = $file; } // Parcourir les fichiers de gauche foreach ($leftFiles as $leftFile) { $path = $leftFile['name']; // Vérifier si ce chemin existe à droite if (isset($rightIndex[$path])) { $rightFile = $rightIndex[$path]; $conflicts[] = [ 'path' => $path, 'type' => $leftFile['is_dir'] ? 'folder' : 'file', 'left_size' => $leftFile['size'], 'right_size' => $rightFile['size'], 'size_diff' => abs($leftFile['size'] - $rightFile['size']), 'same_size' => $leftFile['size'] === $rightFile['size'] ]; } } Config::log("Conflits détectés : " . count($conflicts) . " chemins en doublon"); return $conflicts; } /** * Obtient tous les chemins d'une arborescence (liste plate) * * @param array $tree Arborescence * @return array Liste de chemins */ public static function getAllPaths(array $tree): array { $paths = []; if (isset($tree['path']) && $tree['path'] !== '') { $paths[] = $tree['path']; } if (isset($tree['children']) && is_array($tree['children'])) { foreach ($tree['children'] as $child) { $paths = array_merge($paths, self::getAllPaths($child)); } } return $paths; } /** * Filtre l'arborescence selon un pattern de recherche * * @param array $tree Arborescence * @param string $searchTerm Terme de recherche (regex compatible) * @param bool $caseSensitive Sensible à la casse * @return array Arborescence filtrée */ public static function filterTree(array $tree, string $searchTerm, bool $caseSensitive = false): array { // Copier l'arbre $filtered = $tree; if (!isset($filtered['children']) || !is_array($filtered['children'])) { return $filtered; } $filteredChildren = []; foreach ($filtered['children'] as $child) { // Vérifier si le nom matche $name = $child['name']; $matches = $caseSensitive ? (stripos($name, $searchTerm) !== false) : (strpos(strtolower($name), strtolower($searchTerm)) !== false); if ($matches) { // Ce nœud matche, l'inclure $filteredChildren[] = $child; } elseif (isset($child['children']) && is_array($child['children'])) { // Chercher récursivement dans les enfants $filteredChild = self::filterTree($child, $searchTerm, $caseSensitive); if (!empty($filteredChild['children'])) { // Des enfants matchent, inclure ce dossier $filteredChildren[] = $filteredChild; } } } $filtered['children'] = $filteredChildren; return $filtered; } /** * Calcule les statistiques d'une arborescence * * @param array $tree Arborescence * @return array Statistiques (total_files, total_folders, total_size, max_depth) */ public static function getTreeStats(array $tree, int $currentDepth = 0): array { $stats = [ 'total_files' => 0, 'total_folders' => 0, 'total_size' => 0, 'max_depth' => $currentDepth ]; if ($tree['type'] === 'folder') { $stats['total_folders']++; } else { $stats['total_files']++; $stats['total_size'] += $tree['size'] ?? 0; } if (isset($tree['children']) && is_array($tree['children'])) { foreach ($tree['children'] as $child) { $childStats = self::getTreeStats($child, $currentDepth + 1); $stats['total_files'] += $childStats['total_files']; $stats['total_folders'] += $childStats['total_folders']; $stats['total_size'] += $childStats['total_size']; $stats['max_depth'] = max($stats['max_depth'], $childStats['max_depth']); } } return $stats; } /** * Convertit une arborescence en liste plate de chemins * * @param array $tree Arborescence * @param array $selection Sélection utilisateur (path => 'left'|'right'|null) * @return array Liste de chemins sélectionnés */ public static function treeToPathList(array $tree, array $selection = []): array { $paths = []; if (isset($tree['path']) && $tree['path'] !== '') { // Vérifier si ce chemin est sélectionné if (empty($selection) || isset($selection[$tree['path']])) { $paths[] = $tree['path']; } } if (isset($tree['children']) && is_array($tree['children'])) { foreach ($tree['children'] as $child) { $childPaths = self::treeToPathList($child, $selection); $paths = array_merge($paths, $childPaths); } } return $paths; } /** * Trouve un nœud dans l'arborescence par son chemin * * @param array $tree Arborescence * @param string $path Chemin à rechercher * @return array|null Nœud trouvé ou null */ public static function findNodeByPath(array $tree, string $path): ?array { if (isset($tree['path']) && $tree['path'] === $path) { return $tree; } if (isset($tree['children']) && is_array($tree['children'])) { foreach ($tree['children'] as $child) { $found = self::findNodeByPath($child, $path); if ($found !== null) { return $found; } } } return null; } /** * Valide une sélection utilisateur * Vérifie qu'il n'y a pas de conflits (même fichier sélectionné des 2 côtés) * * @param array $selection Map path => 'left'|'right'|null * @return array ['valid' => bool, 'conflicts' => array] */ public static function validateSelection(array $selection): array { $pathsBySide = [ 'left' => [], 'right' => [] ]; // Grouper par côté foreach ($selection as $path => $side) { if ($side === 'left' || $side === 'right') { $pathsBySide[$side][] = $path; } } // Chercher les doublons (même path des 2 côtés) $conflicts = array_intersect($pathsBySide['left'], $pathsBySide['right']); return [ 'valid' => empty($conflicts), 'conflicts' => array_values($conflicts) ]; } /** * Optimise une sélection en supprimant les enfants si le parent est sélectionné * * @param array $selection Map path => 'left'|'right'|null * @param array $tree Arborescence (pour connaître la hiérarchie) * @return array Sélection optimisée */ public static function optimizeSelection(array $selection, array $tree): array { $optimized = []; foreach ($selection as $path => $side) { if ($side === null) { continue; } // Vérifier si un parent de ce chemin est déjà sélectionné du même côté $hasParentSelected = false; foreach ($selection as $otherPath => $otherSide) { if ($otherSide === $side && $otherPath !== $path) { // Vérifier si otherPath est un parent de path if (strpos($path, $otherPath . '/') === 0) { $hasParentSelected = true; break; } } } // Si aucun parent n'est sélectionné, garder ce chemin if (!$hasParentSelected) { $optimized[$path] = $side; } } return $optimized; } }