Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401]

   1  <?php
   2  /*
   3   * Copyright 2016-2017 MongoDB, Inc.
   4   *
   5   * Licensed under the Apache License, Version 2.0 (the "License");
   6   * you may not use this file except in compliance with the License.
   7   * You may obtain a copy of the License at
   8   *
   9   *   http://www.apache.org/licenses/LICENSE-2.0
  10   *
  11   * Unless required by applicable law or agreed to in writing, software
  12   * distributed under the License is distributed on an "AS IS" BASIS,
  13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14   * See the License for the specific language governing permissions and
  15   * limitations under the License.
  16   */
  17  
  18  namespace MongoDB\GridFS;
  19  
  20  use MongoDB\Collection;
  21  use MongoDB\Driver\Cursor;
  22  use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
  23  use MongoDB\Driver\Manager;
  24  use MongoDB\Driver\ReadConcern;
  25  use MongoDB\Driver\ReadPreference;
  26  use MongoDB\Driver\WriteConcern;
  27  use MongoDB\Exception\InvalidArgumentException;
  28  use MongoDB\Exception\UnsupportedException;
  29  use MongoDB\GridFS\Exception\CorruptFileException;
  30  use MongoDB\GridFS\Exception\FileNotFoundException;
  31  use MongoDB\Model\BSONArray;
  32  use MongoDB\Model\BSONDocument;
  33  use MongoDB\Operation\Find;
  34  use stdClass;
  35  use function array_intersect_key;
  36  use function fopen;
  37  use function get_resource_type;
  38  use function in_array;
  39  use function is_array;
  40  use function is_bool;
  41  use function is_integer;
  42  use function is_object;
  43  use function is_resource;
  44  use function is_string;
  45  use function method_exists;
  46  use function MongoDB\apply_type_map_to_document;
  47  use function MongoDB\BSON\fromPHP;
  48  use function MongoDB\BSON\toJSON;
  49  use function property_exists;
  50  use function sprintf;
  51  use function stream_context_create;
  52  use function stream_copy_to_stream;
  53  use function stream_get_meta_data;
  54  use function stream_get_wrappers;
  55  use function urlencode;
  56  
  57  /**
  58   * Bucket provides a public API for interacting with the GridFS files and chunks
  59   * collections.
  60   *
  61   * @api
  62   */
  63  class Bucket
  64  {
  65      /** @var string */
  66      private static $defaultBucketName = 'fs';
  67  
  68      /** @var integer */
  69      private static $defaultChunkSizeBytes = 261120;
  70  
  71      /** @var array */
  72      private static $defaultTypeMap = [
  73          'array' => BSONArray::class,
  74          'document' => BSONDocument::class,
  75          'root' => BSONDocument::class,
  76      ];
  77  
  78      /** @var string */
  79      private static $streamWrapperProtocol = 'gridfs';
  80  
  81      /** @var CollectionWrapper */
  82      private $collectionWrapper;
  83  
  84      /** @var string */
  85      private $databaseName;
  86  
  87      /** @var Manager */
  88      private $manager;
  89  
  90      /** @var string */
  91      private $bucketName;
  92  
  93      /** @var boolean */
  94      private $disableMD5;
  95  
  96      /** @var integer */
  97      private $chunkSizeBytes;
  98  
  99      /** @var ReadConcern */
 100      private $readConcern;
 101  
 102      /** @var ReadPreference */
 103      private $readPreference;
 104  
 105      /** @var array */
 106      private $typeMap;
 107  
 108      /** @var WriteConcern */
 109      private $writeConcern;
 110  
 111      /**
 112       * Constructs a GridFS bucket.
 113       *
 114       * Supported options:
 115       *
 116       *  * bucketName (string): The bucket name, which will be used as a prefix
 117       *    for the files and chunks collections. Defaults to "fs".
 118       *
 119       *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
 120       *    261120 (i.e. 255 KiB).
 121       *
 122       *  * disableMD5 (boolean): When true, no MD5 sum will be generated for
 123       *    each stored file. Defaults to "false".
 124       *
 125       *  * readConcern (MongoDB\Driver\ReadConcern): Read concern.
 126       *
 127       *  * readPreference (MongoDB\Driver\ReadPreference): Read preference.
 128       *
 129       *  * typeMap (array): Default type map for cursors and BSON documents.
 130       *
 131       *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
 132       *
 133       * @param Manager $manager      Manager instance from the driver
 134       * @param string  $databaseName Database name
 135       * @param array   $options      Bucket options
 136       * @throws InvalidArgumentException for parameter/option parsing errors
 137       */
 138      public function __construct(Manager $manager, $databaseName, array $options = [])
 139      {
 140          $options += [
 141              'bucketName' => self::$defaultBucketName,
 142              'chunkSizeBytes' => self::$defaultChunkSizeBytes,
 143              'disableMD5' => false,
 144          ];
 145  
 146          if (isset($options['bucketName']) && ! is_string($options['bucketName'])) {
 147              throw InvalidArgumentException::invalidType('"bucketName" option', $options['bucketName'], 'string');
 148          }
 149  
 150          if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
 151              throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
 152          }
 153  
 154          if (isset($options['chunkSizeBytes']) && $options['chunkSizeBytes'] < 1) {
 155              throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes']));
 156          }
 157  
 158          if (isset($options['disableMD5']) && ! is_bool($options['disableMD5'])) {
 159              throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean');
 160          }
 161  
 162          if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
 163              throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class);
 164          }
 165  
 166          if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
 167              throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class);
 168          }
 169  
 170          if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
 171              throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
 172          }
 173  
 174          if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
 175              throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
 176          }
 177  
 178          $this->manager = $manager;
 179          $this->databaseName = (string) $databaseName;
 180          $this->bucketName = $options['bucketName'];
 181          $this->chunkSizeBytes = $options['chunkSizeBytes'];
 182          $this->disableMD5 = $options['disableMD5'];
 183          $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern();
 184          $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference();
 185          $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap;
 186          $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern();
 187  
 188          $collectionOptions = array_intersect_key($options, ['readConcern' => 1, 'readPreference' => 1, 'typeMap' => 1, 'writeConcern' => 1]);
 189  
 190          $this->collectionWrapper = new CollectionWrapper($manager, $databaseName, $options['bucketName'], $collectionOptions);
 191          $this->registerStreamWrapper();
 192      }
 193  
 194      /**
 195       * Return internal properties for debugging purposes.
 196       *
 197       * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
 198       * @return array
 199       */
 200      public function __debugInfo()
 201      {
 202          return [
 203              'bucketName' => $this->bucketName,
 204              'databaseName' => $this->databaseName,
 205              'manager' => $this->manager,
 206              'chunkSizeBytes' => $this->chunkSizeBytes,
 207              'readConcern' => $this->readConcern,
 208              'readPreference' => $this->readPreference,
 209              'typeMap' => $this->typeMap,
 210              'writeConcern' => $this->writeConcern,
 211          ];
 212      }
 213  
 214      /**
 215       * Delete a file from the GridFS bucket.
 216       *
 217       * If the files collection document is not found, this method will still
 218       * attempt to delete orphaned chunks.
 219       *
 220       * @param mixed $id File ID
 221       * @throws FileNotFoundException if no file could be selected
 222       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 223       */
 224      public function delete($id)
 225      {
 226          $file = $this->collectionWrapper->findFileById($id);
 227          $this->collectionWrapper->deleteFileAndChunksById($id);
 228  
 229          if ($file === null) {
 230              throw FileNotFoundException::byId($id, $this->getFilesNamespace());
 231          }
 232      }
 233  
 234      /**
 235       * Writes the contents of a GridFS file to a writable stream.
 236       *
 237       * @param mixed    $id          File ID
 238       * @param resource $destination Writable Stream
 239       * @throws FileNotFoundException if no file could be selected
 240       * @throws InvalidArgumentException if $destination is not a stream
 241       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 242       */
 243      public function downloadToStream($id, $destination)
 244      {
 245          if (! is_resource($destination) || get_resource_type($destination) != "stream") {
 246              throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
 247          }
 248  
 249          stream_copy_to_stream($this->openDownloadStream($id), $destination);
 250      }
 251  
 252      /**
 253       * Writes the contents of a GridFS file, which is selected by name and
 254       * revision, to a writable stream.
 255       *
 256       * Supported options:
 257       *
 258       *  * revision (integer): Which revision (i.e. documents with the same
 259       *    filename and different uploadDate) of the file to retrieve. Defaults
 260       *    to -1 (i.e. the most recent revision).
 261       *
 262       * Revision numbers are defined as follows:
 263       *
 264       *  * 0 = the original stored file
 265       *  * 1 = the first revision
 266       *  * 2 = the second revision
 267       *  * etc…
 268       *  * -2 = the second most recent revision
 269       *  * -1 = the most recent revision
 270       *
 271       * @param string   $filename    Filename
 272       * @param resource $destination Writable Stream
 273       * @param array    $options     Download options
 274       * @throws FileNotFoundException if no file could be selected
 275       * @throws InvalidArgumentException if $destination is not a stream
 276       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 277       */
 278      public function downloadToStreamByName($filename, $destination, array $options = [])
 279      {
 280          if (! is_resource($destination) || get_resource_type($destination) != "stream") {
 281              throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
 282          }
 283  
 284          stream_copy_to_stream($this->openDownloadStreamByName($filename, $options), $destination);
 285      }
 286  
 287      /**
 288       * Drops the files and chunks collections associated with this GridFS
 289       * bucket.
 290       *
 291       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 292       */
 293      public function drop()
 294      {
 295          $this->collectionWrapper->dropCollections();
 296      }
 297  
 298      /**
 299       * Finds documents from the GridFS bucket's files collection matching the
 300       * query.
 301       *
 302       * @see Find::__construct() for supported options
 303       * @param array|object $filter  Query by which to filter documents
 304       * @param array        $options Additional options
 305       * @return Cursor
 306       * @throws UnsupportedException if options are not supported by the selected server
 307       * @throws InvalidArgumentException for parameter/option parsing errors
 308       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 309       */
 310      public function find($filter = [], array $options = [])
 311      {
 312          return $this->collectionWrapper->findFiles($filter, $options);
 313      }
 314  
 315      /**
 316       * Finds a single document from the GridFS bucket's files collection
 317       * matching the query.
 318       *
 319       * @see FindOne::__construct() for supported options
 320       * @param array|object $filter  Query by which to filter documents
 321       * @param array        $options Additional options
 322       * @return array|object|null
 323       * @throws UnsupportedException if options are not supported by the selected server
 324       * @throws InvalidArgumentException for parameter/option parsing errors
 325       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 326       */
 327      public function findOne($filter = [], array $options = [])
 328      {
 329          return $this->collectionWrapper->findOneFile($filter, $options);
 330      }
 331  
 332      /**
 333       * Return the bucket name.
 334       *
 335       * @return string
 336       */
 337      public function getBucketName()
 338      {
 339          return $this->bucketName;
 340      }
 341  
 342      /**
 343       * Return the chunks collection.
 344       *
 345       * @return Collection
 346       */
 347      public function getChunksCollection()
 348      {
 349          return $this->collectionWrapper->getChunksCollection();
 350      }
 351  
 352      /**
 353       * Return the chunk size in bytes.
 354       *
 355       * @return integer
 356       */
 357      public function getChunkSizeBytes()
 358      {
 359          return $this->chunkSizeBytes;
 360      }
 361  
 362      /**
 363       * Return the database name.
 364       *
 365       * @return string
 366       */
 367      public function getDatabaseName()
 368      {
 369          return $this->databaseName;
 370      }
 371  
 372      /**
 373       * Gets the file document of the GridFS file associated with a stream.
 374       *
 375       * @param resource $stream GridFS stream
 376       * @return array|object
 377       * @throws InvalidArgumentException if $stream is not a GridFS stream
 378       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 379       */
 380      public function getFileDocumentForStream($stream)
 381      {
 382          $file = $this->getRawFileDocumentForStream($stream);
 383  
 384          // Filter the raw document through the specified type map
 385          return apply_type_map_to_document($file, $this->typeMap);
 386      }
 387  
 388      /**
 389       * Gets the file document's ID of the GridFS file associated with a stream.
 390       *
 391       * @param resource $stream GridFS stream
 392       * @return mixed
 393       * @throws CorruptFileException if the file "_id" field does not exist
 394       * @throws InvalidArgumentException if $stream is not a GridFS stream
 395       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 396       */
 397      public function getFileIdForStream($stream)
 398      {
 399          $file = $this->getRawFileDocumentForStream($stream);
 400  
 401          /* Filter the raw document through the specified type map, but override
 402           * the root type so we can reliably access the ID.
 403           */
 404          $typeMap = ['root' => 'stdClass'] + $this->typeMap;
 405          $file = apply_type_map_to_document($file, $typeMap);
 406  
 407          if (! isset($file->_id) && ! property_exists($file, '_id')) {
 408              throw new CorruptFileException('file._id does not exist');
 409          }
 410  
 411          return $file->_id;
 412      }
 413  
 414      /**
 415       * Return the files collection.
 416       *
 417       * @return Collection
 418       */
 419      public function getFilesCollection()
 420      {
 421          return $this->collectionWrapper->getFilesCollection();
 422      }
 423  
 424      /**
 425       * Return the read concern for this GridFS bucket.
 426       *
 427       * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
 428       * @return ReadConcern
 429       */
 430      public function getReadConcern()
 431      {
 432          return $this->readConcern;
 433      }
 434  
 435      /**
 436       * Return the read preference for this GridFS bucket.
 437       *
 438       * @return ReadPreference
 439       */
 440      public function getReadPreference()
 441      {
 442          return $this->readPreference;
 443      }
 444  
 445      /**
 446       * Return the type map for this GridFS bucket.
 447       *
 448       * @return array
 449       */
 450      public function getTypeMap()
 451      {
 452          return $this->typeMap;
 453      }
 454  
 455      /**
 456       * Return the write concern for this GridFS bucket.
 457       *
 458       * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
 459       * @return WriteConcern
 460       */
 461      public function getWriteConcern()
 462      {
 463          return $this->writeConcern;
 464      }
 465  
 466      /**
 467       * Opens a readable stream for reading a GridFS file.
 468       *
 469       * @param mixed $id File ID
 470       * @return resource
 471       * @throws FileNotFoundException if no file could be selected
 472       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 473       */
 474      public function openDownloadStream($id)
 475      {
 476          $file = $this->collectionWrapper->findFileById($id);
 477  
 478          if ($file === null) {
 479              throw FileNotFoundException::byId($id, $this->getFilesNamespace());
 480          }
 481  
 482          return $this->openDownloadStreamByFile($file);
 483      }
 484  
 485      /**
 486       * Opens a readable stream stream to read a GridFS file, which is selected
 487       * by name and revision.
 488       *
 489       * Supported options:
 490       *
 491       *  * revision (integer): Which revision (i.e. documents with the same
 492       *    filename and different uploadDate) of the file to retrieve. Defaults
 493       *    to -1 (i.e. the most recent revision).
 494       *
 495       * Revision numbers are defined as follows:
 496       *
 497       *  * 0 = the original stored file
 498       *  * 1 = the first revision
 499       *  * 2 = the second revision
 500       *  * etc…
 501       *  * -2 = the second most recent revision
 502       *  * -1 = the most recent revision
 503       *
 504       * @param string $filename Filename
 505       * @param array  $options  Download options
 506       * @return resource
 507       * @throws FileNotFoundException if no file could be selected
 508       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 509       */
 510      public function openDownloadStreamByName($filename, array $options = [])
 511      {
 512          $options += ['revision' => -1];
 513  
 514          $file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $options['revision']);
 515  
 516          if ($file === null) {
 517              throw FileNotFoundException::byFilenameAndRevision($filename, $options['revision'], $this->getFilesNamespace());
 518          }
 519  
 520          return $this->openDownloadStreamByFile($file);
 521      }
 522  
 523      /**
 524       * Opens a writable stream for writing a GridFS file.
 525       *
 526       * Supported options:
 527       *
 528       *  * _id (mixed): File document identifier. Defaults to a new ObjectId.
 529       *
 530       *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
 531       *    bucket's chunk size.
 532       *
 533       *  * disableMD5 (boolean): When true, no MD5 sum will be generated for
 534       *    the stored file. Defaults to "false".
 535       *
 536       *  * metadata (document): User data for the "metadata" field of the files
 537       *    collection document.
 538       *
 539       * @param string $filename Filename
 540       * @param array  $options  Upload options
 541       * @return resource
 542       */
 543      public function openUploadStream($filename, array $options = [])
 544      {
 545          $options += ['chunkSizeBytes' => $this->chunkSizeBytes];
 546  
 547          $path = $this->createPathForUpload();
 548          $context = stream_context_create([
 549              self::$streamWrapperProtocol => [
 550                  'collectionWrapper' => $this->collectionWrapper,
 551                  'filename' => $filename,
 552                  'options' => $options,
 553              ],
 554          ]);
 555  
 556          return fopen($path, 'w', false, $context);
 557      }
 558  
 559      /**
 560       * Renames the GridFS file with the specified ID.
 561       *
 562       * @param mixed  $id          File ID
 563       * @param string $newFilename New filename
 564       * @throws FileNotFoundException if no file could be selected
 565       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 566       */
 567      public function rename($id, $newFilename)
 568      {
 569          $updateResult = $this->collectionWrapper->updateFilenameForId($id, $newFilename);
 570  
 571          if ($updateResult->getModifiedCount() === 1) {
 572              return;
 573          }
 574  
 575          /* If the update resulted in no modification, it's possible that the
 576           * file did not exist, in which case we must raise an error. Checking
 577           * the write result's matched count will be most efficient, but fall
 578           * back to a findOne operation if necessary (i.e. legacy writes).
 579           */
 580          $found = $updateResult->getMatchedCount() !== null
 581              ? $updateResult->getMatchedCount() === 1
 582              : $this->collectionWrapper->findFileById($id) !== null;
 583  
 584          if (! $found) {
 585              throw FileNotFoundException::byId($id, $this->getFilesNamespace());
 586          }
 587      }
 588  
 589      /**
 590       * Writes the contents of a readable stream to a GridFS file.
 591       *
 592       * Supported options:
 593       *
 594       *  * _id (mixed): File document identifier. Defaults to a new ObjectId.
 595       *
 596       *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
 597       *    bucket's chunk size.
 598       *
 599       *  * disableMD5 (boolean): When true, no MD5 sum will be generated for
 600       *    the stored file. Defaults to "false".
 601       *
 602       *  * metadata (document): User data for the "metadata" field of the files
 603       *    collection document.
 604       *
 605       * @param string   $filename Filename
 606       * @param resource $source   Readable stream
 607       * @param array    $options  Stream options
 608       * @return mixed ID of the newly created GridFS file
 609       * @throws InvalidArgumentException if $source is not a GridFS stream
 610       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 611       */
 612      public function uploadFromStream($filename, $source, array $options = [])
 613      {
 614          if (! is_resource($source) || get_resource_type($source) != "stream") {
 615              throw InvalidArgumentException::invalidType('$source', $source, 'resource');
 616          }
 617  
 618          $destination = $this->openUploadStream($filename, $options);
 619          stream_copy_to_stream($source, $destination);
 620  
 621          return $this->getFileIdForStream($destination);
 622      }
 623  
 624      /**
 625       * Creates a path for an existing GridFS file.
 626       *
 627       * @param stdClass $file GridFS file document
 628       * @return string
 629       */
 630      private function createPathForFile(stdClass $file)
 631      {
 632          if (! is_object($file->_id) || method_exists($file->_id, '__toString')) {
 633              $id = (string) $file->_id;
 634          } else {
 635              $id = toJSON(fromPHP(['_id' => $file->_id]));
 636          }
 637  
 638          return sprintf(
 639              '%s://%s/%s.files/%s',
 640              self::$streamWrapperProtocol,
 641              urlencode($this->databaseName),
 642              urlencode($this->bucketName),
 643              urlencode($id)
 644          );
 645      }
 646  
 647      /**
 648       * Creates a path for a new GridFS file, which does not yet have an ID.
 649       *
 650       * @return string
 651       */
 652      private function createPathForUpload()
 653      {
 654          return sprintf(
 655              '%s://%s/%s.files',
 656              self::$streamWrapperProtocol,
 657              urlencode($this->databaseName),
 658              urlencode($this->bucketName)
 659          );
 660      }
 661  
 662      /**
 663       * Returns the names of the files collection.
 664       *
 665       * @return string
 666       */
 667      private function getFilesNamespace()
 668      {
 669          return sprintf('%s.%s.files', $this->databaseName, $this->bucketName);
 670      }
 671  
 672      /**
 673       * Gets the file document of the GridFS file associated with a stream.
 674       *
 675       * This returns the raw document from the StreamWrapper, which does not
 676       * respect the Bucket's type map.
 677       *
 678       * @param resource $stream GridFS stream
 679       * @return stdClass
 680       * @throws InvalidArgumentException
 681       */
 682      private function getRawFileDocumentForStream($stream)
 683      {
 684          if (! is_resource($stream) || get_resource_type($stream) != "stream") {
 685              throw InvalidArgumentException::invalidType('$stream', $stream, 'resource');
 686          }
 687  
 688          $metadata = stream_get_meta_data($stream);
 689  
 690          if (! isset($metadata['wrapper_data']) || ! $metadata['wrapper_data'] instanceof StreamWrapper) {
 691              throw InvalidArgumentException::invalidType('$stream wrapper data', isset($metadata['wrapper_data']) ? $metadata['wrapper_data'] : null, StreamWrapper::class);
 692          }
 693  
 694          return $metadata['wrapper_data']->getFile();
 695      }
 696  
 697      /**
 698       * Opens a readable stream for the GridFS file.
 699       *
 700       * @param stdClass $file GridFS file document
 701       * @return resource
 702       */
 703      private function openDownloadStreamByFile(stdClass $file)
 704      {
 705          $path = $this->createPathForFile($file);
 706          $context = stream_context_create([
 707              self::$streamWrapperProtocol => [
 708                  'collectionWrapper' => $this->collectionWrapper,
 709                  'file' => $file,
 710              ],
 711          ]);
 712  
 713          return fopen($path, 'r', false, $context);
 714      }
 715  
 716      /**
 717       * Registers the GridFS stream wrapper if it is not already registered.
 718       */
 719      private function registerStreamWrapper()
 720      {
 721          if (in_array(self::$streamWrapperProtocol, stream_get_wrappers())) {
 722              return;
 723          }
 724  
 725          StreamWrapper::register(self::$streamWrapperProtocol);
 726      }
 727  }