Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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