Skip to content

Commit 3981b98

Browse files
author
brecht.vermeersch
committed
add checksum
1 parent 82c4826 commit 3981b98

File tree

2 files changed

+114
-91
lines changed

2 files changed

+114
-91
lines changed

src/AzureBlobStorageAdapter.php

Lines changed: 93 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
namespace AzureOss\FlysystemAzureBlobStorage;
66

7-
use AzureOss\Storage\Blob\Clients\ContainerClient;
8-
use AzureOss\Storage\Blob\Options\ListBlobsOptions;
9-
use AzureOss\Storage\Blob\Options\UploadBlockBlobOptions;
10-
use AzureOss\Storage\Blob\SAS\BlobSASPermissions;
11-
use AzureOss\Storage\Blob\SAS\BlobSASQueryParameters;
12-
use AzureOss\Storage\Blob\SAS\BlobSASSignatureValues;
13-
use AzureOss\Storage\Common\SAS\SASProtocol;
7+
use AzureOss\Storage\Blob\BlobContainerClient;
8+
use AzureOss\Storage\Blob\BlobServiceClient;
9+
use AzureOss\Storage\Blob\Models\Blob;
10+
use AzureOss\Storage\Blob\Models\BlobProperties;
11+
use AzureOss\Storage\Blob\Models\UploadBlobOptions;
12+
use AzureOss\Storage\Blob\Sas\BlobSasBuilder;
13+
use League\Flysystem\ChecksumAlgoIsNotSupported;
14+
use League\Flysystem\ChecksumProvider;
1415
use League\Flysystem\Config;
1516
use League\Flysystem\DirectoryAttributes;
1617
use League\Flysystem\FileAttributes;
@@ -21,6 +22,7 @@
2122
use League\Flysystem\UnableToDeleteFile;
2223
use League\Flysystem\UnableToListContents;
2324
use League\Flysystem\UnableToMoveFile;
25+
use League\Flysystem\UnableToProvideChecksum;
2426
use League\Flysystem\UnableToReadFile;
2527
use League\Flysystem\UnableToRetrieveMetadata;
2628
use League\Flysystem\UnableToSetVisibility;
@@ -29,21 +31,23 @@
2931
use League\MimeTypeDetection\FinfoMimeTypeDetector;
3032
use League\MimeTypeDetection\MimeTypeDetector;
3133

