Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e982b85

Browse files
committed
PHPLIB-1206 Add default context resolver for GridFS StreamWrapper
1 parent e3b6462 commit e982b85

File tree

5 files changed

+236
-27
lines changed

5 files changed

+236
-27
lines changed

‎src/GridFS/Bucket.php‎

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@
5050
use function MongoDB\BSON\toJSON;
5151
use function property_exists;
5252
use function sprintf;
53+
use function str_contains;
54+
use function str_starts_with;
5355
use function stream_context_create;
5456
use function stream_copy_to_stream;
5557
use function stream_get_meta_data;
5658
use function stream_get_wrappers;
59+
use function strlen;
60+
use function substr;
61+
use function urldecode;
5762
use function urlencode;
5863

5964
/**
@@ -74,6 +79,8 @@ class Bucket
7479

7580
private const STREAM_WRAPPER_PROTOCOL = 'gridfs';
7681

82+
private string $protocol;
83+
7784
private CollectionWrapper $collectionWrapper;
7885

7986
private string $databaseName;
@@ -124,11 +131,16 @@ class Bucket
124131
public function __construct(Manager $manager, string $databaseName, array $options = [])
125132
{
126133
$options += [
134+
'protocol' => self::STREAM_WRAPPER_PROTOCOL,
127135
'bucketName' => self::DEFAULT_BUCKET_NAME,
128136
'chunkSizeBytes' => self::DEFAULT_CHUNK_SIZE_BYTES,
129137
'disableMD5' => false,
130138
];
131139

140+
if (! is_string($options['protocol'])) {
141+
throw InvalidArgumentException::invalidType('"protocol" option', $options['protocol'], 'string');
142+
}
143+
132144
if (! is_string($options['bucketName'])) {
133145
throw InvalidArgumentException::invalidType('"bucketName" option', $options['bucketName'], 'string');
134146
}
@@ -163,6 +175,7 @@ public function __construct(Manager $manager, string $databaseName, array $optio
163175

164176
$this->manager = $manager;
165177
$this->databaseName = $databaseName;
178+
$this->protocol = $options['protocol'];
166179
$this->bucketName = $options['bucketName'];
167180
$this->chunkSizeBytes = $options['chunkSizeBytes'];
168181
$this->disableMD5 = $options['disableMD5'];
@@ -549,7 +562,7 @@ public function openUploadStream(string $filename, array $options = [])
549562

550563
$path = $this->createPathForUpload();
551564
$context = stream_context_create([
552-
self::STREAM_WRAPPER_PROTOCOL => [
565+
$this->protocol => [
553566
'collectionWrapper' => $this->collectionWrapper,
554567
'filename' => $filename,
555568
'options' => $options,
@@ -631,6 +644,60 @@ public function uploadFromStream(string $filename, $source, array $options = [])
631644
return $this->getFileIdForStream($destination);
632645
}
633646

647+
public function createPathForFilename(string $filename): string
648+
{
649+
return $this->createPathForFile((object) ['_id' => $filename]);
650+
}
651+
652+
/**
653+
* Create a stream context from
654+
*
655+
* @see StreamWrapper::setDefaultContextResolver()
656+
* @see stream_context_create()
657+
*
658+
* @param string $path The full url provided to fopen(). It contains the filename.
659+
* gridfs://database_name/collection_name.files/file_name
660+
*
661+
* @return array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}|null
662+
*/
663+
public function resolveStreamContext(string $path, string $mode): ?array
664+
{
665+
// The file can be read only if it belongs to this bucket
666+
$basePath = $this->createPathForFile((object) ['_id' => '']);
667+
if (! str_starts_with($path, $basePath)) {
668+
return null;
669+
}
670+
671+
$filename = urldecode(substr($path, strlen($basePath)));
672+
673+
if (str_contains($mode, 'r')) {
674+
$file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, -1);
675+
676+
// File not found
677+
if ($file === null) {
678+
return null;
679+
}
680+
681+
return [
682+
'collectionWrapper' => $this->collectionWrapper,
683+
'file' => $file,
684+
];
685+
}
686+
687+
if (str_contains($mode, 'w')) {
688+
return [
689+
'collectionWrapper' => $this->collectionWrapper,
690+
'filename' => $filename,
691+
'options' => [
692+
'chunkSizeBytes' => $this->chunkSizeBytes,
693+
'disableMD5' => $this->disableMD5,
694+
],
695+
];
696+
}
697+
698+
return null;
699+
}
700+
634701
/**
635702
* Creates a path for an existing GridFS file.
636703
*
@@ -646,7 +713,7 @@ private function createPathForFile(object $file): string
646713

647714
return sprintf(
648715
'%s://%s/%s.files/%s',
649-
self::STREAM_WRAPPER_PROTOCOL,
716+
$this->protocol,
650717
urlencode($this->databaseName),
651718
urlencode($this->bucketName),
652719
urlencode($id),
@@ -708,7 +775,7 @@ private function openDownloadStreamByFile(object $file)
708775
{
709776
$path = $this->createPathForFile($file);
710777
$context = stream_context_create([
711-
self::STREAM_WRAPPER_PROTOCOL => [
778+
$this->protocol => [
712779
'collectionWrapper' => $this->collectionWrapper,
713780
'file' => $file,
714781
],

‎src/GridFS/StreamWrapper.php‎

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,68 @@
1717

1818
namespace MongoDB\GridFS;
1919

20+
use Closure;
2021
use MongoDB\BSON\UTCDateTime;
2122

2223
use function assert;
24+
use function call_user_func;
2325
use function explode;
2426
use function in_array;
27+
use function is_array;
2528
use function is_integer;
29+
use function is_object;
2630
use function is_resource;
31+
use function is_string;
32+
use function sprintf;
33+
use function str_contains;
2734
use function stream_context_get_options;
2835
use function stream_get_wrappers;
2936
use function stream_wrapper_register;
3037
use function stream_wrapper_unregister;
38+
use function trigger_error;
3139

40+
use const E_USER_WARNING;
3241
use const SEEK_CUR;
3342
use const SEEK_END;
3443
use const SEEK_SET;
3544
use const STREAM_IS_URL;
45+
use const STREAM_REPORT_ERRORS;
3646

3747
/**
3848
* Stream wrapper for reading and writing a GridFS file.
3949
*
4050
* @internal
4151
* @see Bucket::openUploadStream()
4252
* @see Bucket::openDownloadStream()
53+
* @psalm-type ContextOptions = array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}|null
4354
*/
4455
class StreamWrapper
4556
{
4657
/** @var resource|null Stream context (set by PHP) */
4758
public $context;
4859

49-
private ?string $protocol = null;
50-
5160
/** @var ReadableStream|WritableStream|null */
5261
private $stream;
5362

63+
/** @var Closure(string, string): ContextOptions|null */
64+
private static ?Closure $contextResolver = null;
65+
66+
/**
67+
* In order to use the stream wrapper with file names only,...
68+
*
69+
* @see Bucket::resolveStreamContext()
70+
*
71+
* @param Bucket|Closure(string, string):ContextOptions|null $resolver
72+
*/
73+
public static function setDefaultContextResolver($resolver): void
74+
{
75+
if ($resolver instanceof Bucket) {
76+
$resolver = Closure::fromCallable([$resolver, 'resolveStreamContext']);
77+
}
78+
79+
self::$contextResolver = $resolver;
80+
}
81+
5482
public function __destruct()
5583
{
5684
/* This destructor is a workaround for PHP trying to use the stream well
@@ -122,14 +150,44 @@ public function stream_eof(): bool
122150
*/
123151
public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
124152
{
125-
$this->initProtocol($path);
153+
$protocol = $this->parseProtocol($path);
154+
155+
assert(is_resource($this->context));
156+
$contextOptions = stream_context_get_options($this->context)[$protocol] ?? null;
126157

127-
if ($mode === 'r') {
128-
return $this->initReadableStream();
158+
if ($contextOptions === null) {
159+
if (! isset(self::$contextResolver)) {
160+
if ($options & STREAM_REPORT_ERRORS) {
161+
trigger_error(sprintf('No stream context provided for "%s" protocol. Use "%s::setDefaultContextResolver() to provide a default context."', $protocol, self::class), E_USER_WARNING);
162+
}
163+
164+
return false;
165+
}
166+
167+
$contextOptions = call_user_func(self::$contextResolver, $path, $mode);
168+
if ($contextOptions === null) {
169+
if ($options & STREAM_REPORT_ERRORS) {
170+
trigger_error(sprintf('File not found "%s" with the default GridFS resolver.', $path), E_USER_WARNING);
171+
}
172+
173+
return false;
174+
}
129175
}
130176

131-
if ($mode === 'w') {
132-
return $this->initWritableStream();
177+
assert(is_array($contextOptions));
178+
assert(isset($contextOptions['collectionWrapper']) && $contextOptions['collectionWrapper'] instanceof CollectionWrapper);
179+
180+
if (str_contains($mode, 'r')) {
181+
assert(isset($contextOptions['file']) && is_object($contextOptions['file']));
182+
183+
return $this->initReadableStream($contextOptions);
184+
}
185+
186+
if (str_contains($mode, 'w')) {
187+
assert(isset($contextOptions['filename']) && is_string($contextOptions['filename']));
188+
assert(isset($contextOptions['options']) && is_array($contextOptions['options']));
189+
190+
return $this->initWritableStream($contextOptions);
133191
}
134192

135193
return false;
@@ -278,26 +336,24 @@ private function getStatTemplate(): array
278336
*
279337
* @see StreamWrapper::stream_open()
280338
*/
281-
private function initProtocol(string $path): void
339+
private function parseProtocol(string $path): string
282340
{
283341
$parts = explode('://', $path, 2);
284-
$this->protocol = $parts[0] ?: 'gridfs';
342+
343+
return $parts[0] ?: 'gridfs';
285344
}
286345

287346
/**
288347
* Initialize the internal stream for reading.
289348
*
349+
* @param array{collectionWrapper: CollectionWrapper, file: object, ...} $contextOptions
290350
* @see StreamWrapper::stream_open()
291351
*/
292-
private function initReadableStream(): bool
352+
private function initReadableStream(array$contextOptions): bool
293353
{
294-
assert(is_resource($this->context));
295-
$context = stream_context_get_options($this->context);
296-
297-
assert($this->protocol !== null);
298354
$this->stream = new ReadableStream(
299-
$context[$this->protocol]['collectionWrapper'],
300-
$context[$this->protocol]['file'],
355+
$contextOptions['collectionWrapper'],
356+
$contextOptions['file'],
301357
);
302358

303359
return true;
@@ -306,18 +362,15 @@ private function initReadableStream(): bool
306362
/**
307363
* Initialize the internal stream for writing.
308364
*
365+
* @param array{collectionWrapper: CollectionWrapper, filename: string, options: array, ...} $contextOptions
309366
* @see StreamWrapper::stream_open()
310367
*/
311-
private function initWritableStream(): bool
368+
private function initWritableStream(array$contextOptions): bool
312369
{
313-
assert(is_resource($this->context));
314-
$context = stream_context_get_options($this->context);
315-
316-
assert($this->protocol !== null);
317370
$this->stream = new WritableStream(
318-
$context[$this->protocol]['collectionWrapper'],
319-
$context[$this->protocol]['filename'],
320-
$context[$this->protocol]['options'],
371+
$contextOptions['collectionWrapper'],
372+
$contextOptions['filename'],
373+
$contextOptions['options'],
321374
);
322375

323376
return true;

‎tests/GridFS/BucketFunctionalTest.php‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
namespace MongoDB\Tests\GridFS;
44

55
use MongoDB\BSON\Binary;
6+
use MongoDB\BSON\ObjectId;
67
use MongoDB\Collection;
78
use MongoDB\Driver\ReadConcern;
89
use MongoDB\Driver\ReadPreference;
910
use MongoDB\Driver\WriteConcern;
1011
use MongoDB\Exception\InvalidArgumentException;
1112
use MongoDB\GridFS\Bucket;
13+
use MongoDB\GridFS\CollectionWrapper;
1214
use MongoDB\GridFS\Exception\CorruptFileException;
1315
use MongoDB\GridFS\Exception\FileNotFoundException;
1416
use MongoDB\GridFS\Exception\StreamException;
@@ -745,6 +747,44 @@ public function testDanglingOpenWritableStream(): void
745747
$this->assertSame('', $output);
746748
}
747749

750+
public function testCreatePathForFilename(): void
751+
{
752+
$filename = 'filename';
753+
$expected = sprintf('gridfs://%s/%s.files/%s', $this->bucket->getDatabaseName(), $this->bucket->getBucketName(), $filename);
754+
755+
$this->assertSame($expected, $this->bucket->createPathForFilename($filename));
756+
}
757+
758+
public function testResolveStreamContextForRead(): void
759+
{
760+
$stream = $this->bucket->openUploadStream('filename');
761+
fwrite($stream, 'foobar');
762+
fclose($stream);
763+
764+
$context = $this->bucket->resolveStreamContext($this->bucket->createPathForFilename('filename'), 'rb');
765+
766+
$this->assertIsArray($context);
767+
$this->assertArrayHasKey('collectionWrapper', $context);
768+
$this->assertInstanceOf(CollectionWrapper::class, $context['collectionWrapper']);
769+
$this->assertArrayHasKey('file', $context);
770+
$this->assertIsObject($context['file']);
771+
$this->assertInstanceOf(ObjectId::class, $context['file']->_id);
772+
$this->assertSame('filename', $context['file']->filename);
773+
}
774+
775+
public function testResolveStreamContextForWrite(): void
776+
{
777+
$context = $this->bucket->resolveStreamContext($this->bucket->createPathForFilename('filename'), 'wb');
778+
779+
$this->assertIsArray($context);
780+
$this->assertArrayHasKey('collectionWrapper', $context);
781+
$this->assertInstanceOf(CollectionWrapper::class, $context['collectionWrapper']);
782+
$this->assertArrayHasKey('filename', $context);
783+
$this->assertSame('filename', $context['filename']);
784+
$this->assertArrayHasKey('options', $context);
785+
$this->assertSame(['chunkSizeBytes' => 261120, 'disableMD5' => false], $context['options']);
786+
}
787+
748788
/**
749789
* Asserts that an index with the given name exists for the collection.
750790
*

‎tests/GridFS/FunctionalTestCase.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use MongoDB\Collection;
66
use MongoDB\GridFS\Bucket;
7+
use MongoDB\GridFS\StreamWrapper;
78
use MongoDB\Tests\FunctionalTestCase as BaseFunctionalTestCase;
89

910
use function fopen;
@@ -34,6 +35,13 @@ public function setUp(): void
3435
$this->filesCollection = $this->createCollection($this->getDatabaseName(), 'fs.files');
3536
}
3637

38+
public function tearDown(): void
39+
{
40+
StreamWrapper::setDefaultContextResolver(null);
41+
42+
parent::tearDown();
43+
}
44+
3745
/**
3846
* Asserts that a variable is a stream containing the expected data.
3947
*

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /