Skip to content
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

PHPLIB-1206 Allow global registration of GridFS buckets with gridfs:// protocol #1138

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference/class/MongoDBGridFSBucket.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ Methods
/reference/method/MongoDBGridFSBucket-openDownloadStream
/reference/method/MongoDBGridFSBucket-openDownloadStreamByName
/reference/method/MongoDBGridFSBucket-openUploadStream
/reference/method/MongoDBGridFSBucket-registerGlobalStreamWrapperAlias
/reference/method/MongoDBGridFSBucket-rename
/reference/method/MongoDBGridFSBucket-uploadFromStream
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
===========================================================
MongoDB\\GridFS\\Bucket::registerGlobalStreamWrapperAlias()
===========================================================

.. versionadded:: 1.18

.. default-domain:: mongodb

.. contents:: On this page
:local:
:backlinks: none
:depth: 1
:class: singlecol

Definition
----------

.. phpmethod:: MongoDB\\GridFS\\Bucket::registerGlobalStreamWrapperAlias()

Registers an alias for the bucket, which enables files within the bucket to
be accessed using a basic filename string (e.g.
`gridfs://<bucket-alias>/<filename>`).

.. code-block:: php

function registerGlobalStreamWrapperAlias(string $alias): void

Parameters
----------

``$alias`` : array
A non-empty string used to identify the GridFS bucket when accessing files
using the ``gridfs://`` stream wrapper.

Behavior
--------

After registering an alias for the bucket, the most recent revision of a file
can be accessed using a filename string in the form ``gridfs://<bucket-alias>/<filename>``.

Supported stream functions:

- :php:`copy() <copy>`
- :php:`file_exists() <file_exists>`
- :php:`file_get_contents() <file_get_contents>`
- :php:`file_put_contents() <file_put_contents>`
- :php:`filemtime() <filemtime>`
- :php:`filesize() <filesize>`
- :php:`file() <file>`
- :php:`fopen() <fopen>` (with "r", "rb", "w", and "wb" modes)

In read mode, the stream context can contain the option ``gridfs['revision']``
to specify the revision number of the file to read. If omitted, the most recent
revision is read (revision ``-1``).

In write mode, the stream context can contain the option ``gridfs['chunkSizeBytes']``.
If omitted, the defaults are inherited from the ``Bucket`` instance option.

Example
-------

Read and write to a GridFS bucket using the ``gridfs://`` stream wrapper
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following example demonstrates how to register an alias for a GridFS bucket
and use the functions ``file_exists()``, ``file_get_contents()``, and
``file_put_contents()`` to read and write to the bucket.

Each call to these functions makes a request to the server.

.. code-block:: php

<?php

$database = (new MongoDB\Client)->selectDatabase('test');
$bucket = $database->selectGridFSBucket();

$bucket->registerGlobalStreamWrapperAlias('mybucket');

var_dump(file_exists('gridfs://mybucket/hello.txt'));

file_put_contents('gridfs://mybucket/hello.txt', 'Hello, GridFS!');

var_dump(file_exists('gridfs://mybucket/hello.txt'));

echo file_get_contents('gridfs://mybucket/hello.txt');
jmikola marked this conversation as resolved.
Show resolved Hide resolved

The output would then resemble:

.. code-block:: none

bool(false)
bool(true)
Hello, GridFS!

Read a specific revision of a file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Using a stream context, you can specify the revision number of the file to
read. If omitted, the most recent revision is read.

.. code-block:: php

<?php

$database = (new MongoDB\Client)->selectDatabase('test');
$bucket = $database->selectGridFSBucket();

$bucket->registerGlobalStreamWrapperAlias('mybucket');

// Creating revision 0
$handle = fopen('gridfs://mybucket/hello.txt', 'w');
fwrite($handle, 'Hello, GridFS! (v0)');
fclose($handle);

// Creating revision 1
$handle = fopen('gridfs://mybucket/hello.txt', 'w');
fwrite($handle, 'Hello, GridFS! (v1)');
fclose($handle);

// Read revision 0
$context = stream_context_create([
'gridfs' => ['revision' => 0],
]);
$handle = fopen('gridfs://mybucket/hello.txt', 'r', false, $context);
echo fread($handle, 1024);

The output would then resemble:

.. code-block:: none

Hello, GridFS! (v0)
59 changes: 59 additions & 0 deletions examples/gridfs-stream-wrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/**
* For applications that need to interact with GridFS using only a filename string,
* a bucket can be registered with an alias. Files can then be accessed using the
* following pattern: gridfs://<bucket-alias>/<filename>
*/

declare(strict_types=1);

namespace MongoDB\Examples;

use MongoDB\Client;

use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function getenv;
use function stream_context_create;

use const PHP_EOL;

require __DIR__ . '/../vendor/autoload.php';

