diff --git a/README.md b/README.md index cd5f6a1..5164143 100644 --- a/README.md +++ b/README.md @@ -21,22 +21,42 @@ composer require azure-oss/storage-blob-flysystem ``` -## Quickstart +## Usage ```php use AzureOss\FlysystemAzureBlobStorage\AzureBlobStorageAdapter; use AzureOss\Storage\Blob\BlobServiceClient; use League\Flysystem\Filesystem; -$blobServiceClient = BlobServiceClient::fromConnectionString(''); -$containerClient = $blobServiceClient->getContainerClient('quickstart'); +// Create a BlobContainerClient +$containerClient = BlobServiceClient::fromConnectionString($connectionString) + ->getContainerClient('your-container-name'); -$adapter = new AzureBlobStorageAdapter($containerClient, "optional/prefix"); +// Create the adapter +$adapter = new AzureBlobStorageAdapter( + $containerClient, + 'optional-prefix', + useDirectPublicUrl: false, // Set to true to use direct public URLs instead of SAS tokens +); + +// Create the filesystem $filesystem = new Filesystem($adapter); +``` + +### Public URLs -$filesystem->write('hello', 'world!'); +By default, the adapter generates public URLs using SAS tokens with a 1000-year expiration. If you prefer to use direct public URLs without SAS tokens, you can set the `useDirectPublicUrl` parameter to `true`: + +```php +$adapter = new AzureBlobStorageAdapter( + $containerClient, + 'optional-prefix', + useDirectPublicUrl: true, +); ``` +Note that for direct public URLs to work, your container must be configured with public access. If your container is private, you should use the default SAS token approach. + ## Documentation For more information visit the documentation at [azure-oss.github.io](https://azure-oss.github.io/storage/flysystem/). diff --git a/src/AzureBlobStorageAdapter.php b/src/AzureBlobStorageAdapter.php index d877623..42f1348 100644 --- a/src/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorageAdapter.php @@ -46,6 +46,7 @@ public function __construct( string $prefix = "", ?MimeTypeDetector $mimeTypeDetector = null, private readonly string $visibilityHandling = self::ON_VISIBILITY_THROW_ERROR, + private readonly bool $useDirectPublicUrl = false, ) { $this->prefixer = new PathPrefixer($prefix); $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); @@ -230,17 +231,20 @@ public function listContents(string $path, bool $deep): iterable { try { $prefix = $this->prefixer->prefixDirectoryPath($path); + $directories = [$prefix]; - if ($deep) { - foreach ($this->containerClient->getBlobs($prefix) as $item) { - yield $this->normalizeBlob($this->prefixer->stripPrefix($item->name), $item->properties); - } - } else { - foreach ($this->containerClient->getBlobsByHierarchy($prefix) as $item) { + while (!empty($directories)) { + $currentPrefix = array_shift($directories); + + foreach ($this->containerClient->getBlobsByHierarchy($currentPrefix) as $item) { if ($item instanceof Blob) { yield $this->normalizeBlob($this->prefixer->stripPrefix($item->name), $item->properties); } else { yield new DirectoryAttributes($this->prefixer->stripPrefix($item->name)); + + if ($deep) { + $directories[] = $item->name; + } } } } @@ -284,10 +288,16 @@ public function copy(string $source, string $destination, Config $config): void } /** - * @description Azure doesn't support permanent URLs. Instead, we create one that lasts 1000 years. + * @description If useDirectPublicUrl is true, returns the direct public URL. + * Otherwise, Azure doesn't support permanent URLs, so we create one that lasts 1000 years. */ public function publicUrl(string $path, Config $config): string { + if ($this->useDirectPublicUrl) { + $blobClient = $this->containerClient->getBlobClient($this->prefixer->prefixPath($path)); + return (string) $blobClient->uri; + } + return $this->temporaryUrl($path, (new \DateTimeImmutable())->modify("+1000 years"), $config); } diff --git a/tests/AzureBlobStorageTest.php b/tests/AzureBlobStorageTest.php index 6797fa8..4cd808f 100644 --- a/tests/AzureBlobStorageTest.php +++ b/tests/AzureBlobStorageTest.php @@ -190,4 +190,69 @@ public function setting_visibility_causes_errors(): void $adapter->setVisibility('some-file.md', 'public'); } + + #[Test] + public function listing_contents_deep(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + + $adapter->write('dir1/file1.txt', 'content1', new Config()); + $adapter->write('dir1/dir2/file2.txt', 'content2', new Config()); + $adapter->write('dir1/dir2/dir3/file3.txt', 'content3', new Config()); + /** @phpstan-ignore-next-line */ + $contents = iterator_to_array($adapter->listContents('', true)); + + $this->assertCount(6, $contents); // 3 files + 3 directories + + $paths = array_map(fn($item) => $item->path(), $contents); + $this->assertContains('dir1', $paths); + $this->assertContains('dir1/file1.txt', $paths); + $this->assertContains('dir1/dir2', $paths); + $this->assertContains('dir1/dir2/file2.txt', $paths); + $this->assertContains('dir1/dir2/dir3', $paths); + $this->assertContains('dir1/dir2/dir3/file3.txt', $paths); + }); + } + + #[Test] + public function public_url_uses_direct_uri_when_enabled(): void + { + $this->givenWeHaveAnExistingFile('test-file.txt'); + + $adapter = new AzureBlobStorageAdapter( + self::createContainerClient(), + 'flysystem', + useDirectPublicUrl: true, + ); + + $url = $adapter->publicUrl('test-file.txt', new Config()); + + // Direct URL should not contain SAS token parameters + $this->assertStringNotContainsString('sig=', $url); + $this->assertStringNotContainsString('se=', $url); + $this->assertStringNotContainsString('sp=', $url); + + // But should contain the container and blob name + $this->assertStringContainsString('flysystem', $url); + $this->assertStringContainsString('test-file.txt', $url); + } + + #[Test] + public function public_url_uses_sas_token_by_default(): void + { + $this->givenWeHaveAnExistingFile('test-file.txt'); + + $adapter = new AzureBlobStorageAdapter( + self::createContainerClient(), + 'flysystem', + ); + + $url = $adapter->publicUrl('test-file.txt', new Config()); + + // URL with SAS token should contain these parameters + $this->assertStringContainsString('sig=', $url); + $this->assertStringContainsString('se=', $url); + $this->assertStringContainsString('sp=', $url); + } }