From 28e74945598a6b9585f0ca0e7030b18dce929515 Mon Sep 17 00:00:00 2001 From: Josh Manders Date: Mon, 13 Oct 2025 00:20:09 -0500 Subject: [PATCH] fix: support prefix --- src/CloudinaryServiceProvider.php | 2 +- src/CloudinaryStorageAdapter.php | 38 ++++++++++++++++----- tests/Unit/CloudinaryStorageAdapterTest.php | 19 +++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/CloudinaryServiceProvider.php b/src/CloudinaryServiceProvider.php index f128abb..5f261d0 100644 --- a/src/CloudinaryServiceProvider.php +++ b/src/CloudinaryServiceProvider.php @@ -36,7 +36,7 @@ public function boot(): void ]); } - $adapter = new CloudinaryStorageAdapter($cloudinary); + $adapter = new CloudinaryStorageAdapter($cloudinary, null, $config['prefix'] ?? null); return new FilesystemAdapter(new Filesystem($adapter, $config), $adapter, $config); }); diff --git a/src/CloudinaryStorageAdapter.php b/src/CloudinaryStorageAdapter.php index a0184f2..3477d5f 100644 --- a/src/CloudinaryStorageAdapter.php +++ b/src/CloudinaryStorageAdapter.php @@ -14,11 +14,13 @@ class CloudinaryStorageAdapter implements ChecksumProvider, FilesystemAdapter { - private MimeTypeDetector $mimeTypeDetector; - - public function __construct(private Cloudinary $cloudinary, ?MimeTypeDetector $mimeTypeDetector = null) + public function __construct( + private Cloudinary $cloudinary, + private ?MimeTypeDetector $mimeTypeDetector = null, + private ?string $prefix = null) { $this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector; + $this->prefix = $prefix ? str_replace('\\', '/', trim($prefix, '/')) : ''; } public function getUrl(string $path): string @@ -38,7 +40,7 @@ public function copy(string $source, string $destination, Config $config): void public function createDirectory(string $path, Config $config): void { - $this->cloudinary->adminApi()->createFolder($path); + $this->cloudinary->adminApi()->createFolder($this->applyPrefixToPath($path)); } public function delete(string $path): void @@ -58,7 +60,7 @@ public function delete(string $path): void public function deleteDirectory(string $path): void { - $this->cloudinary->adminApi()->deleteAssetsByPrefix($path); + $this->cloudinary->adminApi()->deleteAssetsByPrefix($this->applyPrefixToPath($path)); } public function directoryExists(string $path): bool @@ -105,7 +107,7 @@ public function listContents(string $path, bool $deep): array|\Traversable do { $response = $this->cloudinary->adminApi()->assets([ 'type' => 'upload', - 'prefix' => $path, + 'prefix' => $this->applyPrefixToPath($path), 'max_results' => 500, 'next_cursor' => isset($response) ? $response->offsetGet('next_cursor') : null, ]); @@ -194,12 +196,12 @@ public function checksum(string $path, Config $config): string public function prepareResource(string $path): array { $info = pathinfo($path); - + // Ensure dirname uses forward slashes, regardless of OS $dirname = str_replace('\\', '/', $info['dirname']); // Always use forward slash for path construction $id = $dirname.'/'.$info['filename']; - + $mimeType = $this->mimeTypeDetector->detectMimeTypeFromPath($path); if (strpos($mimeType, 'image/') === 0) { @@ -210,6 +212,26 @@ public function prepareResource(string $path): array return [$id, 'video']; } + // If a prefix is configured, apply it to the id. When applying a prefix + // strip any leading './' or '/' from the generated id so we don't end up + // with paths like "prefix/./file". + if ($this->prefix !== '') { + $normalizedId = ltrim($id, './\\/'); + $id = $this->prefix. + ($normalizedId !== '' ? '/'.$normalizedId : ''); + } + return [$id, 'raw']; } + + private function applyPrefixToPath(string $path): string + { + if ($this->prefix === '') { + return $path; + } + + $trimmed = ltrim(str_replace('\\', '/', $path), '\/'); + + return $this->prefix.($trimmed !== '' ? '/'.$trimmed : ''); + } } diff --git a/tests/Unit/CloudinaryStorageAdapterTest.php b/tests/Unit/CloudinaryStorageAdapterTest.php index 8842d78..c76bb39 100644 --- a/tests/Unit/CloudinaryStorageAdapterTest.php +++ b/tests/Unit/CloudinaryStorageAdapterTest.php @@ -26,6 +26,10 @@ function createApiResponse(array $data, int $statusCode = 200): ApiResponse $this->cloudinary->adminApi()->willReturn($this->adminApi->reveal()); $this->adapter = new CloudinaryStorageAdapter($this->cloudinary->reveal()); + + // Also create an adapter instance configured with a prefix for tests + // that assert prefix behavior. + $this->prefixedAdapter = new CloudinaryStorageAdapter($this->cloudinary->reveal(), null, 'Fixtures'); }); it('can copy a file', function () { @@ -138,6 +142,21 @@ function createApiResponse(array $data, int $statusCode = 200): ApiResponse expect(iterator_to_array($contents))->toHaveCount(1); }); +it('applies configured prefix when listing contents', function () { + $response = createApiResponse([ + 'resources' => [], + ]); + + $this->adminApi->assets(Argument::that(function ($options) { + return $options['type'] === 'upload' + && $options['prefix'] === 'Fixtures/test-dir' + && $options['max_results'] === 500; + }))->willReturn($response)->shouldBeCalled(); + + $contents = $this->prefixedAdapter->listContents('test-dir', false); + expect(iterator_to_array($contents))->toHaveCount(0); +}); + it('can read a file', function () { $this->adminApi->asset( Argument::exact('Fixtures/test-file'),