32-
class AzureBlobStorageAdapter implements FilesystemAdapter, TemporaryUrlGenerator
34+
class AzureBlobStorageAdapter implements FilesystemAdapter, ChecksumProvider, TemporaryUrlGenerator
3335
{
3436
private readonly MimeTypeDetector $mimeTypeDetector;
3537

3638
public function __construct(
37-
private readonly ContainerClient $containerClient,
38-
?MimeTypeDetector $mimeTypeDetector = null,
39+
private readonly BlobContainerClient $containerClient,
40+
?MimeTypeDetector $mimeTypeDetector = null,
3941
) {
4042
$this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();
4143
}
4244

4345
public function fileExists(string $path): bool
4446
{
4547
try {
46-
return $this->containerClient->getBlobClient($path)->exists();
48+
return $this->containerClient
49+
->getBlobClient($path)
50+
->exists();
4751
} catch(\Throwable $e) {
4852
throw UnableToCheckExistence::forLocation($path, $e);
4953
}
@@ -52,15 +56,11 @@ public function fileExists(string $path): bool
5256
public function directoryExists(string $path): bool
5357
{
5458
try {
55-
$options = new ListBlobsOptions(
56-
prefix: $this->getPrefix($path),
57-
maxResults: 1,
58-
delimiter: "/",
59-
);
60-
61-
$response = $this->containerClient->listBlobs($options);
59+
foreach($this->listContents($path, false) as $ignored) {
60+
return true;
61+
};
6262

63-
return count($response->blobs) > 0;
63+
return false;
6464
} catch (\Throwable $e) {
6565
throw UnableToCheckExistence::forLocation($path, $e);
6666
}
@@ -84,11 +84,13 @@ private function upload(string $path, $contents): void
8484
try {
8585
$mimetype = $this->mimeTypeDetector->detectMimetype($path, $contents);
8686

87-
$options = new UploadBlockBlobOptions(
87+
$options = new UploadBlobOptions(
8888
contentType: $mimetype,
8989
);
9090

91-
$this->containerClient->getBlockBlobClient($path)->upload($contents, $options);
91+
$this->containerClient
92+
->getBlobClient($path)
93+
->upload($contents, $options);
9294
} catch (\Throwable $e) {
9395
throw UnableToWriteFile::atLocation($path, previous: $e);
9496
}
@@ -97,9 +99,11 @@ private function upload(string $path, $contents): void
9799
public function read(string $path): string
98100
{
99101
try {
100-
$response = $this->containerClient->getBlobClient($path)->get();
102+
$result = $this->containerClient
103+
->getBlobClient($path)
104+
->downloadStreaming();
101105

102-
return $response->content->getContents();
106+
return $result->content->getContents();
103107
} catch (\Throwable $e) {
104108
throw UnableToReadFile::fromLocation($path, previous: $e);
105109
}
@@ -108,8 +112,11 @@ public function read(string $path): string
108112
public function readStream(string $path)
109113
{
110114
try {
111-
$response = $this->containerClient->getBlobClient($path)->get();
112-
$resource = $response->content->detach();
115+
$result = $this->containerClient
116+
->getBlobClient($path)
117+
->downloadStreaming();
118+
119+
$resource = $result->content->detach();
113120

114121
if($resource === null) {
115122
throw new \Exception("Should not happen");
@@ -124,7 +131,9 @@ public function readStream(string $path)
124131
public function delete(string $path): void
125132
{
126133
try {
127-
$this->containerClient->getBlobClient($path)->deleteIfExists();
134+
$this->containerClient
135+
->getBlobClient($path)
136+
->deleteIfExists();
128137
} catch (\Throwable $e) {
129138
throw UnableToDeleteFile::atLocation($path, previous: $e);
130139
}
@@ -135,7 +144,9 @@ public function deleteDirectory(string $path): void
135144
try {
136145
foreach ($this->listContents($path, true) as $item) {
137146
if ($item instanceof FileAttributes) {
138-
$this->containerClient->getBlobClient($item->path())->delete();
147+
$this->containerClient
148+
->getBlobClient($item->path())
149+
->delete();
139150
}
140151
}
141152
} catch (\Throwable $e) {
@@ -187,50 +198,46 @@ public function fileSize(string $path): FileAttributes
187198

188199
private function fetchMetadata(string $path): FileAttributes
189200
{
190-
$response = $this->containerClient->getBlobClient($path)->getProperties();
201+
$properties = $this->containerClient
202+
->getBlobClient($path)
203+
->getProperties();
191204

192-
return new FileAttributes(
193-
$path,
194-
fileSize: $response->contentLength,
195-
lastModified: $response->lastModified->getTimestamp(),
196-
mimeType: $response->contentType,
197-
);
205+
return $this->normalizeBlob($path, $properties);
198206
}
199207

200208
public function listContents(string $path, bool $deep): iterable
201209
{
202210
try {
203-
do {
204-
$nextMarker = "";
205-
206-
$options = new ListBlobsOptions(
207-
prefix: $this->getPrefix($path),
208-
marker: $nextMarker,
209-
delimiter: $deep ? null : "/",
210-
);
211-
212-
$response = $this->containerClient->listBlobs($options);
211+
$prefix = $path === "" ? null : ltrim($path, "/") . "/";
213212

214-
foreach($response->blobPrefixes as $blobPrefix) {
215-
yield new DirectoryAttributes($blobPrefix->name);
213+
if ($deep) {
214+
foreach ($this->containerClient->getBlobs($prefix) as $item) {
215+
yield $this->normalizeBlob($item->name, $item->properties);
216216
}
217-
218-
foreach ($response->blobs as $blob) {
219-
yield new FileAttributes(
220-
$blob->name,
221-
fileSize: $blob->properties->contentLength,
222-
lastModified: $blob->properties->lastModified->getTimestamp(),
223-
mimeType: $blob->properties->contentType,
224-
);
217+
} else {
218+
foreach ($this->containerClient->getBlobsByHierarchy($prefix) as $item) {
219+
if ($item instanceof Blob) {
220+
yield $this->normalizeBlob($item->name, $item->properties);
221+
} else {
222+
yield new DirectoryAttributes($item->name);
223+
}
225224
}
226-
227-
$nextMarker = $response->nextMarker;
228-
} while ($nextMarker !== "");
225+
}
229226
} catch (\Throwable $e) {
230-
throw UnableToListContents::atLocation($path, $deep, new \Exception());
227+
throw UnableToListContents::atLocation($path, $deep, $e);
231228
}
232229
}
233230

231+
private function normalizeBlob(string $name, BlobProperties $properties): FileAttributes
232+
{
233+
return new FileAttributes(
234+
$name,
235+
fileSize: $properties->contentLength,
236+
lastModified: $properties->lastModified->getTimestamp(),
237+
mimeType: $properties->contentType,
238+
);
239+
}
240+
234241
public function move(string $source, string $destination, Config $config): void
235242
{
236243
try {
@@ -244,42 +251,44 @@ public function move(string $source, string $destination, Config $config): void
244251
public function copy(string $source, string $destination, Config $config): void
245252
{
246253
try {
247-
$this->containerClient->getBlobClient($source)->copy($this->containerClient->containerName, $destination);
254+
$sourceBlobClient = $this->containerClient->getBlobClient($source);
255+
$targetBlobClient = $this->containerClient->getBlobClient($destination);
256+
257+
$targetBlobClient->copyFromUri($sourceBlobClient->uri);
248258
} catch (\Throwable $e) {
249259
throw UnableToCopyFile::fromLocationTo($source, $destination, $e);
250260
}
251261
}
252262

253-
private function getPrefix(string $path): ?string
263+
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config): string
254264
{
255-
return $path === "" ? null : rtrim($path, "/") . "/";
265+
$sasBuilder = BlobSasBuilder::new()
266+
->setExpiresOn($expiresAt)
267+
->setPermissions("r");
268+
269+
$sas = $this->containerClient
270+
->getBlobClient($path)
271+
->generateSasUri($sasBuilder);
272+
273+
return (string) $sas;
256274
}
257275

258-
public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, Config $config): string
276+
public function checksum(string $path, Config $config): string
259277
{
260-
$url = $this->containerClient->getBlobClient($path)->getUrl();
261-
262-
$values = new BlobSASSignatureValues(
263-
containerName: $this->containerClient->containerName,
264-
expiresOn: $expiresAt,
265-
blobName: $path,
266-
permissions: $config->get("permissions", (string) new BlobSASPermissions(read: true)),
267-
identifier: $config->get("identifier"),
268-
startsOn: $config->get("starts_on"),
269-
cacheControl: $config->get("cache_control"),
270-
contentDisposition: $config->get("content_disposition"),
271-
contentEncoding: $config->get("content_encoding"),
272-
contentLanguage: $config->get("content_language"),
273-
contentType: $config->get("content_type"),
274-
encryptionScope: $config->get("encryption_scope"),
275-
ipRange: $config->get("ip_range"),
276-
snapshotTime: $config->get("snapshot_time"),
277-
protocol: $config->get("protocol", SASProtocol::HTTPS_AND_HTTP),
278-
version: $config->get("version"),
279-
);
278+
$algo = $config->get('checksum_algo', 'md5');
279+
280+
if ($algo !== 'md5') {
281+
throw new ChecksumAlgoIsNotSupported();
282+
}
280283

281-
$sas = BlobSASQueryParameters::generate($values, $this->containerClient->sharedKeyCredentials);
284+
try {
285+
$properties = $this->containerClient
286+
->getBlobClient($path)
287+
->getProperties();
282288

283-
return sprintf("%s?%s", $url, $sas);
289+
return bin2hex(base64_decode($properties->contentMD5));
290+
} catch (\Throwable $e) {
291+
throw new UnableToProvideChecksum($e->getMessage(), $path, $e);
292+
}
284293
}
285294
}

tests/AzureBlobStorageTest.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,31 @@
55
namespace AzureOss\FlysystemAzureBlobStorage\Tests;
66

77
use AzureOss\FlysystemAzureBlobStorage\AzureBlobStorageAdapter;
8-
use AzureOss\Storage\Blob\Clients\BlobServiceClient;
9-
use AzureOss\Storage\Blob\Clients\ContainerClient;
10-
use League\Flysystem\AdapterTestUtilities\FilesystemAdapterTestCase as TestCase;
8+
use AzureOss\Storage\Blob\BlobContainerClient;
9+
use AzureOss\Storage\Blob\BlobServiceClient;
10+
use League\Flysystem\AdapterTestUtilities\FilesystemAdapterTestCase;
1111
use League\Flysystem\Config;
1212
use League\Flysystem\FilesystemAdapter;
1313
use League\Flysystem\Visibility;
1414

15-
class AzureBlobStorageTest extends TestCase
15+
class AzureBlobStorageTest extends FilesystemAdapterTestCase
1616
{
1717
public const CONTAINER_NAME = 'flysystem';
1818

1919
protected static function createFilesystemAdapter(): FilesystemAdapter
2020
{
21-
return new AzureBlobStorageAdapter(self::createContainerClient());
21+
$connectionString = getenv('FLYSYSTEM_AZURE_CONNECTION_STRING');
22+
23+
if (empty($connectionString)) {
24+
self::markTestSkipped('FLYSYSTEM_AZURE_CONNECTION_STRING is not provided.');
25+
}
26+
27+
return new AzureBlobStorageAdapter(
28+
self::createContainerClient(),
29+
);
2230
}
2331

24-
private static function createContainerClient(): ContainerClient
32+
private static function createContainerClient(): BlobContainerClient
2533
{
2634
$connectionString = getenv('FLYSYSTEM_AZURE_CONNECTION_STRING');
2735

@@ -32,11 +40,17 @@ private static function createContainerClient(): ContainerClient
3240
return BlobServiceClient::fromConnectionString($connectionString)->getContainerClient('flysystem');
3341
}
3442

43+
public static function setUpBeforeClass(): void
44+
{
45+
self::createContainerClient()->deleteIfExists();
46+
self::createContainerClient()->create();
47+
}
48+
3549
public function overwriting_a_file(): void
3650
{
3751
$this->runScenario(
3852
function () {
39-
$this->givenWeHaveAnExistingFile('path.txt', 'contents');
53+
$this->givenWeHaveAnExistingFile('path.txt');
4054
$adapter = $this->adapter();
4155

4256
$adapter->write('path.txt', 'new contents', new Config());

0 commit comments

Comments
 (0)