From d024be6de0324f44131de8209d25d8cba6ec800b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20H=C3=A9lias?= Date: Sat, 2 Sep 2023 14:57:03 +0200 Subject: [PATCH] feat: add read only support --- composer.json | 1 + docs/1-getting-started.md | 34 ++++++++++++++++++- docs/B-configuration-reference.md | 20 +++++++---- src/DependencyInjection/Configuration.php | 1 + .../FlysystemExtension.php | 34 ++++++++++++++++--- .../FlysystemExtensionTest.php | 14 ++++++++ tests/Kernel/EmptyAppKernel.php | 2 +- tests/Kernel/FlysystemAppKernel.php | 2 +- tests/Kernel/FrameworkAppKernel.php | 2 +- tests/Kernel/config/flysystem.yaml | 6 ++++ tests/Kernel/config/services.yaml | 1 + 11 files changed, 102 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 232f94b..cb506ec 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "league/flysystem-ftp": "^3.1", "league/flysystem-google-cloud-storage": "^3.1", "league/flysystem-memory": "^3.1", + "league/flysystem-read-only": "^3.15", "league/flysystem-sftp-v3": "^3.1", "symfony/dotenv": "^5.4|^6.0", "symfony/framework-bundle": "^5.4|^6.0", diff --git a/docs/1-getting-started.md b/docs/1-getting-started.md index a774dc1..c4a2b53 100644 --- a/docs/1-getting-started.md +++ b/docs/1-getting-started.md @@ -137,7 +137,13 @@ it gives you to swap the actual implementation during tests. More specifically, it can be useful to swap from a persisted storage to a memory one during tests, both to ensure the state is reset between tests and to increase tests speed. -To achieve this, you can overwrite your storages in the test environment: +To achieve this, you need to install the memory provider: + +``` +composer require league/flysystem-memory +``` + +Then, you can overwrite your storages in the test environment: ```yaml # config/packages/flysystem.yaml @@ -162,6 +168,32 @@ flysystem: This configuration will swap every reference to the `users.storage` service (or to the `FilesystemOperator $usersStorage` typehint) from a local adapter to a memory one during tests. +## Using read only to disallow any write operations + +In some context, it can be useful to protect any write operations on the storage service. + +To achieve this, you need to install the read-only package : + +``` +composer require league/flysystem-read-only +``` + +And then, you can configure the storage with the `readonly` options. + +```yaml +# config/packages/flysystem.yaml + +flysystem: + storages: + users.storage: + adapter: 'local' + options: + directory: '%kernel.project_dir%/storage/users' + readonly: true +``` + +With this configuration, any write operation will throw a suitable exception + ## Next [Cloud storage providers](https://github.com/thephpleague/flysystem-bundle/blob/master/docs/2-cloud-storage-providers.md) diff --git a/docs/B-configuration-reference.md b/docs/B-configuration-reference.md index dab8483..8abf782 100644 --- a/docs/B-configuration-reference.md +++ b/docs/B-configuration-reference.md @@ -11,11 +11,11 @@ flysystem: prefix: 'optional/path/prefix' users2.storage: - adapter: 'azure' - options: - client: 'azure_client_service' - container: 'container_name' - prefix: 'optional/path/prefix' + adapter: 'azure' + options: + client: 'azure_client_service' + container: 'container_name' + prefix: 'optional/path/prefix' users3.storage: adapter: 'ftp' @@ -78,5 +78,13 @@ flysystem: source: 'flysystem_storage_service_to_use' users10.storage: - adapter: 'custom_adapter' + adapter: 'custom_adapter' + + users11.storage: + adapter: 'local' + options: + directory: '/tmp/storage' + public_url_generator: 'flysystem_public_url_generator_service_to_use' + temporary_url_generator: 'flysystem_temporary_url_generator_service_to_use' + read_only: true ``` diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 67fd47d..647fecd 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -51,6 +51,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->scalarNode('public_url_generator')->defaultNull()->end() ->scalarNode('temporary_url_generator')->defaultNull()->end() + ->booleanNode('read_only')->defaultFalse()->end() ->end() ->end() ->defaultValue([]) diff --git a/src/DependencyInjection/FlysystemExtension.php b/src/DependencyInjection/FlysystemExtension.php index a1bfaef..403682f 100644 --- a/src/DependencyInjection/FlysystemExtension.php +++ b/src/DependencyInjection/FlysystemExtension.php @@ -15,7 +15,9 @@ use League\Flysystem\FilesystemOperator; use League\Flysystem\FilesystemReader; use League\Flysystem\FilesystemWriter; +use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter; use League\FlysystemBundle\Adapter\AdapterDefinitionFactory; +use League\FlysystemBundle\Exception\MissingPackageException; use League\FlysystemBundle\Lazy\LazyFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -63,16 +65,29 @@ private function createStoragesDefinitions(array $config, ContainerBuilder $cont // Create adapter definition if ($adapter = $definitionFactory->createDefinition($storageConfig['adapter'], $storageConfig['options'])) { // Native adapter - $container->setDefinition('flysystem.adapter.'.$storageName, $adapter)->setPublic(false); + $container->setDefinition($id = 'flysystem.adapter.'.$storageName, $adapter)->setPublic(false); } else { // Custom adapter - $container->setAlias('flysystem.adapter.'.$storageName, $storageConfig['adapter'])->setPublic(false); + $container->setAlias($id = 'flysystem.adapter.'.$storageName, $storageConfig['adapter'])->setPublic(false); + } + + // Create ReadOnly adapter + if ($storageConfig['read_only']) { + if (!class_exists(ReadOnlyFilesystemAdapter::class)) { + throw new MissingPackageException("Missing package, to use the readonly option, run:\n\ncomposer require league/flysystem-read-only"); + } + + $originalAdapterId = $id; + $container->setDefinition( + $id = $id.'.read_only', + $this->createReadOnlyAdapterDefinition(new Reference($originalAdapterId)) + ); } // Create storage definition $container->setDefinition( $storageName, - $this->createStorageDefinition($storageName, new Reference('flysystem.adapter.'.$storageName), $storageConfig) + $this->createStorageDefinition($storageName, new Reference($id), $storageConfig) ); // Register named autowiring alias @@ -82,7 +97,7 @@ private function createStoragesDefinitions(array $config, ContainerBuilder $cont } } - private function createLazyStorageDefinition(string $storageName, array $options) + private function createLazyStorageDefinition(string $storageName, array $options): Definition { $resolver = new OptionsResolver(); $resolver->setRequired('source'); @@ -98,7 +113,7 @@ private function createLazyStorageDefinition(string $storageName, array $options return $definition; } - private function createStorageDefinition(string $storageName, Reference $adapter, array $config) + private function createStorageDefinition(string $storageName, Reference $adapter, array $config): Definition { $publicUrl = null; if ($config['public_url']) { @@ -122,4 +137,13 @@ private function createStorageDefinition(string $storageName, Reference $adapter return $definition; } + + private function createReadOnlyAdapterDefinition(Reference $adapter): Definition + { + $definition = new Definition(ReadOnlyFilesystemAdapter::class); + $definition->setPublic(false); + $definition->setArgument(0, $adapter); + + return $definition; + } } diff --git a/tests/DependencyInjection/FlysystemExtensionTest.php b/tests/DependencyInjection/FlysystemExtensionTest.php index 8f3eee7..fa4538e 100644 --- a/tests/DependencyInjection/FlysystemExtensionTest.php +++ b/tests/DependencyInjection/FlysystemExtensionTest.php @@ -16,6 +16,7 @@ use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\StorageClient; use League\Flysystem\FilesystemOperator; +use League\Flysystem\UnableToWriteFile; use MicrosoftAzure\Storage\Blob\BlobRestProxy; use PHPUnit\Framework\TestCase; use Symfony\Component\Dotenv\Dotenv; @@ -104,6 +105,19 @@ public function testUrlGenerators() self::assertSame('https://example.org/temporary/test1.txt?expiresAt=1670846026', $fs->temporaryUrl('test1.txt', new \DateTimeImmutable('@1670846026'))); } + public function testReadOnly() + { + $kernel = $this->createFysystemKernel(); + $container = $kernel->getContainer()->get('test.service_container'); + + $fs = $container->get('flysystem.test.fs_read_only'); + + $this->expectException(UnableToWriteFile::class); + $this->expectExceptionMessage('Unable to write file at location: path/to/file. This is a readonly adapter.'); + + $fs->write('/path/to/file', 'Unable to write in read only'); + } + private function createFysystemKernel() { (new Dotenv())->populate([ diff --git a/tests/Kernel/EmptyAppKernel.php b/tests/Kernel/EmptyAppKernel.php index 7ff0dd8..05c2010 100644 --- a/tests/Kernel/EmptyAppKernel.php +++ b/tests/Kernel/EmptyAppKernel.php @@ -25,7 +25,7 @@ public function registerBundles(): iterable return [new FlysystemBundle()]; } - public function registerContainerConfiguration(LoaderInterface $loader) + public function registerContainerConfiguration(LoaderInterface $loader): void { $loader->load(function (ContainerBuilder $container) { $container->loadFromExtension('flysystem', [ diff --git a/tests/Kernel/FlysystemAppKernel.php b/tests/Kernel/FlysystemAppKernel.php index 3b05dfc..8074b51 100644 --- a/tests/Kernel/FlysystemAppKernel.php +++ b/tests/Kernel/FlysystemAppKernel.php @@ -29,7 +29,7 @@ public function registerBundles(): iterable return [new FrameworkBundle(), new FlysystemBundle()]; } - public function registerContainerConfiguration(LoaderInterface $loader) + public function registerContainerConfiguration(LoaderInterface $loader): void { $adapterClients = $this->adapterClients; diff --git a/tests/Kernel/FrameworkAppKernel.php b/tests/Kernel/FrameworkAppKernel.php index bb9d0fa..042f75d 100644 --- a/tests/Kernel/FrameworkAppKernel.php +++ b/tests/Kernel/FrameworkAppKernel.php @@ -26,7 +26,7 @@ public function registerBundles(): iterable return [new FrameworkBundle(), new FlysystemBundle()]; } - public function registerContainerConfiguration(LoaderInterface $loader) + public function registerContainerConfiguration(LoaderInterface $loader): void { $loader->load(function (ContainerBuilder $container) { $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]); diff --git a/tests/Kernel/config/flysystem.yaml b/tests/Kernel/config/flysystem.yaml index ea54bb0..7b990f3 100644 --- a/tests/Kernel/config/flysystem.yaml +++ b/tests/Kernel/config/flysystem.yaml @@ -98,3 +98,9 @@ flysystem: directory: '/tmp/storage' public_url_generator: 'flysystem.test.public_url_generator' temporary_url_generator: 'flysystem.test.temporary_url_generator' + + fs_read_only: + adapter: 'local' + options: + directory: '/tmp/storage' + read_only: true diff --git a/tests/Kernel/config/services.yaml b/tests/Kernel/config/services.yaml index af53ff8..ccf7610 100644 --- a/tests/Kernel/config/services.yaml +++ b/tests/Kernel/config/services.yaml @@ -25,3 +25,4 @@ services: flysystem.test.fs_public_url: { alias: 'fs_public_url' } flysystem.test.fs_public_urls: { alias: 'fs_public_urls' } flysystem.test.fs_url_generator: { alias: 'fs_url_generator' } + flysystem.test.fs_read_only: { alias: 'fs_read_only' }