Skip to content

PHPORM-186 GridFS adapter for Filesystem #2985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
144 changes: 144 additions & 0 deletions docs/filesystems.txt
Original file line number Diff line number Diff line change
@@ -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 <https://flysystem.thephpleague.com/docs/adapter/gridfs/>`__.
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S: we try to avoid the future tense and "via" in the docs. Also, I'd edit this sentence slightly so it introduces the code example:

Suggested change
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 <https://laravel.com/docs/{+laravel-docs-version+}/filesystem>`__,
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the complete list of supported options, with default values repeated just so users know what they can set? Since you have a table below with all options, I'd consider reducing this code example to just the necessary options.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not, but personally I find the complete example more synthetic and quicker to read, or even copy and paste.
I understand that we want developers to use the default minimal config, so that's what I'm going to keep.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the complete example more synthetic and quicker to read

Same. If this example was annotated with comments to call out default values (and thus assist user's with reducing their own config) I think it'd stand well on its own and we wouldn't even need the table that follows.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added optional settings in comments. Keeping the table for details.

],
],

.. 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S: remove the article to match the other descriptions

Suggested change
- 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 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: what do you mean by "specific requirements"? I think it would be helpful to clarify them here

``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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S: introduce the code

Suggested change
The following example writes a file to the ``gridfs`` disk, then reads the file:

.. 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 <https://laravel.com/docs/{+laravel-docs-version+}/filesystem>`__.

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 </tutorial/gridfs/>` if you
need to work with revisions.

.. code-block:: php

// Create a 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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since PathPrefixer doesn't apply here, would users need to write /hello.txt, or do the prefixes only kick in when a path is used? I expect this could be a pain point for users, that may warrant a note.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use the leading / in the PHP GridFS documentation. I don't know what to write in a note.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no prefixing done in PHPLIB, so the filename is used exactly as provided.

My question here is: would hello.txt here refer to the same file written by Flysystem, or would you need to use /hello.txt? It's not clear to me if Flysystem adds a prefix to every file name or just those that appear to use paths (i.e. already contain a / somewhere in the middle of the string).

Copy link
Member

@jmikola jmikola Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this is answered by #2985 (comment). In that case, I suppose the note would be to remind users that prefixing is entirely handled by Flysystem. So if they're using a non-empty prefix option they'll need to ensure that filenames are prefixed manually when using PHPLIB directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flysystem always adds the prefix to the file name stored in MongoDB.

69 changes: 69 additions & 0 deletions src/MongoDBServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@

namespace MongoDB\Laravel;

use Closure;
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\GridFS\Bucket;
use MongoDB\Laravel\Cache\MongoStore;
use MongoDB\Laravel\Eloquent\Model;
use MongoDB\Laravel\Queue\MongoConnector;
use RuntimeException;

use function assert;
use function class_exists;
use function get_debug_type;
use function is_string;
use function sprintf;

class MongoDBServiceProvider extends ServiceProvider
{
Expand Down Expand Up @@ -66,5 +79,61 @@ 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"');
}

$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.', $config['connection'] ?? $app['config']['database.default']));
}

$bucket = $connection->getMongoClient()
->selectDatabase($config['database'] ?? $connection->getDatabaseName())
->selectGridFSBucket(['bucketName' => $config['bucket'] ?? 'fs', 'disableMD5' => true]);
} 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'] ?? '');

/** @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);
}

/** Prevent using backslash on Windows in {@see FilesystemAdapter::__construct()} */
$config['directory_separator'] = '/';

return new FilesystemAdapter(new Filesystem($adapter, $config), $adapter, $config);
});
});
}
}
Loading
Loading