From 49dd91a9d6b5373198a432816d9334bd1deba3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 30 May 2024 18:54:56 +0200 Subject: [PATCH 1/8] GridFS adapter for Filesystem --- composer.json | 2 + src/MongoDBServiceProvider.php | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/composer.json b/composer.json index 84229b00f..ce3ea7e49 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,8 @@ }, "require-dev": { "mongodb/builder": "^0.2", + "league/flysystem-gridfs": "^3.28", + "league/flysystem-read-only": "^3.0", "phpunit/phpunit": "^10.3", "orchestra/testbench": "^8.0|^9.0", "mockery/mockery": "^1.4.4", diff --git a/src/MongoDBServiceProvider.php b/src/MongoDBServiceProvider.php index 50c042230..376561729 100644 --- a/src/MongoDBServiceProvider.php +++ b/src/MongoDBServiceProvider.php @@ -6,13 +6,27 @@ use Illuminate\Cache\CacheManager; use Illuminate\Cache\Repository; +use Illuminate\Filesystem\FilesystemAdapter; +use Illuminate\Filesystem\FilesystemManager; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; +use InvalidArgumentException; +use League\Flysystem\Filesystem; +use League\Flysystem\GridFS\GridFSAdapter; +use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter; +use MongoDB\Client; +use MongoDB\GridFS\Bucket; use MongoDB\Laravel\Cache\MongoStore; use MongoDB\Laravel\Eloquent\Model; use MongoDB\Laravel\Queue\MongoConnector; +use RuntimeException; +use function array_keys; use function assert; +use function class_exists; +use function implode; +use function is_string; +use function sprintf; class MongoDBServiceProvider extends ServiceProvider { @@ -66,5 +80,74 @@ public function register() return new MongoConnector($this->app['db']); }); }); + + $this->registerFlysystemAdapter(); + } + + private function registerFlysystemAdapter(): void + { + // GridFS adapter for filesystem + $this->app->resolving('filesystem', static function (FilesystemManager $filesystemManager) { + $filesystemManager->extend('gridfs', static function (Application $app, array $config) { + if (! class_exists(GridFSAdapter::class)) { + throw new RuntimeException('GridFS adapter for Flysystem is missing. Try running "composer require league/flysystem-gridfs"'); + } + + // Reuse an existing database connection + if (isset($config['connection'])) { + if (isset($config['mongodb_uri'])) { + throw new InvalidArgumentException('In GridFS configuration, "connection" and "mongodb_uri" options cannot be set together.'); + } + + $connection = $app['db']->connection($config['connection']); + if (! $connection instanceof Connection) { + throw new InvalidArgumentException(sprintf('The database connection "%s" does not use the "mongodb" driver.', $connection['connection'])); + } + + $bucket = $connection->getMongoClient() + ->selectDatabase($config['database'] ?? $connection->getDatabaseName()) + ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs']); + } elseif (isset($config['mongodb_uri'])) { + // Create a new MongoDB client from the config + if (! isset($config['database'])) { + throw new InvalidArgumentException('MongoDB "database" name is required for filesystem GridFS configuration'); + } + + $bucket = (new Client($config['mongodb_uri'], $config['mongodb_uri_options'] ?? [], $config['mongodb_driver_options'] ?? [])) + ->selectDatabase($config['database']) + ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs']); + } elseif (isset($config['bucket'])) { + // Allows setting the bucket instance directly + $bucket = $config['bucket']; + if (is_string($bucket)) { + // Resolves the "bucket" service + $bucket = $app->get($bucket); + } elseif ($bucket instanceof \Closure) { + $bucket = $bucket($app, $config); + } + + if (! $bucket instanceof Bucket) { + throw new InvalidArgumentException(sprintf('Provided GridFS bucket is not a instance of "%s"', Bucket::class)); + } + } + + if (! isset($bucket)) { + throw new InvalidArgumentException(sprintf('The "gridfs" configuration requires the "connection", "mongodb_uri", or "bucket" option. Got "%s"', implode('", "', array_keys($config)))); + } + + $adapter = new GridFSAdapter($bucket, $config['prefix'] ?? ''); + + /** @see FilesystemManager::createFlysystem() */ + if ($config['read-only'] ?? false) { + if (! class_exists(ReadOnlyFilesystemAdapter::class)) { + throw new RuntimeException('Read-only Adapter for Flysystem is missing. Try running "composer require league/flysystem-read-only"'); + } + + $adapter = new ReadOnlyFilesystemAdapter($adapter); + } + + return new FilesystemAdapter(new Filesystem($adapter, $config), $adapter, $config); + }); + }); } } From 59129f9dd204967113a36b914efe087d08bff923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 30 May 2024 22:28:25 +0200 Subject: [PATCH 2/8] Add tests and remove Client instanciation from Configuration --- src/MongoDBServiceProvider.php | 18 ++----- tests/FilesystemsTest.php | 99 ++++++++++++++++++++++++++++++++++ tests/config/filesystems.php | 32 +++++++++++ 3 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 tests/FilesystemsTest.php create mode 100644 tests/config/filesystems.php diff --git a/src/MongoDBServiceProvider.php b/src/MongoDBServiceProvider.php index 376561729..f4d045716 100644 --- a/src/MongoDBServiceProvider.php +++ b/src/MongoDBServiceProvider.php @@ -4,6 +4,7 @@ namespace MongoDB\Laravel; +use Closure; use Illuminate\Cache\CacheManager; use Illuminate\Cache\Repository; use Illuminate\Filesystem\FilesystemAdapter; @@ -14,7 +15,6 @@ use League\Flysystem\Filesystem; use League\Flysystem\GridFS\GridFSAdapter; use League\Flysystem\ReadOnly\ReadOnlyFilesystemAdapter; -use MongoDB\Client; use MongoDB\GridFS\Bucket; use MongoDB\Laravel\Cache\MongoStore; use MongoDB\Laravel\Eloquent\Model; @@ -106,23 +106,15 @@ private function registerFlysystemAdapter(): void $bucket = $connection->getMongoClient() ->selectDatabase($config['database'] ?? $connection->getDatabaseName()) - ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs']); - } elseif (isset($config['mongodb_uri'])) { - // Create a new MongoDB client from the config - if (! isset($config['database'])) { - throw new InvalidArgumentException('MongoDB "database" name is required for filesystem GridFS configuration'); - } + ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs', 'disableMD5' => true]); - $bucket = (new Client($config['mongodb_uri'], $config['mongodb_uri_options'] ?? [], $config['mongodb_driver_options'] ?? [])) - ->selectDatabase($config['database']) - ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs']); - } elseif (isset($config['bucket'])) { // Allows setting the bucket instance directly + } elseif (isset($config['bucket'])) { $bucket = $config['bucket']; + // Resolves the "bucket" service if (is_string($bucket)) { - // Resolves the "bucket" service $bucket = $app->get($bucket); - } elseif ($bucket instanceof \Closure) { + } elseif ($bucket instanceof Closure) { $bucket = $bucket($app, $config); } diff --git a/tests/FilesystemsTest.php b/tests/FilesystemsTest.php new file mode 100644 index 000000000..a0c399ba4 --- /dev/null +++ b/tests/FilesystemsTest.php @@ -0,0 +1,99 @@ +getMongoDB()->drop(); + + parent::tearDown(); + } + + public static function provideDiskConfigurations(): Generator + { + yield [ + 'gridfs-connection-minimal', + [ + 'driver' => 'gridfs', + 'connection' => 'mongodb', + ], + ]; + + yield [ + 'gridfs-connection-full', + [ + 'driver' => 'gridfs', + 'connection' => 'mongodb', + 'database' => env('MONGODB_DATABASE', 'unittest'), + 'bucket' => 'fs', + 'prefix' => 'foo/', + ], + ]; + + yield [ + 'gridfs-bucket-service', + [ + 'driver' => 'gridfs', + 'bucket' => 'bucket', + ], + ]; + + yield [ + 'gridfs-bucket-factory', + [ + 'driver' => 'gridfs', + 'bucket' => static fn (Application $app) => $app['db'] + ->connection('mongodb') + ->getMongoDB() + ->selectGridFSBucket(), + ], + ]; + } + + #[DataProvider('provideDiskConfigurations')] + public function testConfig(string $name, array $options) + { + // Service used by "gridfs-bucket-service" + $this->app->singleton('bucket', static fn (Application $app) => $app['db'] + ->connection('mongodb') + ->getMongoDB() + ->selectGridFSBucket()); + + $this->app['config']->set('filesystems.disks.' . $name, $options); + + $disk = Storage::disk($name); + $disk->put($filename = $name . '.txt', $value = microtime()); + $this->assertEquals($value, $disk->get($filename), 'File saved'); + } + + public function testReadOnlyAndThrowOption() + { + $this->app['config']->set('filesystems.disks.gridfs-readonly', [ + 'driver' => 'gridfs', + 'connection' => 'mongodb', + // Use ReadOnlyAdapter + 'read-only' => true, + // Throw exceptions + 'throw' => true, + ]); + + $disk = Storage::disk('gridfs-readonly'); + + $this->expectException(UnableToWriteFile::class); + $this->expectExceptionMessage('This is a readonly adapter.'); + + $disk->put('file.txt', ''); + } +} diff --git a/tests/config/filesystems.php b/tests/config/filesystems.php new file mode 100644 index 000000000..03c67a45b --- /dev/null +++ b/tests/config/filesystems.php @@ -0,0 +1,32 @@ + [ + 'gridfs-connection-minimal' => [ + 'driver' => 'gridfs', + 'connection' => 'mongodb', + ], + + 'gridfs-connection-full' => [ + 'driver' => 'gridfs', + 'connection' => 'mongodb', + ], + + 'gridfs-bucket-service' => [ + 'driver' => 'gridfs', + 'bucket' => 'bucket', + ], + + 'gridfs-bucket-factory' => [ + 'driver' => 'gridfs', + 'bucket' => static fn (Application $app) => $app['db'] + ->connection('mongodb') + ->getMongoDB() + ->selectGridFSBucket(), + ], + ], +]; From 04e9c8612bc3a6ba489fe5e01f7451532d405434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 May 2024 00:23:46 +0200 Subject: [PATCH 3/8] Add docs --- CHANGELOG.md | 1 + docs/filesystems.txt | 144 +++++++++++++++++++++++++++++++++ src/MongoDBServiceProvider.php | 46 +++++------ tests/FilesystemsTest.php | 83 +++++++++++++++---- 4 files changed, 232 insertions(+), 42 deletions(-) create mode 100644 docs/filesystems.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index d8626f27a..9e6ff4b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. * Rename queue option `table` to `collection` * Replace queue option `expire` with `retry_after` * Revert behavior of `createOrFirst` to delegate to `firstOrCreate` when in transaction by @GromNaN in [#2984](https://github.com/mongodb/laravel-mongodb/pull/2984) +* Add GridFS integration for Laravel File Storage by @GromNaN in [#2984](https://github.com/mongodb/laravel-mongodb/pull/2985) ## [4.3.1] - 2024-05-31 diff --git a/docs/filesystems.txt b/docs/filesystems.txt new file mode 100644 index 000000000..35d5a1668 --- /dev/null +++ b/docs/filesystems.txt @@ -0,0 +1,144 @@ +.. _laravel-queues: + +====== +Queues +====== + +.. facet:: + :name: genre + :values: tutorial + +.. meta:: + :keywords: php framework, gridfs, code example + +Overview +-------- + +Using MongoDB to store files can be done using the +`GridFS Adapter for Flysystem `__. +GridFS lets you store files of unlimited size in the same database as your data. +This ensures the integrity of transactions and backups. + + +Configuration +------------- + +Before using the GridFS driver, you will need to install the Flysystem GridFS package via the Composer package manager: + +.. code-block:: bash + + composer require league/flysystem-gridfs + +Configure `Laravel File Storage `__, +to use the ``gridfs`` driver in ``config/filesystems.php``: + +.. code-block:: php + + 'disks' => [ + 'gridfs' => [ + 'driver' => 'gridfs', + 'connection' => 'mongodb', + 'database' => 'files', + 'bucket' => 'fs', + 'prefix' => '', + 'read-only' => false, + 'throw' => false, + ], + ], + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Setting + - Description + + * - ``driver`` + - **Required**. Specifies the filesystem driver to use. Must be ``gridfs`` for MongoDB. + + * - ``connection`` + - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. + + * - ``database`` + - Name of the MongoDB database fot the GridFS bucket. Use the database of the connection if not specified. + + * - ``bucket`` + - Name of the GridFS bucket. A database can contain multiple buckets identified by their name. Defaults to ``fs``. + + * - ``prefix`` + - Specifies a prefix for the name of the files that are stored in the bucket. Using a distinct bucket is recommended + in order to store the files in a different collection. + + * - ``read-only`` + - If ``true``, writing to the GridFS bucket is disabled. Write operations will return ``false`` or throw exceptions + depending on the configuration of ``throw``. Defaults to ``false``. + + * - ``throw`` + - If ``true``, exceptions are thrown when an operation cannot be performed. Defaults to ``false``, the operations + return ``true`` in case of succes, ``false`` in case of error. + +If you have specific requirements, you can use a factory or a service name to define the bucket. The options +``connection`` and ``database`` are ignored when a ``MongoDB\GridFS\Bucket`` is provided: + +.. code-block:: php + + use Illuminate\Foundation\Application; + use MongoDB\GridFS\Bucket; + + 'disks' => [ + 'gridfs' => [ + 'driver' => 'gridfs', + 'bucket' => static function (Application $app): Bucket { + return $app['db']->connection('mongodb') + ->getMongoDB() + ->selectGridFSBucket([ + 'bucketName' => 'avatars', + 'chunkSizeBytes' => 261120, + ]); + }, + ], + ], + +Usage +----- + +The benefits of using Laravel File Storage facade, is that it provides a common +interface for all the supported file systems. Use the ``gridfs`` disk in the +same way as the ``local`` disk. + +.. code-block:: php + + $disk = Storage::disk('gridfs'); + + // Write the file "hello.txt" into GridFS + $disk->put('hello.txt', 'Hello World!'); + + // Read the file + echo $disk->get('hello.txt'); // Hello World! + +To learn more Laravel File Storage, see +`Laravel File Storage `__. + +Versioning +---------- + +In GridFS, file names are metadata to file objects identified by unique MongoDB ObjectID. +There may be more than one file with the same name, they are called "revisions": + +- Reading a file reads the last revision of this file name +- Writing to a file name creates a new revision for this file name +- Renaming a file renames all the revisions of this file name +- Deleting a file deletes all the revisions of this file name + +The GridFS Adapter for Flysystem does not provide access to a specific revision +of a filename, you must use the :manual:`GridFS API ` if you +need to work with revisions. + +.. code-block:: php + + // Create a MongoDB Bucket service from the MongoDB connection + /** @var \MongoDB\GridFS\Bucket $bucket */ + $bucket = $app['db']->connection('mongodb')->getMongoDB()->selectGridFSBucket(); + + // Download the last but one version of a file + $bucket->openDownloadStreamByName('hello.txt', ['revision' => -2]) diff --git a/src/MongoDBServiceProvider.php b/src/MongoDBServiceProvider.php index f4d045716..e1956eb42 100644 --- a/src/MongoDBServiceProvider.php +++ b/src/MongoDBServiceProvider.php @@ -21,10 +21,9 @@ use MongoDB\Laravel\Queue\MongoConnector; use RuntimeException; -use function array_keys; use function assert; use function class_exists; -use function implode; +use function get_debug_type; use function is_string; use function sprintf; @@ -93,38 +92,30 @@ private function registerFlysystemAdapter(): void throw new RuntimeException('GridFS adapter for Flysystem is missing. Try running "composer require league/flysystem-gridfs"'); } - // Reuse an existing database connection - if (isset($config['connection'])) { - if (isset($config['mongodb_uri'])) { - throw new InvalidArgumentException('In GridFS configuration, "connection" and "mongodb_uri" options cannot be set together.'); - } + $bucket = $config['bucket'] ?? null; + + // Get the bucket from a factory function + if ($bucket instanceof Closure) { + $bucket = $bucket($app, $config); + } + // Get the bucket from a service + if (is_string($bucket) && $app->has($bucket)) { + $bucket = $app->get($bucket); + } + + // Get the bucket from the database connection + if (is_string($bucket) || $bucket === null) { $connection = $app['db']->connection($config['connection']); if (! $connection instanceof Connection) { - throw new InvalidArgumentException(sprintf('The database connection "%s" does not use the "mongodb" driver.', $connection['connection'])); + throw new InvalidArgumentException(sprintf('The database connection "%s" does not use the "mongodb" driver.', $config['connection'] ?? $app['config']['database.default'])); } $bucket = $connection->getMongoClient() ->selectDatabase($config['database'] ?? $connection->getDatabaseName()) ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs', 'disableMD5' => true]); - - // Allows setting the bucket instance directly - } elseif (isset($config['bucket'])) { - $bucket = $config['bucket']; - // Resolves the "bucket" service - if (is_string($bucket)) { - $bucket = $app->get($bucket); - } elseif ($bucket instanceof Closure) { - $bucket = $bucket($app, $config); - } - - if (! $bucket instanceof Bucket) { - throw new InvalidArgumentException(sprintf('Provided GridFS bucket is not a instance of "%s"', Bucket::class)); - } - } - - if (! isset($bucket)) { - throw new InvalidArgumentException(sprintf('The "gridfs" configuration requires the "connection", "mongodb_uri", or "bucket" option. Got "%s"', implode('", "', array_keys($config)))); + } elseif (! $bucket instanceof Bucket) { + throw new InvalidArgumentException(sprintf('Unexpected value for GridFS "bucket" configuration. Expecting "%s". Got "%s"', Bucket::class, get_debug_type($bucket))); } $adapter = new GridFSAdapter($bucket, $config['prefix'] ?? ''); @@ -138,6 +129,9 @@ private function registerFlysystemAdapter(): void $adapter = new ReadOnlyFilesystemAdapter($adapter); } + /** Prevent using backslash on Windows in {@see FilesystemAdapter::__construct()} */ + $config['directory_separator'] = '/'; + return new FilesystemAdapter(new Filesystem($adapter, $config), $adapter, $config); }); }); diff --git a/tests/FilesystemsTest.php b/tests/FilesystemsTest.php index a0c399ba4..b59c02c82 100644 --- a/tests/FilesystemsTest.php +++ b/tests/FilesystemsTest.php @@ -6,33 +6,35 @@ use Illuminate\Foundation\Application; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; +use InvalidArgumentException; use League\Flysystem\UnableToWriteFile; +use MongoDB\GridFS\Bucket; use PHPUnit\Framework\Attributes\DataProvider; +use stdClass; use function env; use function microtime; +use function stream_get_contents; class FilesystemsTest extends TestCase { public function tearDown(): void { - DB::connection('mongodb')->getMongoDB()->drop(); + $this->getBucket()->drop(); parent::tearDown(); } - public static function provideDiskConfigurations(): Generator + public static function provideValidOptions(): Generator { - yield [ - 'gridfs-connection-minimal', + yield 'connection-minimal' => [ [ 'driver' => 'gridfs', 'connection' => 'mongodb', ], ]; - yield [ - 'gridfs-connection-full', + yield 'connection-full' => [ [ 'driver' => 'gridfs', 'connection' => 'mongodb', @@ -42,16 +44,14 @@ public static function provideDiskConfigurations(): Generator ], ]; - yield [ - 'gridfs-bucket-service', + yield 'bucket-service' => [ [ 'driver' => 'gridfs', 'bucket' => 'bucket', ], ]; - yield [ - 'gridfs-bucket-factory', + yield 'bucket-factory' => [ [ 'driver' => 'gridfs', 'bucket' => static fn (Application $app) => $app['db'] @@ -62,22 +62,54 @@ public static function provideDiskConfigurations(): Generator ]; } - #[DataProvider('provideDiskConfigurations')] - public function testConfig(string $name, array $options) + #[DataProvider('provideValidOptions')] + public function testValidOptions(array $options) { - // Service used by "gridfs-bucket-service" + // Service used by "bucket-service" $this->app->singleton('bucket', static fn (Application $app) => $app['db'] ->connection('mongodb') ->getMongoDB() ->selectGridFSBucket()); - $this->app['config']->set('filesystems.disks.' . $name, $options); + $this->app['config']->set('filesystems.disks.' . $this->dataName(), $options); - $disk = Storage::disk($name); - $disk->put($filename = $name . '.txt', $value = microtime()); + $disk = Storage::disk($this->dataName()); + $disk->put($filename = $this->dataName() . '.txt', $value = microtime()); $this->assertEquals($value, $disk->get($filename), 'File saved'); } + public static function provideInvalidOptions(): Generator + { + yield 'not-mongodb-connection' => [ + ['driver' => 'gridfs', 'connection' => 'sqlite'], + 'The database connection "sqlite" does not use the "mongodb" driver.', + ]; + + yield 'factory-not-bucket' => [ + ['driver' => 'gridfs', 'bucket' => static fn () => new stdClass()], + 'Unexpected value for GridFS "bucket" configuration. Expecting "MongoDB\GridFS\Bucket". Got "stdClass"', + ]; + + yield 'service-not-bucket' => [ + ['driver' => 'gridfs', 'bucket' => 'bucket'], + 'Unexpected value for GridFS "bucket" configuration. Expecting "MongoDB\GridFS\Bucket". Got "stdClass"', + ]; + } + + #[DataProvider('provideInvalidOptions')] + public function testInvalidOptions(array $options, string $message) + { + // Service used by "service-not-bucket" + $this->app->singleton('bucket', static fn () => new stdClass()); + + $this->app['config']->set('filesystems.disks.' . $this->dataName(), $options); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + Storage::disk($this->dataName()); + } + public function testReadOnlyAndThrowOption() { $this->app['config']->set('filesystems.disks.gridfs-readonly', [ @@ -96,4 +128,23 @@ public function testReadOnlyAndThrowOption() $disk->put('file.txt', ''); } + + public function testPrefix() + { + $this->app['config']->set('filesystems.disks.gridfs-prefix', [ + 'driver' => 'gridfs', + 'connection' => 'mongodb', + 'prefix' => 'foo/bar/', + ]); + + $disk = Storage::disk('gridfs-prefix'); + $disk->put('hello/world.txt', 'Hello World!'); + $this->assertSame('Hello World!', $disk->get('hello/world.txt')); + $this->assertSame('Hello World!', stream_get_contents($this->getBucket()->openDownloadStreamByName('foo/bar/hello/world.txt')), 'File name is prefixed in the bucket'); + } + + private function getBucket(): Bucket + { + return DB::connection('mongodb')->getMongoDB()->selectGridFSBucket(); + } } From 0780822cc0034ac35c53d9dba1accb848fc24555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 31 May 2024 00:33:28 +0200 Subject: [PATCH 4/8] Remove file --- CHANGELOG.md | 5 ++++- docs/filesystems.txt | 2 +- tests/config/filesystems.php | 32 -------------------------------- 3 files changed, 5 insertions(+), 34 deletions(-) delete mode 100644 tests/config/filesystems.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e6ff4b6e..0345701b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [4.5.0] - upcoming + +* Add GridFS integration for Laravel File Storage by @GromNaN in [#2984](https://github.com/mongodb/laravel-mongodb/pull/2985) + ## [4.4.0] - 2024-05-31 * Support collection name prefix by @GromNaN in [#2930](https://github.com/mongodb/laravel-mongodb/pull/2930) @@ -9,7 +13,6 @@ All notable changes to this project will be documented in this file. * Rename queue option `table` to `collection` * Replace queue option `expire` with `retry_after` * Revert behavior of `createOrFirst` to delegate to `firstOrCreate` when in transaction by @GromNaN in [#2984](https://github.com/mongodb/laravel-mongodb/pull/2984) -* Add GridFS integration for Laravel File Storage by @GromNaN in [#2984](https://github.com/mongodb/laravel-mongodb/pull/2985) ## [4.3.1] - 2024-05-31 diff --git a/docs/filesystems.txt b/docs/filesystems.txt index 35d5a1668..9bab50d9a 100644 --- a/docs/filesystems.txt +++ b/docs/filesystems.txt @@ -136,7 +136,7 @@ need to work with revisions. .. code-block:: php - // Create a MongoDB Bucket service from the MongoDB connection + // Create a bucket service from the MongoDB connection /** @var \MongoDB\GridFS\Bucket $bucket */ $bucket = $app['db']->connection('mongodb')->getMongoDB()->selectGridFSBucket(); diff --git a/tests/config/filesystems.php b/tests/config/filesystems.php deleted file mode 100644 index 03c67a45b..000000000 --- a/tests/config/filesystems.php +++ /dev/null @@ -1,32 +0,0 @@ - [ - 'gridfs-connection-minimal' => [ - 'driver' => 'gridfs', - 'connection' => 'mongodb', - ], - - 'gridfs-connection-full' => [ - 'driver' => 'gridfs', - 'connection' => 'mongodb', - ], - - 'gridfs-bucket-service' => [ - 'driver' => 'gridfs', - 'bucket' => 'bucket', - ], - - 'gridfs-bucket-factory' => [ - 'driver' => 'gridfs', - 'bucket' => static fn (Application $app) => $app['db'] - ->connection('mongodb') - ->getMongoDB() - ->selectGridFSBucket(), - ], - ], -]; From 9c7b6ba6be346898be28452ce6d2b0b4f6ccedc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 3 Jun 2024 11:53:52 +0200 Subject: [PATCH 5/8] Docs review --- composer.json | 1 + docs/filesystems.txt | 62 +++++++++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/composer.json b/composer.json index ce3ea7e49..af060bb3c 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,7 @@ "illuminate/bus": "< 10.37.2" }, "suggest": { + "league/flysystem-gridfs": "Filesystem storage in MongoDB with GridFS", "mongodb/builder": "Provides a fluent aggregation builder for MongoDB pipelines" }, "minimum-stability": "dev", diff --git a/docs/filesystems.txt b/docs/filesystems.txt index 9bab50d9a..8b7c23f7c 100644 --- a/docs/filesystems.txt +++ b/docs/filesystems.txt @@ -1,8 +1,8 @@ -.. _laravel-queues: +.. _laravel-filesystems: -====== -Queues -====== +================== +GridFS Filesystems +================== .. facet:: :name: genre @@ -14,22 +14,23 @@ Queues Overview -------- -Using MongoDB to store files can be done using the -`GridFS Adapter for Flysystem `__. +You can use the +`GridFS Adapter for Flysystem `__ +to store large files in MongoDB. GridFS lets you store files of unlimited size in the same database as your data. -This ensures the integrity of transactions and backups. Configuration ------------- -Before using the GridFS driver, you will need to install the Flysystem GridFS package via the Composer package manager: +Before using the GridFS driver, install the Flysystem GridFS package through the +Composer package manager by running the following command: .. code-block:: bash composer require league/flysystem-gridfs -Configure `Laravel File Storage `__, +Configure `Laravel File Storage `__ to use the ``gridfs`` driver in ``config/filesystems.php``: .. code-block:: php @@ -38,14 +39,11 @@ to use the ``gridfs`` driver in ``config/filesystems.php``: 'gridfs' => [ 'driver' => 'gridfs', 'connection' => 'mongodb', - 'database' => 'files', - 'bucket' => 'fs', - 'prefix' => '', - 'read-only' => false, - 'throw' => false, ], ], +You can configure the disk the following settings in ``config/filesystems.php``: + .. list-table:: :header-rows: 1 :widths: 25 75 @@ -60,10 +58,10 @@ to use the ``gridfs`` driver in ``config/filesystems.php``: - The database connection used to store jobs. It must be a ``mongodb`` connection. The driver uses the default connection if a connection is not specified. * - ``database`` - - Name of the MongoDB database fot the GridFS bucket. Use the database of the connection if not specified. + - Name of the MongoDB database for the GridFS bucket. The driver uses the database of the connection if a database is not specified. * - ``bucket`` - - Name of the GridFS bucket. A database can contain multiple buckets identified by their name. Defaults to ``fs``. + - Name or instance of the GridFS bucket. A database can contain multiple buckets identified by their name. Defaults to ``fs``. * - ``prefix`` - Specifies a prefix for the name of the files that are stored in the bucket. Using a distinct bucket is recommended @@ -74,11 +72,11 @@ to use the ``gridfs`` driver in ``config/filesystems.php``: depending on the configuration of ``throw``. Defaults to ``false``. * - ``throw`` - - If ``true``, exceptions are thrown when an operation cannot be performed. Defaults to ``false``, the operations - return ``true`` in case of succes, ``false`` in case of error. + - If ``true``, exceptions are thrown when an operation cannot be performed. If ``false``, + operations return ``true`` on success and ``false`` on error. Defaults to ``false``. -If you have specific requirements, you can use a factory or a service name to define the bucket. The options -``connection`` and ``database`` are ignored when a ``MongoDB\GridFS\Bucket`` is provided: +You can also use a factory or a service name to create an instance of ``MongoDB\GridFS\Bucket``. +In this case, the options ``connection`` and ``database`` are ignored: .. code-block:: php @@ -102,9 +100,9 @@ If you have specific requirements, you can use a factory or a service name to de Usage ----- -The benefits of using Laravel File Storage facade, is that it provides a common -interface for all the supported file systems. Use the ``gridfs`` disk in the -same way as the ``local`` disk. +A benefit of using Laravel Filesystem is that it provides a common interface +for all the supported file systems. You can use the ``gridfs`` disk in the same +way as the ``local`` disk. .. code-block:: php @@ -117,22 +115,28 @@ same way as the ``local`` disk. echo $disk->get('hello.txt'); // Hello World! To learn more Laravel File Storage, see -`Laravel File Storage `__. +`Laravel File Storage `__ +in the Laravel documentation. Versioning ---------- -In GridFS, file names are metadata to file objects identified by unique MongoDB ObjectID. -There may be more than one file with the same name, they are called "revisions": +In GridFS, file names are metadata in file documents identified by unique +MongoDB ObjectID. If multiple documents share the same file name, they are +considered "revisions" and further distinguished by creation timestamps. + +The Laravel MongoDB integration uses the GridFS Flysystem adapter. It interacts +with file revisions in the following ways: - Reading a file reads the last revision of this file name -- Writing to a file name creates a new revision for this file name +- Writing a file creates a new revision for this file name - Renaming a file renames all the revisions of this file name - Deleting a file deletes all the revisions of this file name The GridFS Adapter for Flysystem does not provide access to a specific revision -of a filename, you must use the :manual:`GridFS API ` if you -need to work with revisions. +of a filename. You must use the +`GridFS API `__ +if you need to work with revisions, as shown in the following code: .. code-block:: php From 38158909b30bfa03d6fdba4413d1f08ab97ed3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 3 Jun 2024 12:05:41 +0200 Subject: [PATCH 6/8] Prevent cascading resolution of the bucket option --- src/MongoDBServiceProvider.php | 18 ++++++++---------- tests/FilesystemsTest.php | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/MongoDBServiceProvider.php b/src/MongoDBServiceProvider.php index e1956eb42..0932048c9 100644 --- a/src/MongoDBServiceProvider.php +++ b/src/MongoDBServiceProvider.php @@ -94,18 +94,14 @@ private function registerFlysystemAdapter(): void $bucket = $config['bucket'] ?? null; - // Get the bucket from a factory function if ($bucket instanceof Closure) { + // Get the bucket from a factory function $bucket = $bucket($app, $config); - } - - // Get the bucket from a service - if (is_string($bucket) && $app->has($bucket)) { + } elseif (is_string($bucket) && $app->has($bucket)) { + // Get the bucket from a service $bucket = $app->get($bucket); - } - - // Get the bucket from the database connection - if (is_string($bucket) || $bucket === null) { + } elseif (is_string($bucket) || $bucket === null) { + // Get the bucket from the database connection $connection = $app['db']->connection($config['connection']); if (! $connection instanceof Connection) { throw new InvalidArgumentException(sprintf('The database connection "%s" does not use the "mongodb" driver.', $config['connection'] ?? $app['config']['database.default'])); @@ -114,7 +110,9 @@ private function registerFlysystemAdapter(): void $bucket = $connection->getMongoClient() ->selectDatabase($config['database'] ?? $connection->getDatabaseName()) ->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs', 'disableMD5' => true]); - } elseif (! $bucket instanceof Bucket) { + } + + if (! $bucket instanceof Bucket) { throw new InvalidArgumentException(sprintf('Unexpected value for GridFS "bucket" configuration. Expecting "%s". Got "%s"', Bucket::class, get_debug_type($bucket))); } diff --git a/tests/FilesystemsTest.php b/tests/FilesystemsTest.php index b59c02c82..3b9fa8e5f 100644 --- a/tests/FilesystemsTest.php +++ b/tests/FilesystemsTest.php @@ -110,7 +110,7 @@ public function testInvalidOptions(array $options, string $message) Storage::disk($this->dataName()); } - public function testReadOnlyAndThrowOption() + public function testReadOnlyAndThrowOptions() { $this->app['config']->set('filesystems.disks.gridfs-readonly', [ 'driver' => 'gridfs', From e633d9a0e2ad9a81d95f981296d7e0b13e85fa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 4 Jun 2024 09:54:30 +0200 Subject: [PATCH 7/8] Docs review --- docs/filesystems.txt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/filesystems.txt b/docs/filesystems.txt index 8b7c23f7c..450a670d1 100644 --- a/docs/filesystems.txt +++ b/docs/filesystems.txt @@ -39,6 +39,11 @@ to use the ``gridfs`` driver in ``config/filesystems.php``: 'gridfs' => [ 'driver' => 'gridfs', 'connection' => 'mongodb', + // 'database' => null, + // 'bucket' => 'fs', + // 'prefix' => '', + // 'read-only' => false, + // 'throw' => false, ], ], @@ -80,8 +85,8 @@ In this case, the options ``connection`` and ``database`` are ignored: .. code-block:: php - use Illuminate\Foundation\Application; - use MongoDB\GridFS\Bucket; + use Illuminate\Foundation\Application; + use MongoDB\GridFS\Bucket; 'disks' => [ 'gridfs' => [ @@ -121,8 +126,8 @@ in the Laravel documentation. Versioning ---------- -In GridFS, file names are metadata in file documents identified by unique -MongoDB ObjectID. If multiple documents share the same file name, they are +File names in GridFS are metadata in file documents, which are identified by +unique ObjectIDs. If multiple documents share the same file name, they are considered "revisions" and further distinguished by creation timestamps. The Laravel MongoDB integration uses the GridFS Flysystem adapter. It interacts From e67b59269d6e43e8f9284d94fda03d00f2413a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 4 Jun 2024 15:17:38 +0200 Subject: [PATCH 8/8] Add note about prefix with gridfs api --- docs/filesystems.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/filesystems.txt b/docs/filesystems.txt index 450a670d1..065b5c08f 100644 --- a/docs/filesystems.txt +++ b/docs/filesystems.txt @@ -70,7 +70,8 @@ You can configure the disk the following settings in ``config/filesystems.php``: * - ``prefix`` - Specifies a prefix for the name of the files that are stored in the bucket. Using a distinct bucket is recommended - in order to store the files in a different collection. + in order to store the files in a different collection, instead of using a prefix. + The prefix should not start with a leading slash ``/``. * - ``read-only`` - If ``true``, writing to the GridFS bucket is disabled. Write operations will return ``false`` or throw exceptions @@ -151,3 +152,8 @@ if you need to work with revisions, as shown in the following code: // Download the last but one version of a file $bucket->openDownloadStreamByName('hello.txt', ['revision' => -2]) + +.. note:: + + If you use a prefix the Filesystem component, you will have to handle it + by yourself when using the GridFS API directly.