diff --git a/database/factories/FileSystemItemFactory.php b/database/factories/FileSystemItemFactory.php index a9321d4..fcedce1 100644 --- a/database/factories/FileSystemItemFactory.php +++ b/database/factories/FileSystemItemFactory.php @@ -88,7 +88,7 @@ public function inFolder(FileSystemItem $folder): static ]); } - public function withParent(?int $parentId): static + public function withParent(int|string|null $parentId): static { return $this->state(fn (array $attributes) => [ 'parent_id' => $parentId, diff --git a/src/Adapters/DatabaseAdapter.php b/src/Adapters/DatabaseAdapter.php index 7f52e00..c77464d 100644 --- a/src/Adapters/DatabaseAdapter.php +++ b/src/Adapters/DatabaseAdapter.php @@ -9,6 +9,7 @@ use MWGuerra\FileManager\Contracts\FileManagerAdapterInterface; use MWGuerra\FileManager\Contracts\FileManagerItemInterface; use MWGuerra\FileManager\Contracts\FileSystemItemInterface; +use Ramsey\Uuid\Uuid; /** * Database adapter for file management. @@ -54,16 +55,24 @@ protected function model(): string } /** - * Find a model by ID. + * Find a model by ID (supports both integer and UUID). */ - protected function find(int $id): ?FileSystemItemInterface + protected function find(int|string $id): ?FileSystemItemInterface { return $this->model()::find($id); } + /** + * Check if a string looks like a valid UUID. + */ + protected function isUuid(string $value): bool + { + return Uuid::isValid($value); + } + /** * Convert identifier to model. - * In database mode, identifier is the model ID. + * In database mode, identifier is the model ID (int or UUID). */ protected function getModelFromIdentifier(string $identifier): ?FileSystemItemInterface { @@ -71,6 +80,10 @@ protected function getModelFromIdentifier(string $identifier): ?FileSystemItemIn return $this->find((int) $identifier); } + if ($this->isUuid($identifier)) { + return $this->find($identifier); + } + return null; } @@ -79,10 +92,10 @@ protected function getModelFromIdentifier(string $identifier): ?FileSystemItemIn * For database mode, we need to resolve the path to a folder ID. * * This method is optimized to avoid N+1 queries by: - * 1. Short-circuiting for numeric IDs + * 1. Short-circuiting for numeric IDs and UUIDs * 2. Using recursive path traversal when needed */ - protected function pathToFolderId(?string $path): ?int + protected function pathToFolderId(?string $path): int|string|null { if ($path === null || $path === '' || $path === '/') { return null; @@ -93,6 +106,11 @@ protected function pathToFolderId(?string $path): ?int return (int) $path; } + // Path might be a UUID + if ($this->isUuid($path)) { + return $path; + } + // Normalize path $path = '/' . ltrim($path, '/'); @@ -107,7 +125,7 @@ protected function pathToFolderId(?string $path): ?int * 1. It only queries folders at each level of the path * 2. It stops as soon as a segment isn't found */ - protected function resolvePathToId(string $path): ?int + protected function resolvePathToId(string $path): int|string|null { // Split path into segments $segments = array_filter(explode('/', $path), fn ($s) => $s !== ''); diff --git a/src/Console/Commands/RebuildFileSystemItemsCommand.php b/src/Console/Commands/RebuildFileSystemItemsCommand.php index 5b8ec39..d3a1f0b 100644 --- a/src/Console/Commands/RebuildFileSystemItemsCommand.php +++ b/src/Console/Commands/RebuildFileSystemItemsCommand.php @@ -132,7 +132,7 @@ public function handle(): int /** * Recursively scan a directory and create database records. */ - protected function scanDirectory($storage, string $modelClass, string $path, ?int $parentId): void + protected function scanDirectory($storage, string $modelClass, string $path, int|string|null $parentId): void { // Get directories and files $directories = $storage->directories($path); diff --git a/src/Console/Commands/UploadFolderCommand.php b/src/Console/Commands/UploadFolderCommand.php index 0ee7f01..3243c8c 100644 --- a/src/Console/Commands/UploadFolderCommand.php +++ b/src/Console/Commands/UploadFolderCommand.php @@ -188,7 +188,7 @@ protected function countLocalItems(string $path): array /** * Ensure target folder exists in storage and database. */ - protected function ensureTargetFolder(string $modelClass, $storage, string $target): ?int + protected function ensureTargetFolder(string $modelClass, $storage, string $target): int|string|null { $parts = array_filter(explode('/', $target)); $parentId = null; @@ -237,7 +237,7 @@ protected function uploadDirectory( $storage, string $storagePath, ?string $modelClass, - ?int $parentId + int|string|null $parentId ): void { $items = File::files($localPath); $directories = File::directories($localPath); diff --git a/src/Contracts/FileSystemItemInterface.php b/src/Contracts/FileSystemItemInterface.php index b748864..7965173 100644 --- a/src/Contracts/FileSystemItemInterface.php +++ b/src/Contracts/FileSystemItemInterface.php @@ -90,17 +90,17 @@ public function getFormattedDuration(): string; /** * Get folder tree structure for sidebar. */ - public static function getFolderTree(?int $parentId = null): array; + public static function getFolderTree(int|string|null $parentId = null): array; /** * Get items in a folder (by parent_id). */ - public static function getItemsInFolder(?int $parentId = null): Collection; + public static function getItemsInFolder(int|string|null $parentId = null): Collection; /** * Count direct files in a folder (static version for null parent). */ - public static function getDirectFileCountForFolder(?int $folderId): int; + public static function getDirectFileCountForFolder(int|string|null $folderId): int; /** * Determine file type from mime type. diff --git a/src/Filament/Pages/FileManager.php b/src/Filament/Pages/FileManager.php index 7e58227..5339396 100644 --- a/src/Filament/Pages/FileManager.php +++ b/src/Filament/Pages/FileManager.php @@ -387,7 +387,7 @@ public function navigateTo(?string $path): void /** * Navigate to folder by ID (for database mode compatibility). */ - public function navigateToId(?int $folderId): void + public function navigateToId(int|string|null $folderId): void { $this->currentPath = $folderId ? (string) $folderId : null; $this->selectedItems = []; diff --git a/src/Models/FileSystemItem.php b/src/Models/FileSystemItem.php index 04fa1ca..9f0c5ff 100644 --- a/src/Models/FileSystemItem.php +++ b/src/Models/FileSystemItem.php @@ -288,7 +288,7 @@ public function getFileCountRecursive(): int /** * Count direct files in a folder (static version for null parent). */ - public static function getDirectFileCountForFolder(?int $folderId): int + public static function getDirectFileCountForFolder(int|string|null $folderId): int { return static::where('parent_id', $folderId) ->where('type', '!=', FileSystemItemType::Folder->value) @@ -298,7 +298,7 @@ public static function getDirectFileCountForFolder(?int $folderId): int /** * Count all files in a folder and its descendants (static version for null parent). */ - public static function getFileCountForFolder(?int $folderId): int + public static function getFileCountForFolder(int|string|null $folderId): int { if ($folderId === null) { // Root: count all files in the system @@ -312,7 +312,7 @@ public static function getFileCountForFolder(?int $folderId): int /** * Get folder tree structure for sidebar. */ - public static function getFolderTree(?int $parentId = null): array + public static function getFolderTree(int|string|null $parentId = null): array { $folders = static::where('type', FileSystemItemType::Folder->value) ->where('parent_id', $parentId) @@ -333,7 +333,7 @@ public static function getFolderTree(?int $parentId = null): array /** * Get items in a folder (by parent_id). */ - public static function getItemsInFolder(?int $parentId = null): \Illuminate\Database\Eloquent\Collection + public static function getItemsInFolder(int|string|null $parentId = null): \Illuminate\Database\Eloquent\Collection { return static::where('parent_id', $parentId) ->orderByRaw("CASE WHEN type = '" . FileSystemItemType::Folder->value . "' THEN 0 ELSE 1 END") diff --git a/tests/Fixtures/UuidFileSystemItem.php b/tests/Fixtures/UuidFileSystemItem.php new file mode 100644 index 0000000..825050c --- /dev/null +++ b/tests/Fixtures/UuidFileSystemItem.php @@ -0,0 +1,257 @@ + 'integer', + 'duration' => 'integer', + ]; + + protected static function newFactory(): Factory + { + return UuidFileSystemItemFactory::new(); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(static::class, 'parent_id'); + } + + public function children(): HasMany + { + return $this->hasMany(static::class, 'parent_id'); + } + + public function descendants(): HasMany + { + return $this->children()->with('descendants'); + } + + public function getType(): FileSystemItemType + { + return FileSystemItemType::from($this->type); + } + + public function getFileType(): ?FileType + { + return $this->file_type ? FileType::from($this->file_type) : null; + } + + public function isFolder(): bool + { + return $this->type === FileSystemItemType::Folder->value; + } + + public function isFile(): bool + { + return $this->type === FileSystemItemType::File->value; + } + + public function isVideo(): bool + { + return $this->isFile() && $this->file_type === FileType::Video->value; + } + + public function isImage(): bool + { + return $this->isFile() && $this->file_type === FileType::Image->value; + } + + public function isDocument(): bool + { + return $this->isFile() && $this->file_type === FileType::Document->value; + } + + public function isAudio(): bool + { + return $this->isFile() && $this->file_type === FileType::Audio->value; + } + + public function ancestors(): array + { + $ancestors = []; + $current = $this->parent; + + while ($current) { + array_unshift($ancestors, $current); + $current = $current->parent; + } + + return $ancestors; + } + + public function getFullPath(): string + { + if ($this->parent_id === null) { + return '/' . $this->name; + } + + $path = []; + $current = $this; + + while ($current) { + array_unshift($path, $current->name); + $current = $current->parent; + } + + return '/' . implode('/', $path); + } + + public function getDepth(): int + { + return count($this->ancestors()); + } + + public function moveTo($newParent): bool|string + { + /** @var static|null $newParent */ + if ($this->isFolder() && $newParent) { + $parentIds = collect($newParent->ancestors())->pluck('id')->push($newParent->id); + if ($parentIds->contains($this->id)) { + return 'Cannot move a folder into itself or its descendants'; + } + } + + $targetParentId = $newParent?->id; + $existingItem = static::where('parent_id', $targetParentId) + ->where('name', $this->name) + ->where('id', '!=', $this->id) + ->first(); + + if ($existingItem) { + return 'An item with this name already exists in the destination folder'; + } + + $this->parent_id = $targetParentId; + + return $this->save(); + } + + public function getDirectFileCount(): int + { + return $this->children() + ->where('type', '!=', FileSystemItemType::Folder->value) + ->count(); + } + + public function getFileCount(): int + { + $count = $this->getDirectFileCount(); + + foreach ($this->children()->where('type', FileSystemItemType::Folder->value)->get() as $folder) { + $count += $folder->getFileCount(); + } + + return $count; + } + + public function getFormattedSize(): string + { + if (!$this->size) { + return ''; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $size = $this->size; + $unitIndex = 0; + + while ($size >= 1024 && $unitIndex < count($units) - 1) { + $size /= 1024; + $unitIndex++; + } + + return round($size, 1) . ' ' . $units[$unitIndex]; + } + + public function getFormattedDuration(): string + { + if (!$this->duration) { + return ''; + } + + $minutes = floor($this->duration / 60); + $seconds = $this->duration % 60; + + return sprintf('%d:%02d', $minutes, $seconds); + } + + public static function getDirectFileCountForFolder(int|string|null $folderId): int + { + return static::where('parent_id', $folderId) + ->where('type', '!=', FileSystemItemType::Folder->value) + ->count(); + } + + public static function getFileCountForFolder(int|string|null $folderId): int + { + if ($folderId === null) { + return static::where('type', '!=', FileSystemItemType::Folder->value)->count(); + } + + $folder = static::find($folderId); + + return $folder ? $folder->getFileCount() : 0; + } + + public static function getFolderTree(int|string|null $parentId = null): array + { + $folders = static::where('type', FileSystemItemType::Folder->value) + ->where('parent_id', $parentId) + ->orderBy('name') + ->get(); + + return $folders->map(fn ($folder) => [ + 'id' => $folder->id, + 'name' => $folder->name, + 'children' => static::getFolderTree($folder->id), + 'file_count' => $folder->getDirectFileCount(), + ])->toArray(); + } + + public static function getItemsInFolder(int|string|null $parentId = null): \Illuminate\Database\Eloquent\Collection + { + return static::where('parent_id', $parentId) + ->orderByRaw("CASE WHEN type = '" . FileSystemItemType::Folder->value . "' THEN 0 ELSE 1 END") + ->orderBy('name') + ->get(); + } + + public static function determineFileType(string $mimeType): string + { + return FileType::fromMimeType($mimeType)->value; + } +} diff --git a/tests/Fixtures/UuidFileSystemItemFactory.php b/tests/Fixtures/UuidFileSystemItemFactory.php new file mode 100644 index 0000000..048c1e1 --- /dev/null +++ b/tests/Fixtures/UuidFileSystemItemFactory.php @@ -0,0 +1,96 @@ + fake()->unique()->word() . '_' . fake()->randomNumber(5), + 'type' => FileSystemItemType::File->value, + 'file_type' => FileType::Document->value, + 'parent_id' => null, + 'size' => fake()->numberBetween(1024, 10485760), + 'duration' => null, + 'thumbnail' => null, + 'storage_path' => 'uploads/' . fake()->uuid() . '.pdf', + ]; + } + + public function folder(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => FileSystemItemType::Folder->value, + 'file_type' => null, + 'size' => null, + 'storage_path' => null, + ]); + } + + public function file(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => FileSystemItemType::File->value, + ]); + } + + public function video(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => FileSystemItemType::File->value, + 'file_type' => FileType::Video->value, + 'duration' => fake()->numberBetween(60, 3600), + 'storage_path' => 'uploads/' . fake()->uuid() . '.mp4', + ]); + } + + public function image(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => FileSystemItemType::File->value, + 'file_type' => FileType::Image->value, + 'thumbnail' => 'thumbnails/' . fake()->uuid() . '.jpg', + 'storage_path' => 'uploads/' . fake()->uuid() . '.jpg', + ]); + } + + public function audio(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => FileSystemItemType::File->value, + 'file_type' => FileType::Audio->value, + 'duration' => fake()->numberBetween(60, 600), + 'storage_path' => 'uploads/' . fake()->uuid() . '.mp3', + ]); + } + + public function document(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => FileSystemItemType::File->value, + 'file_type' => FileType::Document->value, + 'storage_path' => 'uploads/' . fake()->uuid() . '.pdf', + ]); + } + + public function inFolder(UuidFileSystemItem $folder): static + { + return $this->state(fn (array $attributes) => [ + 'parent_id' => $folder->id, + ]); + } + + public function withParent(int|string|null $parentId): static + { + return $this->state(fn (array $attributes) => [ + 'parent_id' => $parentId, + ]); + } +} diff --git a/tests/Unit/Adapters/DatabaseAdapterUuidTest.php b/tests/Unit/Adapters/DatabaseAdapterUuidTest.php new file mode 100644 index 0000000..bcdc4e1 --- /dev/null +++ b/tests/Unit/Adapters/DatabaseAdapterUuidTest.php @@ -0,0 +1,533 @@ +uuid('id')->primary(); + $table->uuid('parent_id')->nullable(); + $table->string('name'); + $table->string('type'); + $table->string('file_type')->nullable(); + $table->unsignedBigInteger('size')->nullable(); + $table->unsignedInteger('duration')->nullable(); + $table->string('thumbnail')->nullable(); + $table->string('storage_path')->nullable(); + $table->timestamps(); + + $table->index('type'); + $table->index('file_type'); + $table->unique(['parent_id', 'name']); + + $table->foreign('parent_id') + ->references('id') + ->on('uuid_file_system_items') + ->cascadeOnDelete(); + }); + + Storage::fake('testing'); + $this->adapter = new DatabaseAdapter(UuidFileSystemItem::class, 'testing', 'uploads'); +}); + +afterEach(function () { + Schema::dropIfExists('uuid_file_system_items'); +}); + +describe('UUID Support - getItems', function () { + it('returns items from root folder with UUID ids', function () { + $folder1 = UuidFileSystemItem::factory()->folder()->create(['name' => 'folder1']); + $folder2 = UuidFileSystemItem::factory()->folder()->create(['name' => 'folder2']); + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf']); + + // Verify UUIDs are being used + expect($folder1->id)->toBeString() + ->and(strlen($folder1->id))->toBe(36); + + $items = $this->adapter->getItems(); + + expect($items)->toHaveCount(3); + }); + + it('returns items from specific folder using UUID', function () { + $folder = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + UuidFileSystemItem::factory()->file()->create(['name' => 'child.pdf', 'parent_id' => $folder->id]); + UuidFileSystemItem::factory()->file()->create(['name' => 'root.pdf']); + + // Using UUID string + $items = $this->adapter->getItems($folder->id); + + expect($items)->toHaveCount(1) + ->and($items->first()->getName())->toBe('child.pdf'); + }); + + it('returns empty collection for empty folder with UUID', function () { + $folder = UuidFileSystemItem::factory()->folder()->create(['name' => 'empty']); + + $items = $this->adapter->getItems($folder->id); + + expect($items)->toHaveCount(0); + }); +}); + +describe('UUID Support - getFolders', function () { + it('returns only folders from root with UUID ids', function () { + UuidFileSystemItem::factory()->folder()->create(['name' => 'folder1']); + UuidFileSystemItem::factory()->folder()->create(['name' => 'folder2']); + UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf']); + + $folders = $this->adapter->getFolders(); + + expect($folders)->toHaveCount(2) + ->and($folders->every(fn ($item) => $item->isFolder()))->toBeTrue(); + }); + + it('returns folders from specific parent using UUID', function () { + $parent = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + UuidFileSystemItem::factory()->folder()->create(['name' => 'child1', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->folder()->create(['name' => 'child2', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->folder()->create(['name' => 'other']); + + $folders = $this->adapter->getFolders($parent->id); + + expect($folders)->toHaveCount(2); + }); +}); + +describe('UUID Support - getItem', function () { + it('returns item by UUID', function () { + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'document.pdf']); + + // Verify UUID format + expect($file->id)->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i'); + + $item = $this->adapter->getItem($file->id); + + expect($item)->not->toBeNull() + ->and($item)->toBeInstanceOf(DatabaseItem::class) + ->and($item->getName())->toBe('document.pdf'); + }); + + it('returns null for nonexistent UUID', function () { + $item = $this->adapter->getItem('00000000-0000-0000-0000-000000000000'); + + expect($item)->toBeNull(); + }); + + it('returns null for invalid UUID format', function () { + $item = $this->adapter->getItem('invalid-uuid'); + + expect($item)->toBeNull(); + }); +}); + +describe('UUID Support - createFolder', function () { + it('creates folder in root with UUID', function () { + $result = $this->adapter->createFolder('new-folder'); + + expect($result)->toBeInstanceOf(DatabaseItem::class) + ->and($result->getName())->toBe('new-folder') + ->and($result->isFolder())->toBeTrue() + ->and($result->getIdentifier())->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i'); + + $this->assertDatabaseHas('uuid_file_system_items', [ + 'name' => 'new-folder', + 'type' => 'folder', + 'parent_id' => null, + ]); + }); + + it('creates folder in parent folder using UUID', function () { + $parent = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + + $result = $this->adapter->createFolder('child', $parent->id); + + expect($result)->toBeInstanceOf(DatabaseItem::class) + ->and($result->getName())->toBe('child'); + + $this->assertDatabaseHas('uuid_file_system_items', [ + 'name' => 'child', + 'type' => 'folder', + 'parent_id' => $parent->id, + ]); + }); + + it('returns error for duplicate folder name with UUID', function () { + UuidFileSystemItem::factory()->folder()->create(['name' => 'existing']); + + $result = $this->adapter->createFolder('existing'); + + expect($result)->toBe('A folder with this name already exists'); + }); +}); + +describe('UUID Support - uploadFile', function () { + it('uploads file to root with UUID', function () { + $file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf'); + + $result = $this->adapter->uploadFile($file); + + expect($result)->toBeInstanceOf(DatabaseItem::class) + ->and($result->getName())->toBe('document.pdf') + ->and($result->isFile())->toBeTrue() + ->and($result->getIdentifier())->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i'); + + $this->assertDatabaseHas('uuid_file_system_items', [ + 'name' => 'document.pdf', + 'type' => 'file', + 'parent_id' => null, + ]); + }); + + it('uploads file to specific folder using UUID', function () { + $folder = UuidFileSystemItem::factory()->folder()->create(['name' => 'uploads']); + $file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf'); + + $result = $this->adapter->uploadFile($file, $folder->id); + + expect($result)->toBeInstanceOf(DatabaseItem::class); + + $this->assertDatabaseHas('uuid_file_system_items', [ + 'name' => 'document.pdf', + 'parent_id' => $folder->id, + ]); + }); + + it('renames file on duplicate with UUID', function () { + UuidFileSystemItem::factory()->file()->create(['name' => 'existing.pdf']); + $file = UploadedFile::fake()->create('existing.pdf', 100, 'application/pdf'); + + $result = $this->adapter->uploadFile($file); + + expect($result)->toBeInstanceOf(DatabaseItem::class) + ->and($result->getName())->not->toBe('existing.pdf') + ->and($result->getName())->toContain('existing_'); + }); +}); + +describe('UUID Support - rename', function () { + it('renames item successfully using UUID', function () { + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'old-name.pdf']); + + $result = $this->adapter->rename($file->id, 'new-name.pdf'); + + expect($result)->toBeTrue(); + + $this->assertDatabaseHas('uuid_file_system_items', [ + 'id' => $file->id, + 'name' => 'new-name.pdf', + ]); + }); + + it('returns error for nonexistent UUID', function () { + $result = $this->adapter->rename('00000000-0000-0000-0000-000000000000', 'new-name.pdf'); + + expect($result)->toBe('Item not found'); + }); + + it('returns error for duplicate name with UUID', function () { + UuidFileSystemItem::factory()->file()->create(['name' => 'existing.pdf']); + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'other.pdf']); + + $result = $this->adapter->rename($file->id, 'existing.pdf'); + + expect($result)->toBe('An item with this name already exists in this folder'); + }); +}); + +describe('UUID Support - move', function () { + it('moves item to another folder using UUID', function () { + $target = UuidFileSystemItem::factory()->folder()->create(['name' => 'target']); + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf']); + + $result = $this->adapter->move($file->id, $target->id); + + expect($result)->toBeTrue(); + + $this->assertDatabaseHas('uuid_file_system_items', [ + 'id' => $file->id, + 'parent_id' => $target->id, + ]); + }); + + it('moves item to root using UUID', function () { + $folder = UuidFileSystemItem::factory()->folder()->create(['name' => 'folder']); + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf', 'parent_id' => $folder->id]); + + $result = $this->adapter->move($file->id, null); + + expect($result)->toBeTrue(); + + $this->assertDatabaseHas('uuid_file_system_items', [ + 'id' => $file->id, + 'parent_id' => null, + ]); + }); + + it('returns error for nonexistent UUID', function () { + $result = $this->adapter->move('00000000-0000-0000-0000-000000000000', null); + + expect($result)->toBe('Item not found'); + }); + + it('returns error when moving to same location with UUID', function () { + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf']); + + $result = $this->adapter->move($file->id, null); + + expect($result)->toBe('Item is already in this folder'); + }); + + it('returns error for duplicate name in target with UUID', function () { + $target = UuidFileSystemItem::factory()->folder()->create(['name' => 'target']); + UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf', 'parent_id' => $target->id]); + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf']); + + $result = $this->adapter->move($file->id, $target->id); + + expect($result)->toBe('An item with this name already exists in the destination folder'); + }); + + it('prevents moving folder into itself with UUID', function () { + $folder = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + $child = UuidFileSystemItem::factory()->folder()->create(['name' => 'child', 'parent_id' => $folder->id]); + + $result = $this->adapter->move($folder->id, $child->id); + + expect($result)->toBe('Cannot move a folder into itself or its descendants'); + }); +}); + +describe('UUID Support - delete', function () { + it('deletes file successfully using UUID', function () { + Storage::disk('testing')->put('uploads/file.pdf', 'content'); + $file = UuidFileSystemItem::factory()->file()->create([ + 'name' => 'file.pdf', + 'storage_path' => 'uploads/file.pdf', + ]); + + $result = $this->adapter->delete($file->id); + + expect($result)->toBeTrue(); + + $this->assertDatabaseMissing('uuid_file_system_items', ['id' => $file->id]); + }); + + it('deletes folder successfully using UUID', function () { + $folder = UuidFileSystemItem::factory()->folder()->create(['name' => 'folder']); + + $result = $this->adapter->delete($folder->id); + + expect($result)->toBeTrue(); + + $this->assertDatabaseMissing('uuid_file_system_items', ['id' => $folder->id]); + }); + + it('returns error for nonexistent UUID', function () { + $result = $this->adapter->delete('00000000-0000-0000-0000-000000000000'); + + expect($result)->toBe('Item not found'); + }); +}); + +describe('UUID Support - deleteMany', function () { + it('deletes multiple items using UUIDs', function () { + $file1 = UuidFileSystemItem::factory()->file()->create(['name' => 'file1.pdf']); + $file2 = UuidFileSystemItem::factory()->file()->create(['name' => 'file2.pdf']); + $file3 = UuidFileSystemItem::factory()->file()->create(['name' => 'file3.pdf']); + + $count = $this->adapter->deleteMany([$file1->id, $file2->id]); + + expect($count)->toBe(2); + + $this->assertDatabaseMissing('uuid_file_system_items', ['id' => $file1->id]); + $this->assertDatabaseMissing('uuid_file_system_items', ['id' => $file2->id]); + $this->assertDatabaseHas('uuid_file_system_items', ['id' => $file3->id]); + }); +}); + +describe('UUID Support - exists', function () { + it('returns true for existing UUID', function () { + $file = UuidFileSystemItem::factory()->file()->create(['name' => 'file.pdf']); + + expect($this->adapter->exists($file->id))->toBeTrue(); + }); + + it('returns false for nonexistent UUID', function () { + expect($this->adapter->exists('00000000-0000-0000-0000-000000000000'))->toBeFalse(); + }); +}); + +describe('UUID Support - getUrl', function () { + it('returns url for file with storage path using UUID', function () { + Storage::disk('testing')->put('uploads/file.pdf', 'content'); + $file = UuidFileSystemItem::factory()->file()->create([ + 'name' => 'file.pdf', + 'storage_path' => 'uploads/file.pdf', + ]); + + $url = $this->adapter->getUrl($file->id); + + expect($url)->toBeString() + ->and($url)->toContain('file.pdf'); + }); + + it('returns null for nonexistent UUID', function () { + $url = $this->adapter->getUrl('00000000-0000-0000-0000-000000000000'); + + expect($url)->toBeNull(); + }); + + it('returns null for folder using UUID', function () { + $folder = UuidFileSystemItem::factory()->folder()->create(['name' => 'folder']); + + $url = $this->adapter->getUrl($folder->id); + + expect($url)->toBeNull(); + }); +}); + +describe('UUID Support - getContents', function () { + it('returns file contents using UUID', function () { + Storage::disk('testing')->put('uploads/file.txt', 'Hello World'); + $file = UuidFileSystemItem::factory()->file()->create([ + 'name' => 'file.txt', + 'storage_path' => 'uploads/file.txt', + ]); + + $contents = $this->adapter->getContents($file->id); + + expect($contents)->toBe('Hello World'); + }); + + it('returns null for nonexistent UUID', function () { + $contents = $this->adapter->getContents('00000000-0000-0000-0000-000000000000'); + + expect($contents)->toBeNull(); + }); +}); + +describe('UUID Support - getStream', function () { + it('returns stream for file using UUID', function () { + Storage::disk('testing')->put('uploads/file.txt', 'content'); + $file = UuidFileSystemItem::factory()->file()->create([ + 'name' => 'file.txt', + 'storage_path' => 'uploads/file.txt', + ]); + + $stream = $this->adapter->getStream($file->id); + + expect($stream)->toBeResource(); + fclose($stream); + }); + + it('returns null for nonexistent UUID', function () { + $stream = $this->adapter->getStream('00000000-0000-0000-0000-000000000000'); + + expect($stream)->toBeNull(); + }); +}); + +describe('UUID Support - getSize', function () { + it('returns stored size using UUID', function () { + $file = UuidFileSystemItem::factory()->file()->create([ + 'name' => 'file.pdf', + 'size' => 12345, + ]); + + $size = $this->adapter->getSize($file->id); + + expect($size)->toBe(12345); + }); + + it('returns null for nonexistent UUID', function () { + $size = $this->adapter->getSize('00000000-0000-0000-0000-000000000000'); + + expect($size)->toBeNull(); + }); +}); + +describe('UUID Support - breadcrumbs', function () { + it('returns root breadcrumb for root path', function () { + $breadcrumbs = $this->adapter->getBreadcrumbs(null); + + expect($breadcrumbs)->toHaveCount(1) + ->and($breadcrumbs[0]['name'])->toBe('Root'); + }); + + it('returns breadcrumbs for nested folder using UUIDs', function () { + $parent = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + $child = UuidFileSystemItem::factory()->folder()->create(['name' => 'child', 'parent_id' => $parent->id]); + + $breadcrumbs = $this->adapter->getBreadcrumbs($child->id); + + expect($breadcrumbs)->toHaveCount(3) + ->and($breadcrumbs[0]['name'])->toBe('Root') + ->and($breadcrumbs[1]['name'])->toBe('parent') + ->and($breadcrumbs[2]['name'])->toBe('child'); + }); +}); + +describe('UUID Support - folder tree', function () { + it('returns folder tree structure with UUIDs', function () { + $folder1 = UuidFileSystemItem::factory()->folder()->create(['name' => 'folder1']); + $folder2 = UuidFileSystemItem::factory()->folder()->create(['name' => 'folder2']); + UuidFileSystemItem::factory()->folder()->create(['name' => 'child', 'parent_id' => $folder1->id]); + + $tree = $this->adapter->getFolderTree(); + + expect($tree)->toHaveCount(2); + }); +}); + +describe('UUID Support - model methods', function () { + it('getFolderTree works with UUID parent_id', function () { + $parent = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + UuidFileSystemItem::factory()->folder()->create(['name' => 'child1', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->folder()->create(['name' => 'child2', 'parent_id' => $parent->id]); + + $tree = UuidFileSystemItem::getFolderTree($parent->id); + + expect($tree)->toHaveCount(2) + ->and($tree[0]['name'])->toBe('child1') + ->and($tree[1]['name'])->toBe('child2'); + }); + + it('getItemsInFolder works with UUID parent_id', function () { + $parent = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + UuidFileSystemItem::factory()->file()->create(['name' => 'file1.pdf', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->file()->create(['name' => 'file2.pdf', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->file()->create(['name' => 'other.pdf']); + + $items = UuidFileSystemItem::getItemsInFolder($parent->id); + + expect($items)->toHaveCount(2); + }); + + it('getDirectFileCountForFolder works with UUID folder_id', function () { + $parent = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + UuidFileSystemItem::factory()->file()->create(['name' => 'file1.pdf', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->file()->create(['name' => 'file2.pdf', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->folder()->create(['name' => 'subfolder', 'parent_id' => $parent->id]); + + $count = UuidFileSystemItem::getDirectFileCountForFolder($parent->id); + + expect($count)->toBe(2); + }); + + it('getFileCountForFolder works with UUID folder_id', function () { + $parent = UuidFileSystemItem::factory()->folder()->create(['name' => 'parent']); + $child = UuidFileSystemItem::factory()->folder()->create(['name' => 'child', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->file()->create(['name' => 'file1.pdf', 'parent_id' => $parent->id]); + UuidFileSystemItem::factory()->file()->create(['name' => 'file2.pdf', 'parent_id' => $child->id]); + + $count = UuidFileSystemItem::getFileCountForFolder($parent->id); + + expect($count)->toBe(2); + }); +});