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\BSON\Binary;
  21  use MongoDB\Driver\Cursor;
  22  use MongoDB\Exception\InvalidArgumentException;
  23  use MongoDB\GridFS\Exception\CorruptFileException;
  24  
  25  use function assert;
  26  use function ceil;
  27  use function floor;
  28  use function is_integer;
  29  use function is_object;
  30  use function property_exists;
  31  use function sprintf;
  32  use function strlen;
  33  use function substr;
  34  
  35  /**
  36   * ReadableStream abstracts the process of reading a GridFS file.
  37   *
  38   * @internal
  39   */
  40  class ReadableStream
  41  {
  42      /** @var string|null */
  43      private $buffer;
  44  
  45      /** @var integer */
  46      private $bufferOffset = 0;
  47  
  48      /** @var integer */
  49      private $chunkSize;
  50  
  51      /** @var integer */
  52      private $chunkOffset = 0;
  53  
  54      /** @var Cursor|null */
  55      private $chunksIterator;
  56  
  57      /** @var CollectionWrapper */
  58      private $collectionWrapper;
  59  
  60      /** @var integer */
  61      private $expectedLastChunkSize = 0;
  62  
  63      /** @var object */
  64      private $file;
  65  
  66      /** @var integer */
  67      private $length;
  68  
  69      /** @var integer */
  70      private $numChunks = 0;
  71  
  72      /**
  73       * Constructs a readable GridFS stream.
  74       *
  75       * @param CollectionWrapper $collectionWrapper GridFS collection wrapper
  76       * @param object            $file              GridFS file document
  77       * @throws CorruptFileException
  78       */
  79      public function __construct(CollectionWrapper $collectionWrapper, object $file)
  80      {
  81          if (! isset($file->chunkSize) || ! is_integer($file->chunkSize) || $file->chunkSize < 1) {
  82              throw new CorruptFileException('file.chunkSize is not an integer >= 1');
  83          }
  84  
  85          if (! isset($file->length) || ! is_integer($file->length) || $file->length < 0) {
  86              throw new CorruptFileException('file.length is not an integer > 0');
  87          }
  88  
  89          if (! isset($file->_id) && ! property_exists($file, '_id')) {
  90              throw new CorruptFileException('file._id does not exist');
  91          }
  92  
  93          $this->file = $file;
  94          $this->chunkSize = $file->chunkSize;
  95          $this->length = $file->length;
  96  
  97          $this->collectionWrapper = $collectionWrapper;
  98  
  99          if ($this->length > 0) {
 100              $this->numChunks = (integer) ceil($this->length / $this->chunkSize);
 101              $this->expectedLastChunkSize = $this->length - (($this->numChunks - 1) * $this->chunkSize);
 102          }
 103      }
 104  
 105      /**
 106       * Return internal properties for debugging purposes.
 107       *
 108       * @see https://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
 109       * @return array
 110       */
 111      public function __debugInfo(): array
 112      {
 113          return [
 114              'bucketName' => $this->collectionWrapper->getBucketName(),
 115              'databaseName' => $this->collectionWrapper->getDatabaseName(),
 116              'file' => $this->file,
 117          ];
 118      }
 119  
 120      public function close(): void
 121      {
 122          // Nothing to do
 123      }
 124  
 125      public function getFile(): object
 126      {
 127          return $this->file;
 128      }
 129  
 130      public function getSize(): int
 131      {
 132          return $this->length;
 133      }
 134  
 135      /**
 136       * Return whether the current read position is at the end of the stream.
 137       */
 138      public function isEOF(): bool
 139      {
 140          if ($this->chunkOffset === $this->numChunks - 1) {
 141              return $this->bufferOffset >= $this->expectedLastChunkSize;
 142          }
 143  
 144          return $this->chunkOffset >= $this->numChunks;
 145      }
 146  
 147      /**
 148       * Read bytes from the stream.
 149       *
 150       * Note: this method may return a string smaller than the requested length
 151       * if data is not available to be read.
 152       *
 153       * @param integer $length Number of bytes to read
 154       * @throws InvalidArgumentException if $length is negative
 155       */
 156      public function readBytes(int $length): string
 157      {
 158          if ($length < 0) {
 159              throw new InvalidArgumentException(sprintf('$length must be >= 0; given: %d', $length));
 160          }
 161  
 162          if ($this->chunksIterator === null) {
 163              $this->initChunksIterator();
 164          }
 165  
 166          if ($this->buffer === null && ! $this->initBufferFromCurrentChunk()) {
 167              return '';
 168          }
 169  
 170          assert($this->buffer !== null);
 171  
 172          $data = '';
 173  
 174          while (strlen($data) < $length) {
 175              if ($this->bufferOffset >= strlen($this->buffer) && ! $this->initBufferFromNextChunk()) {
 176                  break;
 177              }
 178  
 179              $initialDataLength = strlen($data);
 180              $data .= substr($this->buffer, $this->bufferOffset, $length - $initialDataLength);
 181              $this->bufferOffset += strlen($data) - $initialDataLength;
 182          }
 183  
 184          return $data;
 185      }
 186  
 187      /**
 188       * Seeks the chunk and buffer offsets for the next read operation.
 189       *
 190       * @throws InvalidArgumentException if $offset is out of range
 191       */
 192      public function seek(int $offset): void
 193      {
 194          if ($offset < 0 || $offset > $this->file->length) {
 195              throw new InvalidArgumentException(sprintf('$offset must be >= 0 and <= %d; given: %d', $this->file->length, $offset));
 196          }
 197  
 198          /* Compute the offsets for the chunk and buffer (i.e. chunk data) from
 199           * which we will expect to read after seeking. If the chunk offset
 200           * changed, we'll also need to reset the buffer.
 201           */
 202          $lastChunkOffset = $this->chunkOffset;
 203          $this->chunkOffset = (integer) floor($offset / $this->chunkSize);
 204          $this->bufferOffset = $offset % $this->chunkSize;
 205  
 206          if ($lastChunkOffset === $this->chunkOffset) {
 207              return;
 208          }
 209  
 210          if ($this->chunksIterator === null) {
 211              return;
 212          }
 213  
 214          // Clear the buffer since the current chunk will be changed
 215          $this->buffer = null;
 216  
 217          /* If we are seeking to a previous chunk, we need to reinitialize the
 218           * chunk iterator.
 219           */
 220          if ($lastChunkOffset > $this->chunkOffset) {
 221              $this->chunksIterator = null;
 222  
 223              return;
 224          }
 225  
 226          /* If we are seeking to a subsequent chunk, we do not need to
 227           * reinitalize the chunk iterator. Instead, we can simply move forward
 228           * to $this->chunkOffset.
 229           */
 230          $numChunks = $this->chunkOffset - $lastChunkOffset;
 231          for ($i = 0; $i < $numChunks; $i++) {
 232              $this->chunksIterator->next();
 233          }
 234      }
 235  
 236      /**
 237       * Return the current position of the stream.
 238       *
 239       * This is the offset within the stream where the next byte would be read.
 240       */
 241      public function tell(): int
 242      {
 243          return ($this->chunkOffset * $this->chunkSize) + $this->bufferOffset;
 244      }
 245  
 246      /**
 247       * Initialize the buffer to the current chunk's data.
 248       *
 249       * @return boolean Whether there was a current chunk to read
 250       * @throws CorruptFileException if an expected chunk could not be read successfully
 251       */
 252      private function initBufferFromCurrentChunk(): bool
 253      {
 254          if ($this->chunkOffset === 0 && $this->numChunks === 0) {
 255              return false;
 256          }
 257  
 258          if ($this->chunksIterator === null) {
 259              return false;
 260          }
 261  
 262          if (! $this->chunksIterator->valid()) {
 263              throw CorruptFileException::missingChunk($this->chunkOffset);
 264          }
 265  
 266          $currentChunk = $this->chunksIterator->current();
 267          assert(is_object($currentChunk));
 268  
 269          if ($currentChunk->n !== $this->chunkOffset) {
 270              throw CorruptFileException::unexpectedIndex($currentChunk->n, $this->chunkOffset);
 271          }
 272  
 273          if (! $currentChunk->data instanceof Binary) {
 274              throw CorruptFileException::invalidChunkData($this->chunkOffset);
 275          }
 276  
 277          $this->buffer = $currentChunk->data->getData();
 278  
 279          $actualChunkSize = strlen($this->buffer);
 280  
 281          $expectedChunkSize = $this->chunkOffset === $this->numChunks - 1
 282              ? $this->expectedLastChunkSize
 283              : $this->chunkSize;
 284  
 285          if ($actualChunkSize !== $expectedChunkSize) {
 286              throw CorruptFileException::unexpectedSize($actualChunkSize, $expectedChunkSize);
 287          }
 288  
 289          return true;
 290      }
 291  
 292      /**
 293       * Advance to the next chunk and initialize the buffer to its data.
 294       *
 295       * @return boolean Whether there was a next chunk to read
 296       * @throws CorruptFileException if an expected chunk could not be read successfully
 297       */
 298      private function initBufferFromNextChunk(): bool
 299      {
 300          if ($this->chunkOffset === $this->numChunks - 1) {
 301              return false;
 302          }
 303  
 304          if ($this->chunksIterator === null) {
 305              return false;
 306          }
 307  
 308          $this->bufferOffset = 0;
 309          $this->chunkOffset++;
 310          $this->chunksIterator->next();
 311  
 312          return $this->initBufferFromCurrentChunk();
 313      }
 314  
 315      /**
 316       * Initializes the chunk iterator starting from the current offset.
 317       */
 318      private function initChunksIterator(): void
 319      {
 320          $this->chunksIterator = $this->collectionWrapper->findChunksByFileId($this->file->_id, $this->chunkOffset);
 321          $this->chunksIterator->rewind();
 322      }
 323  }