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