From 773f117812b78c23f7a69a1b80ce6b54945b8439 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Sat, 19 Jun 2021 00:15:00 +0300 Subject: [PATCH 01/21] AzureBlobStorageAdapter --- src/AzureBlobStorage/.gitattributes | 8 + src/AzureBlobStorage/.gitignore | 5 + .../AzureBlobStorageAdapter.php | 284 ++++++++++++++++++ src/AzureBlobStorage/AzureBlobStorageTest.php | 65 ++++ src/AzureBlobStorage/composer.json | 29 ++ 5 files changed, 391 insertions(+) create mode 100644 src/AzureBlobStorage/.gitattributes create mode 100644 src/AzureBlobStorage/.gitignore create mode 100644 src/AzureBlobStorage/AzureBlobStorageAdapter.php create mode 100644 src/AzureBlobStorage/AzureBlobStorageTest.php create mode 100644 src/AzureBlobStorage/composer.json diff --git a/src/AzureBlobStorage/.gitattributes b/src/AzureBlobStorage/.gitattributes new file mode 100644 index 000000000..2d1bc9058 --- /dev/null +++ b/src/AzureBlobStorage/.gitattributes @@ -0,0 +1,8 @@ +* text=auto + +tests/ +.gitignore +.gitattributes +.travis.yml +phpunit.xml + diff --git a/src/AzureBlobStorage/.gitignore b/src/AzureBlobStorage/.gitignore new file mode 100644 index 000000000..af35797ca --- /dev/null +++ b/src/AzureBlobStorage/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +/coverage/ +/coverage.xml +/composer.lock +/phpunit.xml diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php new file mode 100644 index 000000000..0e874d7a5 --- /dev/null +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -0,0 +1,284 @@ +client = $client; + $this->container = $container; + $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); + $this->maxResultsForContentsListing = $maxResultsForContentsListing; + } + + private function upload(string $destination, $contents, Config $config): void + { + try { + $options = $this->getOptionsFromConfig($config); + + if (empty($options->getContentType())) { + $options->setContentType($this->mimeTypeDetector->detectMimeType($destination, $contents)); + } + + $this->client->createBlockBlob( + $this->container, + $destination, + $contents, + $options + ); + } catch (Throwable $exception) { + throw UnableToWriteFile::atLocation($destination, '', $exception); + } + } + + public function copy(string $source, string $destination, Config $config): void + { + $this->client->copyBlob($this->container, $destination, $this->container, $source); + } + + public function delete(string $path): void + { + try { + $this->client->deleteBlob($this->container, $path); + } catch (Throwable $exception) { + throw UnableToDeleteFile::atLocation($path, '', $exception); + } + } + + public function read(string $path): string + { + $response = $this->readStream($path); + + return stream_get_contents($response); + } + + public function readStream($path) + { + try { + $response = $this->client->getBlob($this->container, $path); + + return $response->getContentStream(); + } catch (Throwable $exception) { + throw UnableToReadFile::fromLocation($path); + } + } + + public function listContents(string $path, bool $deep = false): iterable + { + if (strlen($path) > 0) { + $path = rtrim($path, '/') . '/'; + } + + $options = new ListBlobsOptions(); + $options->setPrefix($path); + $options->setMaxResults($this->maxResultsForContentsListing); + + if (!$deep) { + $options->setDelimiter('/'); + } + + do { + $response = $this->client->listBlobs($this->container, $options); + $continuationToken = $response->getContinuationToken(); + + foreach ($response->getBlobs() as $blob) { + $name = $blob->getName(); + + if ($path === '' || strpos($name, $path) === 0) { + yield $this->normalizeBlobProperties($name, $blob->getProperties()); + } + } + + if (!$deep) { + foreach ($response->getBlobPrefixes() as $blobPrefix) { + yield new DirectoryAttributes( + rtrim($blobPrefix->getName(), '/') + ); + } + } + $options->setContinuationToken($continuationToken); + } while ($continuationToken instanceof ContinuationToken); + } + + private function getMetadata($path): FileAttributes + { + return $this->normalizeBlobProperties( + $path, + $this->client->getBlobProperties($this->container, $path)->getProperties() + ); + } + + public function getSize($path): FileAttributes + { + try { + return $this->getMetadata($path); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::fileSize($path, '', $exception); + } + } + + private function getOptionsFromConfig(Config $config): CreateBlockBlobOptions + { + $options = $config->get('blobOptions', new CreateBlockBlobOptions()); + foreach (self::META_OPTIONS as $option) { + if (!$config->get($option)) { + continue; + } + call_user_func([$options, "set$option"], $config->get($option)); + } + $mimeType = $config->get('mimetype'); + if ($mimeType !== null) { + $options->setContentType($mimeType); + } + + return $options; + } + + private function normalizeBlobProperties($path, BlobProperties $properties): FileAttributes + { + return new FileAttributes( + $path, + $properties->getContentLength(), + null, + $properties->getLastModified()->getTimestamp(), + $properties->getContentType() + ); + } + + public function fileExists(string $path): bool + { + try { + return $this->getMetadata($path) !== null; + } catch (Throwable $exception) { + if ($exception instanceof ServiceException && $exception->getCode() === 404) { + return false; + } + throw UnableToCheckFileExistence::forLocation($path, $exception); + } + } + + public function deleteDirectory(string $path): void + { + try { + $options = new ListBlobsOptions(); + $options->setPrefix($path . '/'); + $listResults = $this->client->listBlobs($this->container, $options); + foreach ($listResults->getBlobs() as $blob) { + $this->client->deleteBlob($this->container, $blob->getName()); + } + } catch (Throwable $exception) { + UnableToDeleteDirectory::atLocation($path, '', $exception); + } + } + + public function createDirectory(string $path, Config $config): void + { + } + + public function setVisibility(string $path, string $visibility): void + { + } + + public function visibility(string $path): FileAttributes + { + try { + return $this->getMetadata($path); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::visibility($path); + } + } + + public function mimeType(string $path): FileAttributes + { + try { + return $this->getMetadata($path); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::mimeType($path, '', $exception); + } + } + + public function lastModified(string $path): FileAttributes + { + try { + return $this->getMetadata($path); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::lastModified($path, '', $exception); + } + } + + public function fileSize(string $path): FileAttributes + { + try { + return $this->getMetadata($path); + } catch (Throwable $exception) { + throw UnableToRetrieveMetadata::fileSize($path, '', $exception); + } + } + + public function move(string $source, string $destination, Config $config): void + { + try { + $this->copy($source, $destination, $config); + $this->delete($source); + } catch (Throwable $exception) { + throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); + } + } + + public function write(string $path, string $contents, Config $config): void + { + $this->upload($path, $contents, $config); + } + + public function writeStream(string $path, $contents, Config $config): void + { + $this->upload($path, $contents, $config); + } +} diff --git a/src/AzureBlobStorage/AzureBlobStorageTest.php b/src/AzureBlobStorage/AzureBlobStorageTest.php new file mode 100644 index 000000000..d667ffae6 --- /dev/null +++ b/src/AzureBlobStorage/AzureBlobStorageTest.php @@ -0,0 +1,65 @@ +runScenario(function () { + $this->givenWeHaveAnExistingFile('path.txt', 'contents'); + $adapter = $this->adapter(); + + $adapter->write('path.txt', 'new contents', new Config()); + + $contents = $adapter->read('path.txt'); + $this->assertEquals('new contents', $contents); + }); + } + + /** + * @test + */ + public function setting_visibility(): void + { + self::markTestSkipped('Azure does not support visibility'); + } + + /** + * @test + */ + public function failing_to_set_visibility(): void + { + self::markTestSkipped('Azure does not support visibility'); + } + + /** + * @test + */ + public function failing_to_check_visibility(): void + { + self::markTestSkipped('Azure does not support visibility'); + } +} diff --git a/src/AzureBlobStorage/composer.json b/src/AzureBlobStorage/composer.json new file mode 100644 index 000000000..24f939e6a --- /dev/null +++ b/src/AzureBlobStorage/composer.json @@ -0,0 +1,29 @@ +{ + "name": "league/flysystem-azure-blob-storage", + "autoload": { + "psr-4": { + "League\\Flysystem\\AzureBlobStorage\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "League\\Flysystem\\AzureBlobStorage\\Tests\\": "tests/" + } + }, + "require": { + "php": ">=7.2", + "league/flysystem": "^2.0", + "microsoft/azure-storage-blob": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "league/flysystem-adapter-test-utilities": "^2.0" + }, + "license": "MIT", + "authors": [ + { + "name": "Warxcell", + "email": "warxcell@gmail.com" + } + ] +} From 008caba84e6a532a6887aa25599410aaf1deb879 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Tue, 6 Jul 2021 12:18:54 +0300 Subject: [PATCH 02/21] apply CR --- .../AzureBlobStorageAdapter.php | 127 ++++++++---------- 1 file changed, 59 insertions(+), 68 deletions(-) diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index 0e874d7a5..910245f77 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -58,26 +58,6 @@ public function __construct( $this->maxResultsForContentsListing = $maxResultsForContentsListing; } - private function upload(string $destination, $contents, Config $config): void - { - try { - $options = $this->getOptionsFromConfig($config); - - if (empty($options->getContentType())) { - $options->setContentType($this->mimeTypeDetector->detectMimeType($destination, $contents)); - } - - $this->client->createBlockBlob( - $this->container, - $destination, - $contents, - $options - ); - } catch (Throwable $exception) { - throw UnableToWriteFile::atLocation($destination, '', $exception); - } - } - public function copy(string $source, string $destination, Config $config): void { $this->client->copyBlob($this->container, $destination, $this->container, $source); @@ -113,7 +93,7 @@ public function readStream($path) public function listContents(string $path, bool $deep = false): iterable { if (strlen($path) > 0) { - $path = rtrim($path, '/') . '/'; + $path = rtrim($path, '/').'/'; } $options = new ListBlobsOptions(); @@ -147,51 +127,6 @@ public function listContents(string $path, bool $deep = false): iterable } while ($continuationToken instanceof ContinuationToken); } - private function getMetadata($path): FileAttributes - { - return $this->normalizeBlobProperties( - $path, - $this->client->getBlobProperties($this->container, $path)->getProperties() - ); - } - - public function getSize($path): FileAttributes - { - try { - return $this->getMetadata($path); - } catch (Throwable $exception) { - throw UnableToRetrieveMetadata::fileSize($path, '', $exception); - } - } - - private function getOptionsFromConfig(Config $config): CreateBlockBlobOptions - { - $options = $config->get('blobOptions', new CreateBlockBlobOptions()); - foreach (self::META_OPTIONS as $option) { - if (!$config->get($option)) { - continue; - } - call_user_func([$options, "set$option"], $config->get($option)); - } - $mimeType = $config->get('mimetype'); - if ($mimeType !== null) { - $options->setContentType($mimeType); - } - - return $options; - } - - private function normalizeBlobProperties($path, BlobProperties $properties): FileAttributes - { - return new FileAttributes( - $path, - $properties->getContentLength(), - null, - $properties->getLastModified()->getTimestamp(), - $properties->getContentType() - ); - } - public function fileExists(string $path): bool { try { @@ -208,13 +143,13 @@ public function deleteDirectory(string $path): void { try { $options = new ListBlobsOptions(); - $options->setPrefix($path . '/'); + $options->setPrefix($path.'/'); $listResults = $this->client->listBlobs($this->container, $options); foreach ($listResults->getBlobs() as $blob) { $this->client->deleteBlob($this->container, $blob->getName()); } } catch (Throwable $exception) { - UnableToDeleteDirectory::atLocation($path, '', $exception); + throw UnableToDeleteDirectory::atLocation($path, '', $exception); } } @@ -281,4 +216,60 @@ public function writeStream(string $path, $contents, Config $config): void { $this->upload($path, $contents, $config); } + + private function upload(string $destination, $contents, Config $config): void + { + try { + $options = $this->getOptionsFromConfig($config); + + if (empty($options->getContentType())) { + $options->setContentType($this->mimeTypeDetector->detectMimeType($destination, $contents)); + } + + $this->client->createBlockBlob( + $this->container, + $destination, + $contents, + $options + ); + } catch (Throwable $exception) { + throw UnableToWriteFile::atLocation($destination, '', $exception); + } + } + + private function getMetadata($path): FileAttributes + { + return $this->normalizeBlobProperties( + $path, + $this->client->getBlobProperties($this->container, $path)->getProperties() + ); + } + + private function getOptionsFromConfig(Config $config): CreateBlockBlobOptions + { + $options = $config->get('blobOptions', new CreateBlockBlobOptions()); + foreach (self::META_OPTIONS as $option) { + if (!$config->get($option)) { + continue; + } + call_user_func([$options, "set$option"], $config->get($option)); + } + $mimeType = $config->get('mimetype'); + if ($mimeType !== null) { + $options->setContentType($mimeType); + } + + return $options; + } + + private function normalizeBlobProperties($path, BlobProperties $properties): FileAttributes + { + return new FileAttributes( + $path, + $properties->getContentLength(), + null, + $properties->getLastModified()->getTimestamp(), + $properties->getContentType() + ); + } } From 1171ba81cddfd5eae5627a3978ff86492c678758 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Tue, 6 Jul 2021 12:29:12 +0300 Subject: [PATCH 03/21] add previous exception in visibility. --- src/AzureBlobStorage/AzureBlobStorageAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index 910245f77..184ee1a8d 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -166,7 +166,7 @@ public function visibility(string $path): FileAttributes try { return $this->getMetadata($path); } catch (Throwable $exception) { - throw UnableToRetrieveMetadata::visibility($path); + throw UnableToRetrieveMetadata::visibility($path, '', $exception); } } From 201826723ad0355a45d780b324908caa2c238a3f Mon Sep 17 00:00:00 2001 From: Warxcell Date: Tue, 20 Jul 2021 00:40:03 +0300 Subject: [PATCH 04/21] Add PathResolverInterface --- src/AzureBlobStorage/.gitattributes | 11 ++-- src/AzureBlobStorage/.gitignore | 5 -- .../AzureBlobStorageAdapter.php | 64 +++++++++++++------ src/AzureBlobStorage/AzureBlobStorageTest.php | 18 +++--- .../PathResolverInterface.php | 10 +++ src/AzureBlobStorage/Resolved.php | 30 +++++++++ .../StaticContainerPathResolver.php | 20 ++++++ 7 files changed, 121 insertions(+), 37 deletions(-) delete mode 100644 src/AzureBlobStorage/.gitignore create mode 100644 src/AzureBlobStorage/PathResolverInterface.php create mode 100644 src/AzureBlobStorage/Resolved.php create mode 100644 src/AzureBlobStorage/StaticContainerPathResolver.php diff --git a/src/AzureBlobStorage/.gitattributes b/src/AzureBlobStorage/.gitattributes index 2d1bc9058..6250eb06e 100644 --- a/src/AzureBlobStorage/.gitattributes +++ b/src/AzureBlobStorage/.gitattributes @@ -1,8 +1,7 @@ * text=auto -tests/ -.gitignore -.gitattributes -.travis.yml -phpunit.xml - +.github export-ignore +.gitattributes export-ignore +.gitignore export-ignore +**/*Test.php export-ignore +**/*Stub.php export-ignore diff --git a/src/AzureBlobStorage/.gitignore b/src/AzureBlobStorage/.gitignore deleted file mode 100644 index af35797ca..000000000 --- a/src/AzureBlobStorage/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/vendor/ -/coverage/ -/coverage.xml -/composer.lock -/phpunit.xml diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index 184ee1a8d..bfa33a593 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -9,6 +9,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\UnableToCheckFileExistence; +use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; @@ -39,8 +40,8 @@ class AzureBlobStorageAdapter implements FilesystemAdapter ]; /** @var BlobRestProxy */ private $client; - /** @var string */ - private $container; + /** @var PathResolverInterface */ + private $pathResolver; /** @var MimeTypeDetector */ private $mimeTypeDetector; /** @var int */ @@ -48,25 +49,33 @@ class AzureBlobStorageAdapter implements FilesystemAdapter public function __construct( BlobRestProxy $client, - string $container, + PathResolverInterface $pathResolver, MimeTypeDetector $mimeTypeDetector = null, int $maxResultsForContentsListing = 5000 ) { $this->client = $client; - $this->container = $container; + $this->pathResolver = $pathResolver; $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); $this->maxResultsForContentsListing = $maxResultsForContentsListing; } public function copy(string $source, string $destination, Config $config): void { - $this->client->copyBlob($this->container, $destination, $this->container, $source); + $sourceResolved = $this->pathResolver->resolve($source); + $destinationResolved = $this->pathResolver->resolve($destination); + $this->client->copyBlob( + $destinationResolved->getContainer(), + $destinationResolved->getPath(), + $sourceResolved->getContainer(), + $sourceResolved->getPath() + ); } public function delete(string $path): void { try { - $this->client->deleteBlob($this->container, $path); + $resolved = $this->pathResolver->resolve($path); + $this->client->deleteBlob($resolved->getContainer(), $resolved->getPath()); } catch (Throwable $exception) { throw UnableToDeleteFile::atLocation($path, '', $exception); } @@ -82,7 +91,8 @@ public function read(string $path): string public function readStream($path) { try { - $response = $this->client->getBlob($this->container, $path); + $resolved = $this->pathResolver->resolve($path); + $response = $this->client->getBlob($resolved->getContainer(), $resolved->getPath()); return $response->getContentStream(); } catch (Throwable $exception) { @@ -96,8 +106,10 @@ public function listContents(string $path, bool $deep = false): iterable $path = rtrim($path, '/').'/'; } + $resolved = $this->pathResolver->resolve($path); + $options = new ListBlobsOptions(); - $options->setPrefix($path); + $options->setPrefix($resolved->getPath()); $options->setMaxResults($this->maxResultsForContentsListing); if (!$deep) { @@ -105,7 +117,7 @@ public function listContents(string $path, bool $deep = false): iterable } do { - $response = $this->client->listBlobs($this->container, $options); + $response = $this->client->listBlobs($resolved->getContainer(), $options); $continuationToken = $response->getContinuationToken(); foreach ($response->getBlobs() as $blob) { @@ -142,11 +154,17 @@ public function fileExists(string $path): bool public function deleteDirectory(string $path): void { try { - $options = new ListBlobsOptions(); - $options->setPrefix($path.'/'); - $listResults = $this->client->listBlobs($this->container, $options); - foreach ($listResults->getBlobs() as $blob) { - $this->client->deleteBlob($this->container, $blob->getName()); + $resolved = $this->pathResolver->resolve($path); + + if ($resolved->getPath() === '') { + $this->client->deleteContainer($resolved->getContainer()); + } else { + $options = new ListBlobsOptions(); + $options->setPrefix($path.'/'); + $listResults = $this->client->listBlobs($resolved->getContainer(), $options); + foreach ($listResults->getBlobs() as $blob) { + $this->client->deleteBlob($resolved->getContainer(), $blob->getName()); + } } } catch (Throwable $exception) { throw UnableToDeleteDirectory::atLocation($path, '', $exception); @@ -155,6 +173,12 @@ public function deleteDirectory(string $path): void public function createDirectory(string $path, Config $config): void { + try { + $resolved = $this->pathResolver->resolve($path); + $this->client->createContainer($resolved->getContainer()); + } catch (Throwable $exception) { + throw UnableToCreateDirectory::dueToFailure($path, $exception); + } } public function setVisibility(string $path, string $visibility): void @@ -220,6 +244,8 @@ public function writeStream(string $path, $contents, Config $config): void private function upload(string $destination, $contents, Config $config): void { try { + $resolved = $this->pathResolver->resolve($destination); + $options = $this->getOptionsFromConfig($config); if (empty($options->getContentType())) { @@ -227,8 +253,8 @@ private function upload(string $destination, $contents, Config $config): void } $this->client->createBlockBlob( - $this->container, - $destination, + $resolved->getContainer(), + $resolved->getPath(), $contents, $options ); @@ -237,11 +263,13 @@ private function upload(string $destination, $contents, Config $config): void } } - private function getMetadata($path): FileAttributes + private function getMetadata(string $path): FileAttributes { + $resolved = $this->pathResolver->resolve($path); + return $this->normalizeBlobProperties( $path, - $this->client->getBlobProperties($this->container, $path)->getProperties() + $this->client->getBlobProperties($resolved->getContainer(), $resolved->getPath())->getProperties() ); } diff --git a/src/AzureBlobStorage/AzureBlobStorageTest.php b/src/AzureBlobStorage/AzureBlobStorageTest.php index d667ffae6..1d9346bc9 100644 --- a/src/AzureBlobStorage/AzureBlobStorageTest.php +++ b/src/AzureBlobStorage/AzureBlobStorageTest.php @@ -20,7 +20,7 @@ protected static function createFilesystemAdapter(): FilesystemAdapter $connectString = "DefaultEndpointsProtocol=https;AccountName={$accountName};AccountKey={$accountKey}==;EndpointSuffix=core.windows.net"; $client = BlobRestProxy::createBlobService($connectString); - return new AzureBlobStorageAdapter($client, self::CONTAINER_NAME); + return new AzureBlobStorageAdapter($client, new StaticContainerPathResolver(self::CONTAINER_NAME)); } /** @@ -28,15 +28,17 @@ protected static function createFilesystemAdapter(): FilesystemAdapter */ public function overwriting_a_file(): void { - $this->runScenario(function () { - $this->givenWeHaveAnExistingFile('path.txt', 'contents'); - $adapter = $this->adapter(); + $this->runScenario( + function () { + $this->givenWeHaveAnExistingFile('path.txt', 'contents'); + $adapter = $this->adapter(); - $adapter->write('path.txt', 'new contents', new Config()); + $adapter->write('path.txt', 'new contents', new Config()); - $contents = $adapter->read('path.txt'); - $this->assertEquals('new contents', $contents); - }); + $contents = $adapter->read('path.txt'); + $this->assertEquals('new contents', $contents); + } + ); } /** diff --git a/src/AzureBlobStorage/PathResolverInterface.php b/src/AzureBlobStorage/PathResolverInterface.php new file mode 100644 index 000000000..cb437394a --- /dev/null +++ b/src/AzureBlobStorage/PathResolverInterface.php @@ -0,0 +1,10 @@ +container = $container; + $this->path = $path; + } + + public function getContainer(): string + { + return $this->container; + } + + public function getPath(): string + { + return $this->path; + } +} diff --git a/src/AzureBlobStorage/StaticContainerPathResolver.php b/src/AzureBlobStorage/StaticContainerPathResolver.php new file mode 100644 index 000000000..9b7114726 --- /dev/null +++ b/src/AzureBlobStorage/StaticContainerPathResolver.php @@ -0,0 +1,20 @@ +container = $container; + } + + public function resolve(string $path): Resolved + { + return new Resolved($this->container, $path); + } +} From d11060a37df4dec3a982a53c5d8aa0db31ec8886 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Tue, 20 Jul 2021 00:42:21 +0300 Subject: [PATCH 05/21] Fix wrong variable usage. --- src/AzureBlobStorage/AzureBlobStorageAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index bfa33a593..0b1ee6032 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -160,7 +160,7 @@ public function deleteDirectory(string $path): void $this->client->deleteContainer($resolved->getContainer()); } else { $options = new ListBlobsOptions(); - $options->setPrefix($path.'/'); + $options->setPrefix($resolved->getPath().'/'); $listResults = $this->client->listBlobs($resolved->getContainer(), $options); foreach ($listResults->getBlobs() as $blob) { $this->client->deleteBlob($resolved->getContainer(), $blob->getName()); From be663152ffc1ef7482b6c6749e71282c2667853c Mon Sep 17 00:00:00 2001 From: Warxcell Date: Tue, 20 Jul 2021 00:54:53 +0300 Subject: [PATCH 06/21] add first directory as container resolver. --- .../FirstDirectoryAsContainerPathResolver.php | 30 +++++++++++++++++++ ...stDirectoryAsContainerPathResolverTest.php | 29 ++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php create mode 100644 src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php diff --git a/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php b/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php new file mode 100644 index 000000000..4dfa60872 --- /dev/null +++ b/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php @@ -0,0 +1,30 @@ +container = $container; + } + + public function resolve(string $path): Resolved + { + $directories = explode(DIRECTORY_SEPARATOR, $path); + + if (count($directories) <= 1) { + return new Resolved($this->container, $path); + } + + return new Resolved($directories[0], str_replace($directories[0].DIRECTORY_SEPARATOR, '', $path)); + } +} diff --git a/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php b/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php new file mode 100644 index 000000000..97f4f6612 --- /dev/null +++ b/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php @@ -0,0 +1,29 @@ + ['filename.jpg', 'backup-container', 'filename.jpg']; + yield 'single directory' => ['container1/filename.jpg', 'container1', 'filename.jpg']; + yield 'two directories' => ['container1/directory1/filename.jpg', 'container1', 'directory1/filename.jpg']; + } + + /** + * @dataProvider resolvedData + */ + public function testResolved(string $path, string $expectedContainer, string $expectedPath) + { + $resolver = new FirstDirectoryAsContainerPathResolver('backup-container'); + $resolved = $resolver->resolve($path); + + self::assertEquals($expectedContainer, $resolved->getContainer()); + self::assertEquals($expectedPath, $resolved->getPath()); + } +} From e0941f0581d26cccbbab49fb2f79bafeae9405ea Mon Sep 17 00:00:00 2001 From: Warxcell Date: Tue, 20 Jul 2021 20:13:53 +0300 Subject: [PATCH 07/21] Remove rtrim on dir listing --- src/AzureBlobStorage/AzureBlobStorageAdapter.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index 0b1ee6032..d2dd68a37 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -102,10 +102,6 @@ public function readStream($path) public function listContents(string $path, bool $deep = false): iterable { - if (strlen($path) > 0) { - $path = rtrim($path, '/').'/'; - } - $resolved = $this->pathResolver->resolve($path); $options = new ListBlobsOptions(); From 2122a8dbb540af7e7331a9400ce3d1722dbfdc34 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Tue, 20 Jul 2021 20:53:03 +0300 Subject: [PATCH 08/21] Remove Path Resolver. --- .../AzureBlobStorageAdapter.php | 81 ++++++------------- src/AzureBlobStorage/AzureBlobStorageTest.php | 8 +- .../FirstDirectoryAsContainerPathResolver.php | 30 ------- ...stDirectoryAsContainerPathResolverTest.php | 29 ------- .../PathResolverInterface.php | 10 --- src/AzureBlobStorage/Resolved.php | 30 ------- .../StaticContainerPathResolver.php | 20 ----- 7 files changed, 28 insertions(+), 180 deletions(-) delete mode 100644 src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php delete mode 100644 src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php delete mode 100644 src/AzureBlobStorage/PathResolverInterface.php delete mode 100644 src/AzureBlobStorage/Resolved.php delete mode 100644 src/AzureBlobStorage/StaticContainerPathResolver.php diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index d2dd68a37..d091edc67 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -9,7 +9,6 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\UnableToCheckFileExistence; -use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; @@ -26,7 +25,6 @@ use MicrosoftAzure\Storage\Common\Models\ContinuationToken; use Throwable; use function stream_get_contents; -use function strpos; class AzureBlobStorageAdapter implements FilesystemAdapter { @@ -40,8 +38,8 @@ class AzureBlobStorageAdapter implements FilesystemAdapter ]; /** @var BlobRestProxy */ private $client; - /** @var PathResolverInterface */ - private $pathResolver; + /** @var string */ + private $container; /** @var MimeTypeDetector */ private $mimeTypeDetector; /** @var int */ @@ -49,33 +47,30 @@ class AzureBlobStorageAdapter implements FilesystemAdapter public function __construct( BlobRestProxy $client, - PathResolverInterface $pathResolver, + string $container, MimeTypeDetector $mimeTypeDetector = null, int $maxResultsForContentsListing = 5000 ) { $this->client = $client; - $this->pathResolver = $pathResolver; + $this->container = $container; $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); $this->maxResultsForContentsListing = $maxResultsForContentsListing; } public function copy(string $source, string $destination, Config $config): void { - $sourceResolved = $this->pathResolver->resolve($source); - $destinationResolved = $this->pathResolver->resolve($destination); $this->client->copyBlob( - $destinationResolved->getContainer(), - $destinationResolved->getPath(), - $sourceResolved->getContainer(), - $sourceResolved->getPath() + $this->container, + $destination, + $this->container, + $source ); } public function delete(string $path): void { try { - $resolved = $this->pathResolver->resolve($path); - $this->client->deleteBlob($resolved->getContainer(), $resolved->getPath()); + $this->client->deleteBlob($this->container, $path); } catch (Throwable $exception) { throw UnableToDeleteFile::atLocation($path, '', $exception); } @@ -91,8 +86,7 @@ public function read(string $path): string public function readStream($path) { try { - $resolved = $this->pathResolver->resolve($path); - $response = $this->client->getBlob($resolved->getContainer(), $resolved->getPath()); + $response = $this->client->getBlob($this->container, $path); return $response->getContentStream(); } catch (Throwable $exception) { @@ -102,35 +96,26 @@ public function readStream($path) public function listContents(string $path, bool $deep = false): iterable { - $resolved = $this->pathResolver->resolve($path); - $options = new ListBlobsOptions(); - $options->setPrefix($resolved->getPath()); + $options->setPrefix($path); $options->setMaxResults($this->maxResultsForContentsListing); - - if (!$deep) { - $options->setDelimiter('/'); - } + $options->setDelimiter('/'); do { - $response = $this->client->listBlobs($resolved->getContainer(), $options); - $continuationToken = $response->getContinuationToken(); + $response = $this->client->listBlobs($this->container, $options); foreach ($response->getBlobs() as $blob) { $name = $blob->getName(); - - if ($path === '' || strpos($name, $path) === 0) { - yield $this->normalizeBlobProperties($name, $blob->getProperties()); - } + yield $this->normalizeBlobProperties($name, $blob->getProperties()); } if (!$deep) { foreach ($response->getBlobPrefixes() as $blobPrefix) { - yield new DirectoryAttributes( - rtrim($blobPrefix->getName(), '/') - ); + yield new DirectoryAttributes($blobPrefix->getName()); } } + + $continuationToken = $response->getContinuationToken(); $options->setContinuationToken($continuationToken); } while ($continuationToken instanceof ContinuationToken); } @@ -150,17 +135,11 @@ public function fileExists(string $path): bool public function deleteDirectory(string $path): void { try { - $resolved = $this->pathResolver->resolve($path); - - if ($resolved->getPath() === '') { - $this->client->deleteContainer($resolved->getContainer()); - } else { - $options = new ListBlobsOptions(); - $options->setPrefix($resolved->getPath().'/'); - $listResults = $this->client->listBlobs($resolved->getContainer(), $options); - foreach ($listResults->getBlobs() as $blob) { - $this->client->deleteBlob($resolved->getContainer(), $blob->getName()); - } + $options = new ListBlobsOptions(); + $options->setPrefix($path); + $listResults = $this->client->listBlobs($this->container, $options); + foreach ($listResults->getBlobs() as $blob) { + $this->client->deleteBlob($this->container, $blob->getName()); } } catch (Throwable $exception) { throw UnableToDeleteDirectory::atLocation($path, '', $exception); @@ -169,12 +148,6 @@ public function deleteDirectory(string $path): void public function createDirectory(string $path, Config $config): void { - try { - $resolved = $this->pathResolver->resolve($path); - $this->client->createContainer($resolved->getContainer()); - } catch (Throwable $exception) { - throw UnableToCreateDirectory::dueToFailure($path, $exception); - } } public function setVisibility(string $path, string $visibility): void @@ -240,8 +213,6 @@ public function writeStream(string $path, $contents, Config $config): void private function upload(string $destination, $contents, Config $config): void { try { - $resolved = $this->pathResolver->resolve($destination); - $options = $this->getOptionsFromConfig($config); if (empty($options->getContentType())) { @@ -249,8 +220,8 @@ private function upload(string $destination, $contents, Config $config): void } $this->client->createBlockBlob( - $resolved->getContainer(), - $resolved->getPath(), + $this->container, + $destination, $contents, $options ); @@ -261,11 +232,9 @@ private function upload(string $destination, $contents, Config $config): void private function getMetadata(string $path): FileAttributes { - $resolved = $this->pathResolver->resolve($path); - return $this->normalizeBlobProperties( $path, - $this->client->getBlobProperties($resolved->getContainer(), $resolved->getPath())->getProperties() + $this->client->getBlobProperties($this->container, $path)->getProperties() ); } diff --git a/src/AzureBlobStorage/AzureBlobStorageTest.php b/src/AzureBlobStorage/AzureBlobStorageTest.php index 1d9346bc9..d8afbcd80 100644 --- a/src/AzureBlobStorage/AzureBlobStorageTest.php +++ b/src/AzureBlobStorage/AzureBlobStorageTest.php @@ -8,6 +8,7 @@ use League\Flysystem\Config; use League\Flysystem\FilesystemAdapter; use MicrosoftAzure\Storage\Blob\BlobRestProxy; +use function getenv; class AzureBlobStorageTest extends TestCase { @@ -15,12 +16,9 @@ class AzureBlobStorageTest extends TestCase protected static function createFilesystemAdapter(): FilesystemAdapter { - $accountKey = getenv('FLYSYSTEM_AZURE_ACCOUNT_KEY'); - $accountName = getenv('FLYSYSTEM_AZURE_ACCOUNT_NAME'); - $connectString = "DefaultEndpointsProtocol=https;AccountName={$accountName};AccountKey={$accountKey}==;EndpointSuffix=core.windows.net"; - $client = BlobRestProxy::createBlobService($connectString); + $client = BlobRestProxy::createBlobService(getenv('FLYSYSTEM_AZURE_DSN')); - return new AzureBlobStorageAdapter($client, new StaticContainerPathResolver(self::CONTAINER_NAME)); + return new AzureBlobStorageAdapter($client, self::CONTAINER_NAME); } /** diff --git a/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php b/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php deleted file mode 100644 index 4dfa60872..000000000 --- a/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolver.php +++ /dev/null @@ -1,30 +0,0 @@ -container = $container; - } - - public function resolve(string $path): Resolved - { - $directories = explode(DIRECTORY_SEPARATOR, $path); - - if (count($directories) <= 1) { - return new Resolved($this->container, $path); - } - - return new Resolved($directories[0], str_replace($directories[0].DIRECTORY_SEPARATOR, '', $path)); - } -} diff --git a/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php b/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php deleted file mode 100644 index 97f4f6612..000000000 --- a/src/AzureBlobStorage/FirstDirectoryAsContainerPathResolverTest.php +++ /dev/null @@ -1,29 +0,0 @@ - ['filename.jpg', 'backup-container', 'filename.jpg']; - yield 'single directory' => ['container1/filename.jpg', 'container1', 'filename.jpg']; - yield 'two directories' => ['container1/directory1/filename.jpg', 'container1', 'directory1/filename.jpg']; - } - - /** - * @dataProvider resolvedData - */ - public function testResolved(string $path, string $expectedContainer, string $expectedPath) - { - $resolver = new FirstDirectoryAsContainerPathResolver('backup-container'); - $resolved = $resolver->resolve($path); - - self::assertEquals($expectedContainer, $resolved->getContainer()); - self::assertEquals($expectedPath, $resolved->getPath()); - } -} diff --git a/src/AzureBlobStorage/PathResolverInterface.php b/src/AzureBlobStorage/PathResolverInterface.php deleted file mode 100644 index cb437394a..000000000 --- a/src/AzureBlobStorage/PathResolverInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -container = $container; - $this->path = $path; - } - - public function getContainer(): string - { - return $this->container; - } - - public function getPath(): string - { - return $this->path; - } -} diff --git a/src/AzureBlobStorage/StaticContainerPathResolver.php b/src/AzureBlobStorage/StaticContainerPathResolver.php deleted file mode 100644 index 9b7114726..000000000 --- a/src/AzureBlobStorage/StaticContainerPathResolver.php +++ /dev/null @@ -1,20 +0,0 @@ -container = $container; - } - - public function resolve(string $path): Resolved - { - return new Resolved($this->container, $path); - } -} From 7daf4b08516b008c10af0a77933e0ab9b24b88f9 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Sun, 19 Sep 2021 10:21:01 +0300 Subject: [PATCH 09/21] Add path resolver --- .../AzureBlobStorageAdapter.php | 66 +++++++++++-------- src/AzureBlobStorage/Path.php | 26 ++++++++ .../PathResolverInterface.php | 11 ++++ .../StaticContainerPathResolver.php | 22 +++++++ 4 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 src/AzureBlobStorage/Path.php create mode 100644 src/AzureBlobStorage/PathResolverInterface.php create mode 100644 src/AzureBlobStorage/StaticContainerPathResolver.php diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index d091edc67..76b1e20a1 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -24,6 +24,7 @@ use MicrosoftAzure\Storage\Common\Exceptions\ServiceException; use MicrosoftAzure\Storage\Common\Models\ContinuationToken; use Throwable; + use function stream_get_contents; class AzureBlobStorageAdapter implements FilesystemAdapter @@ -38,8 +39,8 @@ class AzureBlobStorageAdapter implements FilesystemAdapter ]; /** @var BlobRestProxy */ private $client; - /** @var string */ - private $container; + /** @var PathResolverInterface */ + private $pathResolver; /** @var MimeTypeDetector */ private $mimeTypeDetector; /** @var int */ @@ -47,30 +48,34 @@ class AzureBlobStorageAdapter implements FilesystemAdapter public function __construct( BlobRestProxy $client, - string $container, + PathResolverInterface $pathResolver, MimeTypeDetector $mimeTypeDetector = null, int $maxResultsForContentsListing = 5000 ) { $this->client = $client; - $this->container = $container; + $this->pathResolver = $pathResolver; $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); $this->maxResultsForContentsListing = $maxResultsForContentsListing; } public function copy(string $source, string $destination, Config $config): void { + $resolvedDestination = $this->pathResolver->resolve($destination); + $resolvedSource = $this->pathResolver->resolve($source); + $this->client->copyBlob( - $this->container, - $destination, - $this->container, - $source + $resolvedDestination->getContainer(), + $resolvedDestination->getPath(), + $resolvedSource->getContainer(), + $resolvedSource->getPath() ); } public function delete(string $path): void { + $resolved = $this->pathResolver->resolve($path); try { - $this->client->deleteBlob($this->container, $path); + $this->client->deleteBlob($resolved->getContainer(), $resolved->getPath()); } catch (Throwable $exception) { throw UnableToDeleteFile::atLocation($path, '', $exception); } @@ -85,8 +90,9 @@ public function read(string $path): string public function readStream($path) { + $resolved = $this->pathResolver->resolve($path); try { - $response = $this->client->getBlob($this->container, $path); + $response = $this->client->getBlob($resolved->getContainer(), $resolved->getPath()); return $response->getContentStream(); } catch (Throwable $exception) { @@ -96,13 +102,15 @@ public function readStream($path) public function listContents(string $path, bool $deep = false): iterable { + $resolved = $this->pathResolver->resolve($path); + $options = new ListBlobsOptions(); - $options->setPrefix($path); + $options->setPrefix($resolved->getPath()); $options->setMaxResults($this->maxResultsForContentsListing); $options->setDelimiter('/'); do { - $response = $this->client->listBlobs($this->container, $options); + $response = $this->client->listBlobs($resolved->getContainer(), $options); foreach ($response->getBlobs() as $blob) { $name = $blob->getName(); @@ -122,8 +130,9 @@ public function listContents(string $path, bool $deep = false): iterable public function fileExists(string $path): bool { + $resolved = $this->pathResolver->resolve($path); try { - return $this->getMetadata($path) !== null; + return $this->getMetadata($resolved) !== null; } catch (Throwable $exception) { if ($exception instanceof ServiceException && $exception->getCode() === 404) { return false; @@ -134,12 +143,14 @@ public function fileExists(string $path): bool public function deleteDirectory(string $path): void { + $resolved = $this->pathResolver->resolve($path); + try { $options = new ListBlobsOptions(); - $options->setPrefix($path); - $listResults = $this->client->listBlobs($this->container, $options); + $options->setPrefix($resolved->getPath()); + $listResults = $this->client->listBlobs($resolved->getContainer(), $options); foreach ($listResults->getBlobs() as $blob) { - $this->client->deleteBlob($this->container, $blob->getName()); + $this->client->deleteBlob($resolved->getContainer(), $blob->getName()); } } catch (Throwable $exception) { throw UnableToDeleteDirectory::atLocation($path, '', $exception); @@ -157,7 +168,7 @@ public function setVisibility(string $path, string $visibility): void public function visibility(string $path): FileAttributes { try { - return $this->getMetadata($path); + return $this->getMetadata($this->pathResolver->resolve($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::visibility($path, '', $exception); } @@ -166,7 +177,7 @@ public function visibility(string $path): FileAttributes public function mimeType(string $path): FileAttributes { try { - return $this->getMetadata($path); + return $this->getMetadata($this->pathResolver->resolve($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::mimeType($path, '', $exception); } @@ -175,7 +186,7 @@ public function mimeType(string $path): FileAttributes public function lastModified(string $path): FileAttributes { try { - return $this->getMetadata($path); + return $this->getMetadata($this->pathResolver->resolve($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::lastModified($path, '', $exception); } @@ -184,7 +195,7 @@ public function lastModified(string $path): FileAttributes public function fileSize(string $path): FileAttributes { try { - return $this->getMetadata($path); + return $this->getMetadata($this->pathResolver->resolve($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::fileSize($path, '', $exception); } @@ -212,16 +223,17 @@ public function writeStream(string $path, $contents, Config $config): void private function upload(string $destination, $contents, Config $config): void { + $resolved = $this->pathResolver->resolve($destination); try { $options = $this->getOptionsFromConfig($config); if (empty($options->getContentType())) { - $options->setContentType($this->mimeTypeDetector->detectMimeType($destination, $contents)); + $options->setContentType($this->mimeTypeDetector->detectMimeType($resolved->getPath(), $contents)); } $this->client->createBlockBlob( - $this->container, - $destination, + $resolved->getContainer(), + $resolved->getPath(), $contents, $options ); @@ -230,11 +242,11 @@ private function upload(string $destination, $contents, Config $config): void } } - private function getMetadata(string $path): FileAttributes + private function getMetadata(Path $path): FileAttributes { return $this->normalizeBlobProperties( - $path, - $this->client->getBlobProperties($this->container, $path)->getProperties() + $path->getPath(), + $this->client->getBlobProperties($path->getContainer(), $path->getPath())->getProperties() ); } @@ -255,7 +267,7 @@ private function getOptionsFromConfig(Config $config): CreateBlockBlobOptions return $options; } - private function normalizeBlobProperties($path, BlobProperties $properties): FileAttributes + private function normalizeBlobProperties(string $path, BlobProperties $properties): FileAttributes { return new FileAttributes( $path, diff --git a/src/AzureBlobStorage/Path.php b/src/AzureBlobStorage/Path.php new file mode 100644 index 000000000..4fb8f20ce --- /dev/null +++ b/src/AzureBlobStorage/Path.php @@ -0,0 +1,26 @@ +container = $container; + $this->path = $path; + } + + public function getContainer() + { + return $this->container; + } + + public function getPath() + { + return $this->path; + } +} + diff --git a/src/AzureBlobStorage/PathResolverInterface.php b/src/AzureBlobStorage/PathResolverInterface.php new file mode 100644 index 000000000..09235442d --- /dev/null +++ b/src/AzureBlobStorage/PathResolverInterface.php @@ -0,0 +1,11 @@ +container = $container; + } + + public function resolve(string $path): Path + { + return new Path($this > $this->container, $path); + } +} + From eef5b06ee45769ab5c64bd823449bd44abd010a5 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Sun, 19 Sep 2021 10:28:20 +0300 Subject: [PATCH 10/21] add create directory --- .../AzureBlobStorageAdapter.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index 76b1e20a1..a258bbb6d 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -9,6 +9,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\UnableToCheckFileExistence; +use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; @@ -20,11 +21,14 @@ use MicrosoftAzure\Storage\Blob\BlobRestProxy; use MicrosoftAzure\Storage\Blob\Models\BlobProperties; use MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions; +use MicrosoftAzure\Storage\Blob\Models\PublicAccessType; use MicrosoftAzure\Storage\Common\Exceptions\ServiceException; use MicrosoftAzure\Storage\Common\Models\ContinuationToken; use Throwable; +use function sprintf; use function stream_get_contents; class AzureBlobStorageAdapter implements FilesystemAdapter @@ -159,6 +163,23 @@ public function deleteDirectory(string $path): void public function createDirectory(string $path, Config $config): void { + $createContainerOptions = new CreateContainerOptions(); + $createContainerOptions->setPublicAccess(PublicAccessType::BLOBS_ONLY); + + try { + $this->client->createContainer( + $this->pathResolver->resolve($path)->getContainer(), + $createContainerOptions + ); + } catch (Throwable $exception) { + if ($exception instanceof ServiceException && 409 === $exception->getCode()) { + throw UnableToCreateDirectory::dueToFailure($path, $exception); + } + throw UnableToCreateDirectory::dueToFailure( + sprintf('Unable to create container "%s".', $path), + $exception + ); + } } public function setVisibility(string $path, string $visibility): void From 286b5601029ff6e43595349726e94b027b8f0b8a Mon Sep 17 00:00:00 2001 From: Yup Date: Thu, 18 Nov 2021 19:38:16 +0200 Subject: [PATCH 11/21] Update src/AzureBlobStorage/StaticContainerPathResolver.php Co-authored-by: Maxime Helias --- src/AzureBlobStorage/StaticContainerPathResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureBlobStorage/StaticContainerPathResolver.php b/src/AzureBlobStorage/StaticContainerPathResolver.php index d327d510d..6c752ec51 100644 --- a/src/AzureBlobStorage/StaticContainerPathResolver.php +++ b/src/AzureBlobStorage/StaticContainerPathResolver.php @@ -16,7 +16,7 @@ public function __construct(string $container) public function resolve(string $path): Path { - return new Path($this > $this->container, $path); + return new Path($this->container, $path); } } From 75b1a7e98ec68231206302a883ce51cc182535b5 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Fri, 14 Jan 2022 23:31:49 +0100 Subject: [PATCH 12/21] WIP: prepare for 3.x azure adapter. --- composer.json | 1 + .../AzureBlobStorageAdapter.php | 21 ++++++++++++------- src/AzureBlobStorage/composer.json | 4 ++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 073340f39..6f157dfbf 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "require-dev": { "ext-zip": "*", "ext-fileinfo": "*", + "microsoft/azure-storage-blob": "^1.1", "phpunit/phpunit": "^9.5.11", "phpstan/phpstan": "^0.12.26", "phpseclib/phpseclib": "^2.0", diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index a258bbb6d..daf815b50 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -8,6 +8,8 @@ use League\Flysystem\DirectoryAttributes; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\FilesystemException; +use League\Flysystem\UnableToCheckExistence; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; @@ -111,22 +113,22 @@ public function listContents(string $path, bool $deep = false): iterable $options = new ListBlobsOptions(); $options->setPrefix($resolved->getPath()); $options->setMaxResults($this->maxResultsForContentsListing); - $options->setDelimiter('/'); + if ($deep) { + $options->setDelimiter('/'); + } do { $response = $this->client->listBlobs($resolved->getContainer(), $options); + foreach ($response->getBlobPrefixes() as $blobPrefix) { + yield new DirectoryAttributes($blobPrefix->getName()); + } + foreach ($response->getBlobs() as $blob) { $name = $blob->getName(); yield $this->normalizeBlobProperties($name, $blob->getProperties()); } - if (!$deep) { - foreach ($response->getBlobPrefixes() as $blobPrefix) { - yield new DirectoryAttributes($blobPrefix->getName()); - } - } - $continuationToken = $response->getContinuationToken(); $options->setContinuationToken($continuationToken); } while ($continuationToken instanceof ContinuationToken); @@ -145,6 +147,11 @@ public function fileExists(string $path): bool } } + public function directoryExists(string $path): bool + { + return false; + } + public function deleteDirectory(string $path): void { $resolved = $this->pathResolver->resolve($path); diff --git a/src/AzureBlobStorage/composer.json b/src/AzureBlobStorage/composer.json index 24f939e6a..4f13794f0 100644 --- a/src/AzureBlobStorage/composer.json +++ b/src/AzureBlobStorage/composer.json @@ -11,8 +11,8 @@ } }, "require": { - "php": ">=7.2", - "league/flysystem": "^2.0", + "php": "^8.0.2", + "league/flysystem": "^2.0.0 || ^3.0.0", "microsoft/azure-storage-blob": "^1.1" }, "require-dev": { From ca769a8d41adba21079863ec16d19947d2bc6d31 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sat, 15 Jan 2022 19:37:00 +0100 Subject: [PATCH 13/21] Rename interface --- src/AzureBlobStorage/AzureBlobStorageAdapter.php | 4 ++-- src/AzureBlobStorage/AzureBlobStorageTest.php | 2 +- .../{PathResolverInterface.php => PathResolver.php} | 2 +- src/AzureBlobStorage/StaticContainerPathResolver.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/AzureBlobStorage/{PathResolverInterface.php => PathResolver.php} (80%) diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index daf815b50..523639535 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -45,7 +45,7 @@ class AzureBlobStorageAdapter implements FilesystemAdapter ]; /** @var BlobRestProxy */ private $client; - /** @var PathResolverInterface */ + /** @var PathResolver */ private $pathResolver; /** @var MimeTypeDetector */ private $mimeTypeDetector; @@ -54,7 +54,7 @@ class AzureBlobStorageAdapter implements FilesystemAdapter public function __construct( BlobRestProxy $client, - PathResolverInterface $pathResolver, + PathResolver $pathResolver, MimeTypeDetector $mimeTypeDetector = null, int $maxResultsForContentsListing = 5000 ) { diff --git a/src/AzureBlobStorage/AzureBlobStorageTest.php b/src/AzureBlobStorage/AzureBlobStorageTest.php index d8afbcd80..2398915f1 100644 --- a/src/AzureBlobStorage/AzureBlobStorageTest.php +++ b/src/AzureBlobStorage/AzureBlobStorageTest.php @@ -18,7 +18,7 @@ protected static function createFilesystemAdapter(): FilesystemAdapter { $client = BlobRestProxy::createBlobService(getenv('FLYSYSTEM_AZURE_DSN')); - return new AzureBlobStorageAdapter($client, self::CONTAINER_NAME); + return new AzureBlobStorageAdapter($client, new StaticContainerPathResolver(self::CONTAINER_NAME)); } /** diff --git a/src/AzureBlobStorage/PathResolverInterface.php b/src/AzureBlobStorage/PathResolver.php similarity index 80% rename from src/AzureBlobStorage/PathResolverInterface.php rename to src/AzureBlobStorage/PathResolver.php index 09235442d..f433d531c 100644 --- a/src/AzureBlobStorage/PathResolverInterface.php +++ b/src/AzureBlobStorage/PathResolver.php @@ -4,7 +4,7 @@ namespace League\Flysystem\AzureBlobStorage; -interface PathResolverInterface +interface PathResolver { public function resolve(string $path): Path; } diff --git a/src/AzureBlobStorage/StaticContainerPathResolver.php b/src/AzureBlobStorage/StaticContainerPathResolver.php index 6c752ec51..5b415563c 100644 --- a/src/AzureBlobStorage/StaticContainerPathResolver.php +++ b/src/AzureBlobStorage/StaticContainerPathResolver.php @@ -4,7 +4,7 @@ namespace League\Flysystem\AzureBlobStorage; -class StaticContainerPathResolver implements PathResolverInterface +class StaticContainerPathResolver implements PathResolver { /** @var string */ private $container; From d4cc6a1c62a2aac694af395866678dcf4bede1f9 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 21:47:22 +0100 Subject: [PATCH 14/21] Whip the Azure adapter into shape --- .../AzureBlobStorageAdapter.php | 188 +++++++++++------- src/AzureBlobStorage/AzureBlobStorageTest.php | 132 +++++++++++- 2 files changed, 242 insertions(+), 78 deletions(-) diff --git a/src/AzureBlobStorage/AzureBlobStorageAdapter.php b/src/AzureBlobStorage/AzureBlobStorageAdapter.php index 523639535..89283add0 100644 --- a/src/AzureBlobStorage/AzureBlobStorageAdapter.php +++ b/src/AzureBlobStorage/AzureBlobStorageAdapter.php @@ -8,29 +8,27 @@ use League\Flysystem\DirectoryAttributes; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; -use League\Flysystem\FilesystemException; -use League\Flysystem\UnableToCheckExistence; +use League\Flysystem\PathPrefixer; +use League\Flysystem\UnableToCheckDirectoryExistence; use League\Flysystem\UnableToCheckFileExistence; -use League\Flysystem\UnableToCreateDirectory; +use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; use League\Flysystem\UnableToRetrieveMetadata; +use League\Flysystem\UnableToSetVisibility; use League\Flysystem\UnableToWriteFile; use League\MimeTypeDetection\FinfoMimeTypeDetector; use League\MimeTypeDetection\MimeTypeDetector; use MicrosoftAzure\Storage\Blob\BlobRestProxy; use MicrosoftAzure\Storage\Blob\Models\BlobProperties; use MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions; -use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions; -use MicrosoftAzure\Storage\Blob\Models\PublicAccessType; use MicrosoftAzure\Storage\Common\Exceptions\ServiceException; use MicrosoftAzure\Storage\Common\Models\ContinuationToken; use Throwable; -use function sprintf; use function stream_get_contents; class AzureBlobStorageAdapter implements FilesystemAdapter @@ -43,46 +41,65 @@ class AzureBlobStorageAdapter implements FilesystemAdapter 'ContentLanguage', 'ContentEncoding', ]; - /** @var BlobRestProxy */ - private $client; - /** @var PathResolver */ - private $pathResolver; - /** @var MimeTypeDetector */ - private $mimeTypeDetector; - /** @var int */ - private $maxResultsForContentsListing; + const ON_VISIBILITY_THROW_ERROR = 'throw'; + const ON_VISIBILITY_IGNORE = 'ignore'; + + private BlobRestProxy $client; + + private MimeTypeDetector $mimeTypeDetector; + + private int $maxResultsForContentsListing; + + private string $container; + + private PathPrefixer $prefixer; + + private string $visibilityHandling; public function __construct( BlobRestProxy $client, - PathResolver $pathResolver, + string $container, + string $prefix = '', MimeTypeDetector $mimeTypeDetector = null, - int $maxResultsForContentsListing = 5000 + int $maxResultsForContentsListing = 5000, + string $visibilityHandling = self::ON_VISIBILITY_THROW_ERROR, ) { $this->client = $client; - $this->pathResolver = $pathResolver; + $this->container = $container; + $this->prefixer = new PathPrefixer($prefix); $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); $this->maxResultsForContentsListing = $maxResultsForContentsListing; + $this->visibilityHandling = $visibilityHandling; } public function copy(string $source, string $destination, Config $config): void { - $resolvedDestination = $this->pathResolver->resolve($destination); - $resolvedSource = $this->pathResolver->resolve($source); - - $this->client->copyBlob( - $resolvedDestination->getContainer(), - $resolvedDestination->getPath(), - $resolvedSource->getContainer(), - $resolvedSource->getPath() - ); + $resolvedDestination = $this->prefixer->prefixPath($destination); + $resolvedSource = $this->prefixer->prefixPath($source); + + try { + $this->client->copyBlob( + $this->container, + $resolvedDestination, + $this->container, + $resolvedSource + ); + } catch (Throwable $throwable) { + throw UnableToCopyFile::fromLocationTo($source, $destination, $throwable); + } } public function delete(string $path): void { - $resolved = $this->pathResolver->resolve($path); + $location = $this->prefixer->prefixPath($path); + try { - $this->client->deleteBlob($resolved->getContainer(), $resolved->getPath()); + $this->client->deleteBlob($this->container, $location); } catch (Throwable $exception) { + if ($exception instanceof ServiceException && $exception->getCode() === 404) { + return; + } + throw UnableToDeleteFile::atLocation($path, '', $exception); } } @@ -96,37 +113,41 @@ public function read(string $path): string public function readStream($path) { - $resolved = $this->pathResolver->resolve($path); + $location = $this->prefixer->prefixPath($path); + try { - $response = $this->client->getBlob($resolved->getContainer(), $resolved->getPath()); + $response = $this->client->getBlob($this->container, $location); return $response->getContentStream(); } catch (Throwable $exception) { - throw UnableToReadFile::fromLocation($path); + throw UnableToReadFile::fromLocation($path, '', $exception); } } public function listContents(string $path, bool $deep = false): iterable { - $resolved = $this->pathResolver->resolve($path); + $resolved = $this->prefixer->prefixDirectoryPath($path); $options = new ListBlobsOptions(); - $options->setPrefix($resolved->getPath()); + $options->setPrefix($resolved); $options->setMaxResults($this->maxResultsForContentsListing); - if ($deep) { + + if ($deep === false) { $options->setDelimiter('/'); } do { - $response = $this->client->listBlobs($resolved->getContainer(), $options); + $response = $this->client->listBlobs($this->container, $options); foreach ($response->getBlobPrefixes() as $blobPrefix) { - yield new DirectoryAttributes($blobPrefix->getName()); + yield new DirectoryAttributes($this->prefixer->stripDirectoryPrefix($blobPrefix->getName())); } foreach ($response->getBlobs() as $blob) { - $name = $blob->getName(); - yield $this->normalizeBlobProperties($name, $blob->getProperties()); + yield $this->normalizeBlobProperties( + $this->prefixer->stripPrefix($blob->getName()), + $blob->getProperties() + ); } $continuationToken = $response->getContinuationToken(); @@ -136,9 +157,9 @@ public function listContents(string $path, bool $deep = false): iterable public function fileExists(string $path): bool { - $resolved = $this->pathResolver->resolve($path); + $resolved = $this->prefixer->prefixPath($path); try { - return $this->getMetadata($resolved) !== null; + return $this->fetchMetadata($resolved) !== null; } catch (Throwable $exception) { if ($exception instanceof ServiceException && $exception->getCode() === 404) { return false; @@ -149,19 +170,39 @@ public function fileExists(string $path): bool public function directoryExists(string $path): bool { - return false; + $resolved = $this->prefixer->prefixDirectoryPath($path); + $options = new ListBlobsOptions(); + $options->setPrefix($resolved); + $options->setMaxResults(1); + + try { + $listResults = $this->client->listBlobs($this->container, $options); + + return count($listResults->getBlobs()) > 0; + } catch (Throwable $exception) { + throw UnableToCheckDirectoryExistence::forLocation($path, $exception); + } } public function deleteDirectory(string $path): void { - $resolved = $this->pathResolver->resolve($path); + $resolved = $this->prefixer->prefixDirectoryPath($path); + $options = new ListBlobsOptions(); + $options->setPrefix($resolved); try { - $options = new ListBlobsOptions(); - $options->setPrefix($resolved->getPath()); - $listResults = $this->client->listBlobs($resolved->getContainer(), $options); + start: + $listResults = $this->client->listBlobs($this->container, $options); + foreach ($listResults->getBlobs() as $blob) { - $this->client->deleteBlob($resolved->getContainer(), $blob->getName()); + $this->client->deleteBlob($this->container, $blob->getName()); + } + + $continuationToken = $listResults->getContinuationToken(); + + if ($continuationToken instanceof ContinuationToken) { + $options->setContinuationToken($continuationToken); + goto start; } } catch (Throwable $exception) { throw UnableToDeleteDirectory::atLocation($path, '', $exception); @@ -170,33 +211,20 @@ public function deleteDirectory(string $path): void public function createDirectory(string $path, Config $config): void { - $createContainerOptions = new CreateContainerOptions(); - $createContainerOptions->setPublicAccess(PublicAccessType::BLOBS_ONLY); - - try { - $this->client->createContainer( - $this->pathResolver->resolve($path)->getContainer(), - $createContainerOptions - ); - } catch (Throwable $exception) { - if ($exception instanceof ServiceException && 409 === $exception->getCode()) { - throw UnableToCreateDirectory::dueToFailure($path, $exception); - } - throw UnableToCreateDirectory::dueToFailure( - sprintf('Unable to create container "%s".', $path), - $exception - ); - } + // this is not supported by Azure } public function setVisibility(string $path, string $visibility): void { + if ($this->visibilityHandling === self::ON_VISIBILITY_THROW_ERROR) { + throw UnableToSetVisibility::atLocation($path, 'Azure does not support this operation.'); + } } public function visibility(string $path): FileAttributes { try { - return $this->getMetadata($this->pathResolver->resolve($path)); + return $this->fetchMetadata($this->prefixer->prefixPath($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::visibility($path, '', $exception); } @@ -205,7 +233,7 @@ public function visibility(string $path): FileAttributes public function mimeType(string $path): FileAttributes { try { - return $this->getMetadata($this->pathResolver->resolve($path)); + return $this->fetchMetadata($this->prefixer->prefixPath($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::mimeType($path, '', $exception); } @@ -214,7 +242,7 @@ public function mimeType(string $path): FileAttributes public function lastModified(string $path): FileAttributes { try { - return $this->getMetadata($this->pathResolver->resolve($path)); + return $this->fetchMetadata($this->prefixer->prefixPath($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::lastModified($path, '', $exception); } @@ -223,7 +251,7 @@ public function lastModified(string $path): FileAttributes public function fileSize(string $path): FileAttributes { try { - return $this->getMetadata($this->pathResolver->resolve($path)); + return $this->fetchMetadata($this->prefixer->prefixPath($path)); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::fileSize($path, '', $exception); } @@ -251,17 +279,17 @@ public function writeStream(string $path, $contents, Config $config): void private function upload(string $destination, $contents, Config $config): void { - $resolved = $this->pathResolver->resolve($destination); + $resolved = $this->prefixer->prefixPath($destination); try { $options = $this->getOptionsFromConfig($config); if (empty($options->getContentType())) { - $options->setContentType($this->mimeTypeDetector->detectMimeType($resolved->getPath(), $contents)); + $options->setContentType($this->mimeTypeDetector->detectMimeType($resolved, $contents)); } $this->client->createBlockBlob( - $resolved->getContainer(), - $resolved->getPath(), + $this->container, + $resolved, $contents, $options ); @@ -270,24 +298,30 @@ private function upload(string $destination, $contents, Config $config): void } } - private function getMetadata(Path $path): FileAttributes + private function fetchMetadata(string $path): FileAttributes { return $this->normalizeBlobProperties( - $path->getPath(), - $this->client->getBlobProperties($path->getContainer(), $path->getPath())->getProperties() + $path, + $this->client->getBlobProperties($this->container, $path)->getProperties() ); } private function getOptionsFromConfig(Config $config): CreateBlockBlobOptions { - $options = $config->get('blobOptions', new CreateBlockBlobOptions()); + $options = new CreateBlockBlobOptions(); + foreach (self::META_OPTIONS as $option) { - if (!$config->get($option)) { + $setting = $config->get($option, '___NOT__SET___'); + + if ($setting === '___NOT__SET___') { continue; } - call_user_func([$options, "set$option"], $config->get($option)); + + call_user_func([$options, "set$option"], $setting); } + $mimeType = $config->get('mimetype'); + if ($mimeType !== null) { $options->setContentType($mimeType); } diff --git a/src/AzureBlobStorage/AzureBlobStorageTest.php b/src/AzureBlobStorage/AzureBlobStorageTest.php index 2398915f1..5e2014216 100644 --- a/src/AzureBlobStorage/AzureBlobStorageTest.php +++ b/src/AzureBlobStorage/AzureBlobStorageTest.php @@ -7,6 +7,8 @@ use League\Flysystem\AdapterTestUtilities\FilesystemAdapterTestCase as TestCase; use League\Flysystem\Config; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\UnableToSetVisibility; +use League\Flysystem\Visibility; use MicrosoftAzure\Storage\Blob\BlobRestProxy; use function getenv; @@ -18,7 +20,7 @@ protected static function createFilesystemAdapter(): FilesystemAdapter { $client = BlobRestProxy::createBlobService(getenv('FLYSYSTEM_AZURE_DSN')); - return new AzureBlobStorageAdapter($client, new StaticContainerPathResolver(self::CONTAINER_NAME)); + return new AzureBlobStorageAdapter($client, self::CONTAINER_NAME, 'ci'); } /** @@ -62,4 +64,132 @@ public function failing_to_check_visibility(): void { self::markTestSkipped('Azure does not support visibility'); } + + public function fetching_unknown_mime_type_of_a_file(): void + { + $this->markTestSkipped('This adapter always returns a mime-type'); + } + + public function listing_contents_recursive(): void + { + $this->markTestSkipped('This adapter does not support creating directories'); + } + + /** + * @test + */ + public function copying_a_file(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + $adapter->write( + 'source.txt', + 'contents to be copied', + new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC]) + ); + + $adapter->copy('source.txt', 'destination.txt', new Config()); + + $this->assertTrue($adapter->fileExists('source.txt')); + $this->assertTrue($adapter->fileExists('destination.txt')); + $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); + }); + } + + /** + * @test + */ + public function moving_a_file(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + $adapter->write( + 'source.txt', + 'contents to be copied', + new Config([Config::OPTION_VISIBILITY => Visibility::PUBLIC]) + ); + $adapter->move('source.txt', 'destination.txt', new Config()); + $this->assertFalse( + $adapter->fileExists('source.txt'), + 'After moving a file should no longer exist in the original location.' + ); + $this->assertTrue( + $adapter->fileExists('destination.txt'), + 'After moving, a file should be present at the new location.' + ); + $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); + }); + } + + /** + * @test + */ + public function copying_a_file_again(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + $adapter->write( + 'source.txt', + 'contents to be copied', + new Config() + ); + + $adapter->copy('source.txt', 'destination.txt', new Config()); + + $this->assertTrue($adapter->fileExists('source.txt')); + $this->assertTrue($adapter->fileExists('destination.txt')); + $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); + }); + } + + /** + * @test + */ + public function setting_visibility_can_be_ignored_not_supported(): void + { + $this->givenWeHaveAnExistingFile('some-file.md'); + $this->expectNotToPerformAssertions(); + + $client = BlobRestProxy::createBlobService(getenv('FLYSYSTEM_AZURE_DSN')); + $adapter = new AzureBlobStorageAdapter($client, self::CONTAINER_NAME, 'ci', null, 50000, AzureBlobStorageAdapter::ON_VISIBILITY_IGNORE); + + $adapter->setVisibility('some-file.md', 'public'); + } + + /** + * @test + */ + public function setting_visibility_causes_errors(): void + { + $this->givenWeHaveAnExistingFile('some-file.md'); + $adapter = $this->adapter(); + + $this->expectException(UnableToSetVisibility::class); + + $adapter->setVisibility('some-file.md', 'public'); + } + + /** + * @test + */ + public function checking_if_a_directory_exists_after_creating_it(): void + { + $this->markTestSkipped('This adapter does not support creating directories'); + } + + /** + * @test + */ + public function setting_visibility_on_a_file_that_does_not_exist(): void + { + $this->markTestSkipped('This adapter does not support visibility'); + } + + /** + * @test + */ + public function creating_a_directory(): void + { + $this->markTestSkipped('This adapter does not support creating directories'); + } } From 60972f085fd168e7106dc48913e5e4a85cb54b90 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 21:55:42 +0100 Subject: [PATCH 15/21] Skip azure tests when the dsn is not set --- src/AzureBlobStorage/AzureBlobStorageTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/AzureBlobStorage/AzureBlobStorageTest.php b/src/AzureBlobStorage/AzureBlobStorageTest.php index 5e2014216..3073f72c9 100644 --- a/src/AzureBlobStorage/AzureBlobStorageTest.php +++ b/src/AzureBlobStorage/AzureBlobStorageTest.php @@ -18,7 +18,13 @@ class AzureBlobStorageTest extends TestCase protected static function createFilesystemAdapter(): FilesystemAdapter { - $client = BlobRestProxy::createBlobService(getenv('FLYSYSTEM_AZURE_DSN')); + $dsn = getenv('FLYSYSTEM_AZURE_DSN'); + + if (empty($dsn)) { + self::markTestSkipped('FLYSYSTEM_AZURE_DSN is not provided.'); + } + + $client = BlobRestProxy::createBlobService($dsn); return new AzureBlobStorageAdapter($client, self::CONTAINER_NAME, 'ci'); } From 96b460b1a176877f8dc823ba3b7aa81b94d70fac Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 21:55:58 +0100 Subject: [PATCH 16/21] CS fixes --- src/AzureBlobStorage/Path.php | 1 - src/AzureBlobStorage/PathResolver.php | 1 - src/AzureBlobStorage/StaticContainerPathResolver.php | 1 - 3 files changed, 3 deletions(-) diff --git a/src/AzureBlobStorage/Path.php b/src/AzureBlobStorage/Path.php index 4fb8f20ce..16ef03551 100644 --- a/src/AzureBlobStorage/Path.php +++ b/src/AzureBlobStorage/Path.php @@ -23,4 +23,3 @@ public function getPath() return $this->path; } } - diff --git a/src/AzureBlobStorage/PathResolver.php b/src/AzureBlobStorage/PathResolver.php index f433d531c..fa6eed1db 100644 --- a/src/AzureBlobStorage/PathResolver.php +++ b/src/AzureBlobStorage/PathResolver.php @@ -8,4 +8,3 @@ interface PathResolver { public function resolve(string $path): Path; } - diff --git a/src/AzureBlobStorage/StaticContainerPathResolver.php b/src/AzureBlobStorage/StaticContainerPathResolver.php index 5b415563c..34ab44674 100644 --- a/src/AzureBlobStorage/StaticContainerPathResolver.php +++ b/src/AzureBlobStorage/StaticContainerPathResolver.php @@ -19,4 +19,3 @@ public function resolve(string $path): Path return new Path($this->container, $path); } } - From 599a114d4c319b974076944545764d0857518f68 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 22:06:55 +0100 Subject: [PATCH 17/21] Remove unused classes. --- src/AzureBlobStorage/AzureBlobStorageTest.php | 3 +++ src/AzureBlobStorage/Path.php | 25 ------------------- src/AzureBlobStorage/PathResolver.php | 10 -------- .../StaticContainerPathResolver.php | 21 ---------------- 4 files changed, 3 insertions(+), 56 deletions(-) delete mode 100644 src/AzureBlobStorage/Path.php delete mode 100644 src/AzureBlobStorage/PathResolver.php delete mode 100644 src/AzureBlobStorage/StaticContainerPathResolver.php diff --git a/src/AzureBlobStorage/AzureBlobStorageTest.php b/src/AzureBlobStorage/AzureBlobStorageTest.php index 3073f72c9..1d144c8c6 100644 --- a/src/AzureBlobStorage/AzureBlobStorageTest.php +++ b/src/AzureBlobStorage/AzureBlobStorageTest.php @@ -12,6 +12,9 @@ use MicrosoftAzure\Storage\Blob\BlobRestProxy; use function getenv; +/** + * @group azure + */ class AzureBlobStorageTest extends TestCase { const CONTAINER_NAME = 'flysystem'; diff --git a/src/AzureBlobStorage/Path.php b/src/AzureBlobStorage/Path.php deleted file mode 100644 index 16ef03551..000000000 --- a/src/AzureBlobStorage/Path.php +++ /dev/null @@ -1,25 +0,0 @@ -container = $container; - $this->path = $path; - } - - public function getContainer() - { - return $this->container; - } - - public function getPath() - { - return $this->path; - } -} diff --git a/src/AzureBlobStorage/PathResolver.php b/src/AzureBlobStorage/PathResolver.php deleted file mode 100644 index fa6eed1db..000000000 --- a/src/AzureBlobStorage/PathResolver.php +++ /dev/null @@ -1,10 +0,0 @@ -container = $container; - } - - public function resolve(string $path): Path - { - return new Path($this->container, $path); - } -} From 98e832158a5054ac7cf3f78f4d9e77048b6139bd Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 22:13:37 +0100 Subject: [PATCH 18/21] Use default author, contributions are in git --- src/AzureBlobStorage/composer.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/AzureBlobStorage/composer.json b/src/AzureBlobStorage/composer.json index 4f13794f0..6f72e7016 100644 --- a/src/AzureBlobStorage/composer.json +++ b/src/AzureBlobStorage/composer.json @@ -5,11 +5,6 @@ "League\\Flysystem\\AzureBlobStorage\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "League\\Flysystem\\AzureBlobStorage\\Tests\\": "tests/" - } - }, "require": { "php": "^8.0.2", "league/flysystem": "^2.0.0 || ^3.0.0", @@ -22,8 +17,8 @@ "license": "MIT", "authors": [ { - "name": "Warxcell", - "email": "warxcell@gmail.com" + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" } ] } From fadeb9121276e118aceedc5bde09d1cd59b14d3f Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 22:15:48 +0100 Subject: [PATCH 19/21] Remove unneeded dev-dependencies --- src/AzureBlobStorage/composer.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/AzureBlobStorage/composer.json b/src/AzureBlobStorage/composer.json index 6f72e7016..76cc1e128 100644 --- a/src/AzureBlobStorage/composer.json +++ b/src/AzureBlobStorage/composer.json @@ -10,10 +10,6 @@ "league/flysystem": "^2.0.0 || ^3.0.0", "microsoft/azure-storage-blob": "^1.1" }, - "require-dev": { - "phpunit/phpunit": "^9.0", - "league/flysystem-adapter-test-utilities": "^2.0" - }, "license": "MIT", "authors": [ { From 3e7ce035e09b7249a6076b611ce062d0fb4d80d5 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 22:16:34 +0100 Subject: [PATCH 20/21] Corrected PSR-4 autoloading root --- src/AzureBlobStorage/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureBlobStorage/composer.json b/src/AzureBlobStorage/composer.json index 76cc1e128..ab26d8a3a 100644 --- a/src/AzureBlobStorage/composer.json +++ b/src/AzureBlobStorage/composer.json @@ -2,7 +2,7 @@ "name": "league/flysystem-azure-blob-storage", "autoload": { "psr-4": { - "League\\Flysystem\\AzureBlobStorage\\": "src/" + "League\\Flysystem\\AzureBlobStorage\\": "" } }, "require": { From c48fdc79de4b4f24311b0f07c1d713cda20cae4b Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Mon, 17 Jan 2022 22:17:13 +0100 Subject: [PATCH 21/21] Add funding info --- src/AzureBlobStorage/.github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/AzureBlobStorage/.github/FUNDING.yml diff --git a/src/AzureBlobStorage/.github/FUNDING.yml b/src/AzureBlobStorage/.github/FUNDING.yml new file mode 100644 index 000000000..39b084a29 --- /dev/null +++ b/src/AzureBlobStorage/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: [frankdejonge] +tidelift: "packagist/league/flysystem" +custom: "https://offset.earth/frankdejonge"