['name' => , ...], // sub-folders, keyed by segment * 'files' => [['name'=>..,'path'=>..,'size'=>..], ...]] * $files are rows with a full-path 'filename'; $folders are rows with 'path'. */ function build_file_tree(array $files, array $folders): array { $root = ['dirs' => [], 'files' => []]; // Returns a *reference* to the node for $path, creating each level as needed. $ensure_dir = function & (string $path) use (&$root) { $node = &$root; foreach (explode('/', trim($path, '/')) as $seg) { if ($seg === '') { continue; } if (!isset($node['dirs'][$seg])) { $node['dirs'][$seg] = ['dirs' => [], 'files' => []]; } $node = &$node['dirs'][$seg]; } return $node; }; // Folders first so empty folders still appear. foreach ($folders as $fo) { $ref = &$ensure_dir($fo['path']); unset($ref); } foreach ($files as $f) { $full = $f['filename']; $dir = dirname($full); $dir = ($dir === '.' || $dir === '') ? '' : $dir; $node = &$ensure_dir($dir); $node['files'][] = [ 'name' => basename($full), 'path' => $full, 'size' => (int) ($f['filesize'] ?? 0), ]; unset($node); } return $root; } /** * Render a tree node as a nested '; return $out; } /** Set of every ancestor directory path of a file, e.g. "a/b/c.txt" => a, a/b. */ function ancestor_dirs(string $path): array { $open = []; $accum = []; $segments = explode('/', trim($path, '/')); array_pop($segments); // drop the filename itself foreach ($segments as $seg) { if ($seg === '') { continue; } $accum[] = $seg; $open[implode('/', $accum)] = true; } return $open; }