Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

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