Skip to content

Commit 70e629d

Browse files
author
brecht.vermeersch
committed
initial commit
0 parents  commit 70e629d

File tree

8 files changed

+556
-0
lines changed

8 files changed

+556
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/vendor
2+
/.build
3+
/composer.lock
4+
/.idea
5+
/.phpunit.cache
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AzureOss\FlysystemAzureBlobStorage;
6+
7+
use AzureOss\Storage\Blob\ContainerClient;
8+
use AzureOss\Storage\Blob\Options\ListBlobsOptions;
9+
use AzureOss\Storage\Blob\Options\UploadBlockBlobOptions;
10+
use DateTimeInterface;
11+
use League\Flysystem\CalculateChecksumFromStream;
12+
use League\Flysystem\ChecksumProvider;
13+
use League\Flysystem\Config;
14+
use League\Flysystem\DirectoryAttributes;
15+
use League\Flysystem\FileAttributes;
16+
use League\Flysystem\FilesystemAdapter;
17+
use League\Flysystem\UnableToCheckExistence;
18+
use League\Flysystem\UnableToCopyFile;
19+
use League\Flysystem\UnableToDeleteDirectory;
20+
use League\Flysystem\UnableToDeleteFile;
21+
use League\Flysystem\UnableToListContents;
22+
use League\Flysystem\UnableToMoveFile;
23+
use League\Flysystem\UnableToReadFile;
24+
use League\Flysystem\UnableToRetrieveMetadata;
25+
use League\Flysystem\UnableToSetVisibility;
26+
use League\Flysystem\UnableToWriteFile;
27+
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
28+
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
29+
use League\MimeTypeDetection\FinfoMimeTypeDetector;
30+
use League\MimeTypeDetection\MimeTypeDetector;
31+
32+
class AzureBlobStorageAdapter implements FilesystemAdapter, ChecksumProvider, PublicUrlGenerator, TemporaryUrlGenerator
33+
{
34+
use CalculateChecksumFromStream;
35+
36+
private readonly MimeTypeDetector $mimeTypeDetector;
37+
38+
public function __construct(
39+
private readonly ContainerClient $containerClient,
40+
?MimeTypeDetector $mimeTypeDetector = null,
41+
) {
42+
$this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();
43+
}
44+
45+
public function fileExists(string $path): bool
46+
{
47+
try {
48+
return $this->containerClient->getBlobClient($path)->exists();
49+
} catch(\Throwable $e) {
50+
throw UnableToCheckExistence::forLocation($path, $e);
51+
}
52+
}
53+
54+
public function directoryExists(string $path): bool
55+
{
56+
try {
57+
$options = new ListBlobsOptions(
58+
prefix: $this->getPrefix($path),
59+
maxResults: 1,
60+
delimiter: "/"
61+
);
62+
63+
$response = $this->containerClient->listBlobs($options);
64+
65+
return count($response->blobs) > 0;
66+
} catch (\Throwable $e) {
67+
throw UnableToCheckExistence::forLocation($path, $e);
68+
}
69+
}
70+
71+
public function write(string $path, string $contents, Config $config): void
72+
{
73+
$this->upload($path, $contents);
74+
}
75+
76+
public function writeStream(string $path, $contents, Config $config): void
77+
{
78+
$this->upload($path, $contents);
79+
}
80+
81+
/**
82+
* @param string|resource $contents
83+
*/
84+
private function upload(string $path, $contents): void
85+
{
86+
try {
87+
$mimetype = $this->mimeTypeDetector->detectMimetype($path, $contents);
88+
89+
$options = new UploadBlockBlobOptions(
90+
contentType: $mimetype,
91+
);
92+
93+
$this->containerClient->getBlockBlobClient($path)->upload($contents, $options);
94+
} catch (\Throwable $e) {
95+
throw UnableToWriteFile::atLocation($path, previous: $e);
96+
}
97+
}
98+
99+
public function read(string $path): string
100+
{
101+
try {
102+
$response = $this->containerClient->getBlobClient($path)->get();
103+
104+
return $response->content->getContents();
105+
} catch (\Throwable $e) {
106+
throw UnableToReadFile::fromLocation($path, previous: $e);
107+
}
108+
}
109+
110+
public function readStream(string $path)
111+
{
112+
try {
113+
$response = $this->containerClient->getBlobClient($path)->get();
114+
$resource = $response->content->detach();
115+
116+
if($resource === null) {
117+
throw new \Exception("Should not happen");
118+
}
119+
120+
return $resource;
121+
} catch (\Throwable $e) {
122+
throw UnableToReadFile::fromLocation($path, previous: $e);
123+
}
124+
}
125+
126+
public function delete(string $path): void
127+
{
128+
try {
129+
$this->containerClient->getBlobClient($path)->deleteIfExists();
130+
} catch (\Throwable $e) {
131+
throw UnableToDeleteFile::atLocation($path, previous: $e);
132+
}
133+
}
134+
135+
public function deleteDirectory(string $path): void
136+
{
137+
try {
138+
foreach ($this->listContents($path, true) as $item) {
139+
if ($item instanceof FileAttributes) {
140+
$this->containerClient->getBlobClient($item->path())->delete();
141+
}
142+
}
143+
} catch (\Throwable $e) {
144+
throw UnableToDeleteDirectory::atLocation($path, previous: $e);
145+
}
146+
}
147+
148+
public function createDirectory(string $path, Config $config): void
149+
{
150+
// Azure does not support this operation.
151+
}
152+
153+
public function setVisibility(string $path, string $visibility): void
154+
{
155+
throw UnableToSetVisibility::atLocation($path, "Azure does not support this operation.");
156+
}
157+
158+
public function visibility(string $path): FileAttributes
159+
{
160+
throw UnableToRetrieveMetadata::visibility($path, "Azure does not support this operation.");
161+
}
162+
163+
public function mimeType(string $path): FileAttributes
164+
{
165+
try {
166+
return $this->fetchMetadata($path);
167+
} catch (\Throwable $e) {
168+
throw UnableToRetrieveMetadata::mimeType($path, previous: $e);
169+
}
170+
}
171+
172+
public function lastModified(string $path): FileAttributes
173+
{
174+
try {
175+
return $this->fetchMetadata($path);
176+
} catch (\Throwable $e) {
177+
throw UnableToRetrieveMetadata::lastModified($path, previous: $e);
178+
}
179+
}
180+
181+
public function fileSize(string $path): FileAttributes
182+
{
183+
try {
184+
return $this->fetchMetadata($path);
185+
} catch (\Throwable $e) {
186+
throw UnableToRetrieveMetadata::lastModified($path, previous: $e);
187+
}
188+
}
189+
190+
private function fetchMetadata(string $path): FileAttributes
191+
{
192+
$response = $this->containerClient->getBlobClient($path)->getProperties();
193+
194+
return new FileAttributes(
195+
$path,
196+
fileSize: $response->contentLength,
197+
lastModified: $response->lastModified->getTimestamp(),
198+
mimeType: $response->contentType
199+
);
200+
}
201+
202+
public function listContents(string $path, bool $deep): iterable
203+
{
204+
try {
205+
do {
206+
$nextMarker = "";
207+
208+
$options = new ListBlobsOptions(
209+
prefix: $this->getPrefix($path),
210+
marker: $nextMarker,
211+
delimiter: $deep ? null : "/",
212+
);
213+
214+
$response = $this->containerClient->listBlobs($options);
215+
216+
foreach($response->blobPrefixes as $blobPrefix) {
217+
yield new DirectoryAttributes($blobPrefix->name);
218+
}
219+
220+
foreach ($response->blobs as $blob) {
221+
yield new FileAttributes(
222+
$blob->name,
223+
fileSize: $blob->properties->contentLength,
224+
lastModified: $blob->properties->lastModified->getTimestamp(),
225+
mimeType: $blob->properties->contentType,
226+
);
227+
}
228+
229+
$nextMarker = $response->nextMarker;
230+
} while ($nextMarker !== "");
231+
} catch (\Throwable $e) {
232+
throw UnableToListContents::atLocation($path, $deep, new \Exception());
233+
}
234+
}
235+
236+
public function move(string $source, string $destination, Config $config): void
237+
{
238+
try {
239+
$this->copy($source, $destination, $config);
240+
$this->delete($source);
241+
} catch (\Throwable $e) {
242+
throw UnableToMoveFile::fromLocationTo($source, $destination, $e);
243+
}
244+
}
245+
246+
public function copy(string $source, string $destination, Config $config): void
247+
{
248+
try {
249+
$this->containerClient->getBlobClient($source)->copy($this->containerClient->containerName, $destination);
250+
} catch (\Throwable $e) {
251+
throw UnableToCopyFile::fromLocationTo($source, $destination, $e);
252+
}
253+
}
254+
255+
private function getPrefix(string $path): ?string
256+
{
257+
return $path === "" ? null : rtrim($path, "/") . "/";
258+
}
259+
260+
public function checksum(string $path, Config $config): string
261+
{
262+
return $this->calculateChecksumFromStream($path, $config);
263+
}
264+
265+
public function publicUrl(string $path, Config $config): string
266+
{
267+
return "TODO";
268+
}
269+
270+
public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
271+
{
272+
return "TODO";
273+
}
274+
}

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Flysystem adapter for
2+
3+
[![Latest Version on Packagist](https://img.shields.io/packagist/v/azure-oss/flysystem-azure-blob-storage?style=flat-square)](https://packagist.org/packages/azure-oss/flysystem-azure-blob-storage)
4+
[![Total Downloads](https://img.shields.io/packagist/dt/azure-oss/flysystem-azure-blob-storage?style=flat-square)](https://packagist.org/packages/azure-oss/flysystem-azure-blob-storage)
5+
6+
## Installation
7+
8+
```bash
9+
composer require azure-oss/flysystem-azure-blob-storage
10+
```
11+
12+
## Notice
13+
14+
It’s important to know this adapter does not fully comply with the adapter contract. The difference(s) is/are:
15+
16+
* Visibility setting or retrieving is not supported.
17+
* Mimetypes are always resolved, where others do not.
18+
* Directory creation is not supported in any way.
19+
20+
## Usage
21+
22+
The connection string can be obtained from the azure portal.
23+
24+
```php
25+
<?php
26+
27+
use AzureOss\FlysystemAzureBlobStorage\AzureBlobStorageAdapter;
28+
use AzureOss\Storage\Blob\BlobServiceClient;
29+
use League\Flysystem\Filesystem;
30+
31+
include __DIR__.'/vendor/autoload.php';
32+
33+
$client = BlobServiceClient::fromConnectionString('connectionString')->getContainerClient('container-name')
34+
$adapter = new AzureBlobStorageAdapter(
35+
$client,
36+
'container-name',
37+
);
38+
$filesystem = new Filesystem($adapter);
39+
```
40+
41+
## Laravel usage

composer.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "azure-oss/flysystem-azure-blob-storage",
3+
"type": "library",
4+
"version": "0.0.0",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Brecht Vermeersch",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"autoload": {
13+
"psr-4": {
14+
"AzureOss\\FlysystemAzureBlobStorage\\": "FlysystemAzureBlobStorage/"
15+
}
16+
},
17+
"autoload-dev": {
18+
"psr-4": {
19+
"AzureOss\\FlysystemAzureBlobStorage\\Tests\\": "tests/"
20+
}
21+
},
22+
"require": {
23+
"php": "^8.1",
24+
"league/flysystem": "^3.28",
25+
"azure-oss/storage": "@dev"
26+
},
27+
"require-dev": {
28+
"laravel/pint": "^1.16",
29+
"phpunit/phpunit": "^10.5",
30+
"phpstan/phpstan": "^1.11",
31+
"league/flysystem-adapter-test-utilities": "^3.28"
32+
},
33+
"minimum-stability": "dev",
34+
"repositories": [
35+
{
36+
"type": "path",
37+
"url": "../azure-storage-php"
38+
}
39+
]
40+
}

phpstan.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
level: 9
3+
paths:
4+
- src
5+
- tests

phpunit.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
5+
cacheDirectory=".phpunit.cache"
6+
executionOrder="depends,defects"
7+
beStrictAboutCoverageMetadata="true"
8+
beStrictAboutOutputDuringTests="true"
9+
failOnRisky="true"
10+
failOnWarning="true"
11+
displayDetailsOnTestsThatTriggerWarnings="true">
12+
<php>
13+
<env name="FLYSYSTEM_AZURE_CONNECTION_STRING" value="UseDevelopmentStorage=true" />
14+
</php>
15+
16+
<testsuites>
17+
<testsuite name="default">
18+
<directory>tests</directory>
19+
</testsuite>
20+
</testsuites>
21+
22+
<source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
23+
<include>
24+
<directory>src</directory>
25+
</include>
26+
</source>
27+
</phpunit>

0 commit comments

Comments
 (0)