$client = new Client(getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/');
$bucket = $client->test->selectGridFSBucket();
$bucket->drop();

// Register the alias "mybucket" for default bucket of the "test" database
$bucket->registerGlobalStreamWrapperAlias('mybucket');

echo 'File exists: ';
echo file_exists('gridfs://mybucket/hello.txt') ? 'yes' : 'no';
echo PHP_EOL;

echo 'Writing file';
file_put_contents('gridfs://mybucket/hello.txt', 'Hello, GridFS!');
echo PHP_EOL;

echo 'File exists: ';
echo file_exists('gridfs://mybucket/hello.txt') ? 'yes' : 'no';
echo PHP_EOL;

echo 'Reading file: ';
echo file_get_contents('gridfs://mybucket/hello.txt');
echo PHP_EOL;

echo 'Writing new version of the file';
file_put_contents('gridfs://mybucket/hello.txt', 'Hello, GridFS! (v2)');
echo PHP_EOL;

echo 'Reading new version of the file: ';
echo file_get_contents('gridfs://mybucket/hello.txt');
echo PHP_EOL;

echo 'Reading previous version of the file: ';
$context = stream_context_create(['gridfs' => ['revision' => -2]]);
echo file_get_contents('gridfs://mybucket/hello.txt', false, $context);
echo PHP_EOL;
24 changes: 10 additions & 14 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@
<code><![CDATA[$options['revision']]]></code>
<code><![CDATA[$options['revision']]]></code>
</MixedArgument>
<MixedArgumentTypeCoercion>
<code>$context</code>
</MixedArgumentTypeCoercion>
</file>
<file src="src/GridFS/ReadableStream.php">
<MixedArgument>
Expand All @@ -89,20 +92,13 @@
</MixedArgument>
</file>
<file src="src/GridFS/StreamWrapper.php">
<MixedArgument>
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
<code><![CDATA[$context[$this->protocol]['file']]]></code>
<code><![CDATA[$context[$this->protocol]['filename']]]></code>
<code><![CDATA[$context[$this->protocol]['options']]]></code>
</MixedArgument>
<MixedArrayAccess>
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
<code><![CDATA[$context[$this->protocol]['collectionWrapper']]]></code>
<code><![CDATA[$context[$this->protocol]['file']]]></code>
<code><![CDATA[$context[$this->protocol]['filename']]]></code>
<code><![CDATA[$context[$this->protocol]['options']]]></code>
</MixedArrayAccess>
<InvalidArgument>
<code>$context</code>
<code>$context</code>
</InvalidArgument>
<MixedAssignment>
<code>$context</code>
</MixedAssignment>
</file>
<file src="src/Model/BSONArray.php">
<MixedAssignment>
Expand Down
72 changes: 72 additions & 0 deletions src/GridFS/Bucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use MongoDB\Exception\UnsupportedException;
use MongoDB\GridFS\Exception\CorruptFileException;
use MongoDB\GridFS\Exception\FileNotFoundException;
use MongoDB\GridFS\Exception\LogicException;
use MongoDB\GridFS\Exception\StreamException;
use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument;
Expand All @@ -39,6 +40,7 @@
use function array_intersect_key;
use function array_key_exists;
use function assert;
use function explode;
use function fopen;
use function get_resource_type;
use function in_array;
Expand All @@ -54,6 +56,7 @@
use function MongoDB\BSON\toJSON;
use function property_exists;
use function sprintf;
use function str_contains;
use function stream_context_create;
use function stream_copy_to_stream;
use function stream_get_meta_data;
Expand Down Expand Up @@ -587,6 +590,29 @@ public function openUploadStream(string $filename, array $options = [])
return fopen($path, 'w', false, $context);
}

/**
* Register an alias to enable basic filename access for this bucket.
*
* For applications that need to interact with GridFS using only a filename
* string, a bucket can be registered with an alias. Files can then be
* accessed using the following pattern:
*
* gridfs://<bucket-alias>/<filename>
GromNaN marked this conversation as resolved.
Show resolved Hide resolved
*
* Read operations will always target the most recent revision of a file.
*
* @param non-empty-string string $alias The alias to use for the bucket
*/
public function registerGlobalStreamWrapperAlias(string $alias): void
{
if ($alias === '' || str_contains($alias, '/')) {
throw new InvalidArgumentException(sprintf('The bucket alias must be a non-empty string without any slash, "%s" given', $alias));
}

// Use a closure to expose the private method into another class
jmikola marked this conversation as resolved.
Show resolved Hide resolved
StreamWrapper::setContextResolver($alias, fn (string $path, string $mode, array $context) => $this->resolveStreamContext($path, $mode, $context));
jmikola marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Renames the GridFS file with the specified ID.
*
Expand Down Expand Up @@ -756,4 +782,50 @@ private function registerStreamWrapper(): void

StreamWrapper::register(self::STREAM_WRAPPER_PROTOCOL);
}

/**
* Create a stream context from the path and mode provided to fopen().
*
* @see StreamWrapper::setContextResolver()
*
* @param string $path The full url provided to fopen(). It contains the filename.
* gridfs://database_name/collection_name.files/file_name
* @param array{revision?: int, chunkSizeBytes?: int, disableMD5?: bool} $context The options provided to fopen()
*
* @return array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}
*
* @throws FileNotFoundException
* @throws LogicException
*/
private function resolveStreamContext(string $path, string $mode, array $context): array
{
// Fallback to an empty filename if the path does not contain one: "gridfs://alias"
$filename = explode('/', $path, 4)[3] ?? '';

if ($mode === 'r' || $mode === 'rb') {
$file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $context['revision'] ?? -1);

if (! is_object($file)) {
throw FileNotFoundException::byFilenameAndRevision($filename, $context['revision'] ?? -1, $path);
}

return [
'collectionWrapper' => $this->collectionWrapper,
'file' => $file,
];
}

if ($mode === 'w' || $mode === 'wb') {
return [
'collectionWrapper' => $this->collectionWrapper,
'filename' => $filename,
GromNaN marked this conversation as resolved.
Show resolved Hide resolved
'options' => $context + [
'chunkSizeBytes' => $this->chunkSizeBytes,
'disableMD5' => $this->disableMD5,
],
];
}

throw LogicException::openModeNotSupported($mode);
}
}