189 lines
5.3 KiB
PHP
189 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\DocumentManager;
|
|
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Str;
|
|
use RecursiveDirectoryIterator;
|
|
use RecursiveIteratorIterator;
|
|
use FilesystemIterator;
|
|
|
|
class FolderCatalog
|
|
{
|
|
/**
|
|
* Cached flattened catalog.
|
|
*/
|
|
protected ?Collection $flattened = null;
|
|
|
|
/**
|
|
* Raw catalog definition pulled from configuration.
|
|
*/
|
|
protected array $catalog;
|
|
|
|
public function __construct(?array $catalog = null)
|
|
{
|
|
$this->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));
|
|
}
|
|
}
|