Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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 ArrayIterator;
  21  use MongoDB\Collection;
  22  use MongoDB\Driver\Cursor;
  23  use MongoDB\Driver\Manager;
  24  use MongoDB\Driver\ReadPreference;
  25  use MongoDB\Exception\InvalidArgumentException;
  26  use MongoDB\UpdateResult;
  27  use MultipleIterator;
  28  use stdClass;
  29  use function abs;
  30  use function count;
  31  use function is_numeric;
  32  use function sprintf;
  33  
  34  /**
  35   * CollectionWrapper abstracts the GridFS files and chunks collections.
  36   *
  37   * @internal
  38   */
  39  class CollectionWrapper
  40  {
  41      /** @var string */
  42      private $bucketName;
  43  
  44      /** @var Collection */
  45      private $chunksCollection;
  46  
  47      /** @var string */
  48      private $databaseName;
  49  
  50      /** @var boolean */
  51      private $checkedIndexes = false;
  52  
  53      /** @var Collection */
  54      private $filesCollection;
  55  
  56      /**
  57       * Constructs a GridFS collection wrapper.
  58       *
  59       * @see Collection::__construct() for supported options
  60       * @param Manager $manager           Manager instance from the driver
  61       * @param string  $databaseName      Database name
  62       * @param string  $bucketName        Bucket name
  63       * @param array   $collectionOptions Collection options
  64       * @throws InvalidArgumentException
  65       */
  66      public function __construct(Manager $manager, $databaseName, $bucketName, array $collectionOptions = [])
  67      {
  68          $this->databaseName = (string) $databaseName;
  69          $this->bucketName = (string) $bucketName;
  70  
  71          $this->filesCollection = new Collection($manager, $databaseName, sprintf('%s.files', $bucketName), $collectionOptions);
  72          $this->chunksCollection = new Collection($manager, $databaseName, sprintf('%s.chunks', $bucketName), $collectionOptions);
  73      }
  74  
  75      /**
  76       * Deletes all GridFS chunks for a given file ID.
  77       *
  78       * @param mixed $id
  79       */
  80      public function deleteChunksByFilesId($id)
  81      {
  82          $this->chunksCollection->deleteMany(['files_id' => $id]);
  83      }
  84  
  85      /**
  86       * Deletes a GridFS file and related chunks by ID.
  87       *
  88       * @param mixed $id
  89       */
  90      public function deleteFileAndChunksById($id)
  91      {
  92          $this->filesCollection->deleteOne(['_id' => $id]);
  93          $this->chunksCollection->deleteMany(['files_id' => $id]);
  94      }
  95  
  96      /**
  97       * Drops the GridFS files and chunks collections.
  98       */
  99      public function dropCollections()
 100      {
 101          $this->filesCollection->drop(['typeMap' => []]);
 102          $this->chunksCollection->drop(['typeMap' => []]);
 103      }
 104  
 105      /**
 106       * Finds GridFS chunk documents for a given file ID and optional offset.
 107       *
 108       * @param mixed   $id        File ID
 109       * @param integer $fromChunk Starting chunk (inclusive)
 110       * @return Cursor
 111       */
 112      public function findChunksByFileId($id, $fromChunk = 0)
 113      {
 114          return $this->chunksCollection->find(
 115              [
 116                  'files_id' => $id,
 117                  'n' => ['$gte' => $fromChunk],
 118              ],
 119              [
 120                  'sort' => ['n' => 1],
 121                  'typeMap' => ['root' => 'stdClass'],
 122              ]
 123          );
 124      }
 125  
 126      /**
 127       * Finds a GridFS file document for a given filename and revision.
 128       *
 129       * Revision numbers are defined as follows:
 130       *
 131       *  * 0 = the original stored file
 132       *  * 1 = the first revision
 133       *  * 2 = the second revision
 134       *  * etc…
 135       *  * -2 = the second most recent revision
 136       *  * -1 = the most recent revision
 137       *
 138       * @see Bucket::downloadToStreamByName()
 139       * @see Bucket::openDownloadStreamByName()
 140       * @param string  $filename
 141       * @param integer $revision
 142       * @return stdClass|null
 143       */
 144      public function findFileByFilenameAndRevision($filename, $revision)
 145      {
 146          $filename = (string) $filename;
 147          $revision = (integer) $revision;
 148  
 149          if ($revision < 0) {
 150              $skip = abs($revision) - 1;
 151              $sortOrder = -1;
 152          } else {
 153              $skip = $revision;
 154              $sortOrder = 1;
 155          }
 156  
 157          return $this->filesCollection->findOne(
 158              ['filename' => $filename],
 159              [
 160                  'skip' => $skip,
 161                  'sort' => ['uploadDate' => $sortOrder],
 162                  'typeMap' => ['root' => 'stdClass'],
 163              ]
 164          );
 165      }
 166  
 167      /**
 168       * Finds a GridFS file document for a given ID.
 169       *
 170       * @param mixed $id
 171       * @return stdClass|null
 172       */
 173      public function findFileById($id)
 174      {
 175          return $this->filesCollection->findOne(
 176              ['_id' => $id],
 177              ['typeMap' => ['root' => 'stdClass']]
 178          );
 179      }
 180  
 181      /**
 182       * Finds documents from the GridFS bucket's files collection.
 183       *
 184       * @see Find::__construct() for supported options
 185       * @param array|object $filter  Query by which to filter documents
 186       * @param array        $options Additional options
 187       * @return Cursor
 188       */
 189      public function findFiles($filter, array $options = [])
 190      {
 191          return $this->filesCollection->find($filter, $options);
 192      }
 193  
 194      /**
 195       * Finds a single document from the GridFS bucket's files collection.
 196       *
 197       * @param array|object $filter  Query by which to filter documents
 198       * @param array        $options Additional options
 199       * @return array|object|null
 200       */
 201      public function findOneFile($filter, array $options = [])
 202      {
 203          return $this->filesCollection->findOne($filter, $options);
 204      }
 205  
 206      /**
 207       * Return the bucket name.
 208       *
 209       * @return string
 210       */
 211      public function getBucketName()
 212      {
 213          return $this->bucketName;
 214      }
 215  
 216      /**
 217       * Return the chunks collection.
 218       *
 219       * @return Collection
 220       */
 221      public function getChunksCollection()
 222      {
 223          return $this->chunksCollection;
 224      }
 225  
 226      /**
 227       * Return the database name.
 228       *
 229       * @return string
 230       */
 231      public function getDatabaseName()
 232      {
 233          return $this->databaseName;
 234      }
 235  
 236      /**
 237       * Return the files collection.
 238       *
 239       * @return Collection
 240       */
 241      public function getFilesCollection()
 242      {
 243          return $this->filesCollection;
 244      }
 245  
 246      /**
 247       * Inserts a document into the chunks collection.
 248       *
 249       * @param array|object $chunk Chunk document
 250       */
 251      public function insertChunk($chunk)
 252      {
 253          if (! $this->checkedIndexes) {
 254              $this->ensureIndexes();
 255          }
 256  
 257          $this->chunksCollection->insertOne($chunk);
 258      }
 259  
 260      /**
 261       * Inserts a document into the files collection.
 262       *
 263       * The file document should be inserted after all chunks have been inserted.
 264       *
 265       * @param array|object $file File document
 266       */
 267      public function insertFile($file)
 268      {
 269          if (! $this->checkedIndexes) {
 270              $this->ensureIndexes();
 271          }
 272  
 273          $this->filesCollection->insertOne($file);
 274      }
 275  
 276      /**
 277       * Updates the filename field in the file document for a given ID.
 278       *
 279       * @param mixed  $id
 280       * @param string $filename
 281       * @return UpdateResult
 282       */
 283      public function updateFilenameForId($id, $filename)
 284      {
 285          return $this->filesCollection->updateOne(
 286              ['_id' => $id],
 287              ['$set' => ['filename' => (string) $filename]]
 288          );
 289      }
 290  
 291      /**
 292       * Create an index on the chunks collection if it does not already exist.
 293       */
 294      private function ensureChunksIndex()
 295      {
 296          $expectedIndex = ['files_id' => 1, 'n' => 1];
 297  
 298          foreach ($this->chunksCollection->listIndexes() as $index) {
 299              if ($index->isUnique() && $this->indexKeysMatch($expectedIndex, $index->getKey())) {
 300                  return;
 301              }
 302          }
 303  
 304          $this->chunksCollection->createIndex($expectedIndex, ['unique' => true]);
 305      }
 306  
 307      /**
 308       * Create an index on the files collection if it does not already exist.
 309       */
 310      private function ensureFilesIndex()
 311      {
 312          $expectedIndex = ['filename' => 1, 'uploadDate' => 1];
 313  
 314          foreach ($this->filesCollection->listIndexes() as $index) {
 315              if ($this->indexKeysMatch($expectedIndex, $index->getKey())) {
 316                  return;
 317              }
 318          }
 319  
 320          $this->filesCollection->createIndex($expectedIndex);
 321      }
 322  
 323      /**
 324       * Ensure indexes on the files and chunks collections exist.
 325       *
 326       * This method is called once before the first write operation on a GridFS
 327       * bucket. Indexes are only be created if the files collection is empty.
 328       */
 329      private function ensureIndexes()
 330      {
 331          if ($this->checkedIndexes) {
 332              return;
 333          }
 334  
 335          $this->checkedIndexes = true;
 336  
 337          if (! $this->isFilesCollectionEmpty()) {
 338              return;
 339          }
 340  
 341          $this->ensureFilesIndex();
 342          $this->ensureChunksIndex();
 343      }
 344  
 345      private function indexKeysMatch(array $expectedKeys, array $actualKeys) : bool
 346      {
 347          if (count($expectedKeys) !== count($actualKeys)) {
 348              return false;
 349          }
 350  
 351          $iterator = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
 352          $iterator->attachIterator(new ArrayIterator($expectedKeys));
 353          $iterator->attachIterator(new ArrayIterator($actualKeys));
 354  
 355          foreach ($iterator as $key => $value) {
 356              list($expectedKey, $actualKey)     = $key;
 357              list($expectedValue, $actualValue) = $value;
 358  
 359              if ($expectedKey !== $actualKey) {
 360                  return false;
 361              }
 362  
 363              /* Since we don't expect special indexes (e.g. text), we mark any
 364               * index with a non-numeric definition as unequal. All others are
 365               * compared against their int value to avoid differences due to
 366               * some drivers using float values in the key specification. */
 367              if (! is_numeric($actualValue) || (int) $expectedValue !== (int) $actualValue) {
 368                  return false;
 369              }
 370          }
 371  
 372          return true;
 373      }
 374  
 375      /**
 376       * Returns whether the files collection is empty.
 377       *
 378       * @return boolean
 379       */
 380      private function isFilesCollectionEmpty()
 381      {
 382          return null === $this->filesCollection->findOne([], [
 383              'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
 384              'projection' => ['_id' => 1],
 385              'typeMap' => [],
 386          ]);
 387      }
 388  }