Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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('<connection-string>');
$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/).
Expand Down
24 changes: 17 additions & 7 deletions src/AzureBlobStorageAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down Expand Up @@ -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);
}

Expand Down
65 changes: 65 additions & 0 deletions tests/AzureBlobStorageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading