catalog = $catalog ?? (array) config('document-folders.catalog', []); } /** * Get the raw catalog structure as defined in the configuration file. */ public function all(): array { return $this->catalog; } /** * Get a flattened collection containing each folder entry with metadata. */ public function flattened(): Collection { if ($this->flattened !== null) { return $this->flattened; } $items = $this->flattenNodes($this->catalog); return $this->flattened = collect($items) ->map(function (array $item) { $path = trim($item['path'] ?? '', '/'); $number = Arr::get($item, 'number'); $name = Arr::get($item, 'name'); return [ 'number' => $number, 'name' => $name, 'path' => $path, 'default_visible' => Arr::get($item, 'default_visible', true), 'is_system' => Arr::get($item, 'is_system', false), 'level' => Arr::get($item, 'level', 0), 'display' => trim(sprintf('%s %s', $number, $name)), ]; }) ->filter(fn ($item) => $item['path'] !== '') ->values(); } /** * Return a collection of directory paths relative to storage/documents. */ public function paths(): Collection { return $this->flattened() ->pluck('path') ->map(fn ($path) => trim($path, '/')) ->unique() ->values(); } /** * Create missing directories under the provided documents root path. * * @return array{created: array, existing: array} */ public function sync(string $rootPath, bool $dryRun = false): array { $rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR); $created = []; $existing = []; foreach ($this->paths() as $relativePath) { $fullPath = $rootPath . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath); if (File::isDirectory($fullPath)) { $existing[] = $relativePath; continue; } if (! $dryRun) { File::ensureDirectoryExists($fullPath); } $created[] = $relativePath; } return compact('created', 'existing'); } /** * Detect directories that exist on disk but are not part of the catalog. */ public function detectExtra(string $rootPath): Collection { $rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR); if (! File::isDirectory($rootPath)) { return collect(); } $defined = $this->paths(); $actual = collect($this->scanDirectories($rootPath)); return $actual->diff($defined)->values(); } /** * Recursively flatten catalog definition. */ protected function flattenNodes(array $nodes, int $level = 0): array { $items = []; foreach ($nodes as $node) { if (! is_array($node)) { continue; } $item = $node; $item['level'] = $level; $items[] = $item; if (! empty($node['children']) && is_array($node['children'])) { $items = array_merge($items, $this->flattenNodes($node['children'], $level + 1)); } } return $items; } /** * Collect relative directory paths recursively from the given root. */ protected function scanDirectories(string $rootPath): array { $rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR); $directories = []; if (! File::isDirectory($rootPath)) { return $directories; } // Include direct children first (top-level ordering) foreach (File::directories($rootPath) as $directory) { $relative = trim(Str::after($directory, $rootPath . DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR); if ($relative !== '') { $directories[] = $relative; } } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($rootPath, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $path => $info) { if ($info->isDir()) { $relative = trim(Str::after($path, $rootPath . DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR); if ($relative !== '') { $directories[] = $relative; } } } return array_values(array_unique($directories)); } }