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 HashContext;
  21  use MongoDB\BSON\Binary;
  22  use MongoDB\BSON\ObjectId;
  23  use MongoDB\BSON\UTCDateTime;
  24  use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
  25  use MongoDB\Exception\InvalidArgumentException;
  26  
  27  use function array_intersect_key;
  28  use function hash_final;
  29  use function hash_init;
  30  use function hash_update;
  31  use function is_array;
  32  use function is_bool;
  33  use function is_integer;
  34  use function is_object;
  35  use function is_string;
  36  use function MongoDB\is_string_array;
  37  use function sprintf;
  38  use function strlen;
  39  use function substr;
  40  
  41  /**
  42   * WritableStream abstracts the process of writing a GridFS file.
  43   *
  44   * @internal
  45   */
  46  class WritableStream
  47  {
  48      /** @var integer */
  49      private static $defaultChunkSizeBytes = 261120;
  50  
  51      /** @var string */
  52      private $buffer = '';
  53  
  54      /** @var integer */
  55      private $chunkOffset = 0;
  56  
  57      /** @var integer */
  58      private $chunkSize;
  59  
  60      /** @var boolean */
  61      private $disableMD5;
  62  
  63      /** @var CollectionWrapper */
  64      private $collectionWrapper;
  65  
  66      /** @var array */
  67      private $file;
  68  
  69      /** @var HashContext|null */
  70      private $hashCtx;
  71  
  72      /** @var boolean */
  73      private $isClosed = false;
  74  
  75      /** @var integer */
  76      private $length = 0;
  77  
  78      /**
  79       * Constructs a writable GridFS stream.
  80       *
  81       * Supported options:
  82       *
  83       *  * _id (mixed): File document identifier. Defaults to a new ObjectId.
  84       *
  85       *  * aliases (array of strings): DEPRECATED An array of aliases.
  86       *    Applications wishing to store aliases should add an aliases field to
  87       *    the metadata document instead.
  88       *
  89       *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
  90       *    261120 (i.e. 255 KiB).
  91       *
  92       *  * disableMD5 (boolean): When true, no MD5 sum will be generated.
  93       *    Defaults to "false".
  94       *
  95       *  * contentType (string): DEPRECATED content type to be stored with the
  96       *    file. This information should now be added to the metadata.
  97       *
  98       *  * metadata (document): User data for the "metadata" field of the files
  99       *    collection document.
 100       *
 101       * @param CollectionWrapper $collectionWrapper GridFS collection wrapper
 102       * @param string            $filename          Filename
 103       * @param array             $options           Upload options
 104       * @throws InvalidArgumentException
 105       */
 106      public function __construct(CollectionWrapper $collectionWrapper, string $filename, array $options = [])
 107      {
 108          $options += [
 109              '_id' => new ObjectId(),
 110              'chunkSizeBytes' => self::$defaultChunkSizeBytes,
 111              'disableMD5' => false,
 112          ];
 113  
 114          if (isset($options['aliases']) && ! is_string_array($options['aliases'])) {
 115              throw InvalidArgumentException::invalidType('"aliases" option', $options['aliases'], 'array of strings');
 116          }
 117  
 118          if (! is_integer($options['chunkSizeBytes'])) {
 119              throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
 120          }
 121  
 122          if ($options['chunkSizeBytes'] < 1) {
 123              throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes']));
 124          }
 125  
 126          if (! is_bool($options['disableMD5'])) {
 127              throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean');
 128          }
 129  
 130          if (isset($options['contentType']) && ! is_string($options['contentType'])) {
 131              throw InvalidArgumentException::invalidType('"contentType" option', $options['contentType'], 'string');
 132          }
 133  
 134          if (isset($options['metadata']) && ! is_array($options['metadata']) && ! is_object($options['metadata'])) {
 135              throw InvalidArgumentException::invalidType('"metadata" option', $options['metadata'], 'array or object');
 136          }
 137  
 138          $this->chunkSize = $options['chunkSizeBytes'];
 139          $this->collectionWrapper = $collectionWrapper;
 140          $this->disableMD5 = $options['disableMD5'];
 141  
 142          if (! $this->disableMD5) {
 143              $this->hashCtx = hash_init('md5');
 144          }
 145  
 146          $this->file = [
 147              '_id' => $options['_id'],
 148              'chunkSize' => $this->chunkSize,
 149              'filename' => $filename,
 150          ] + array_intersect_key($options, ['aliases' => 1, 'contentType' => 1, 'metadata' => 1]);
 151      }
 152  
 153      /**
 154       * Return internal properties for debugging purposes.
 155       *
 156       * @see https://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
 157       */
 158      public function __debugInfo(): array
 159      {
 160          return [
 161              'bucketName' => $this->collectionWrapper->getBucketName(),
 162              'databaseName' => $this->collectionWrapper->getDatabaseName(),
 163              'file' => $this->file,
 164          ];
 165      }
 166  
 167      /**
 168       * Closes an active stream and flushes all buffered data to GridFS.
 169       */
 170      public function close(): void
 171      {
 172          if ($this->isClosed) {
 173              // TODO: Should this be an error condition? e.g. BadMethodCallException
 174              return;
 175          }
 176  
 177          if (strlen($this->buffer) > 0) {
 178              $this->insertChunkFromBuffer();
 179          }
 180  
 181          $this->fileCollectionInsert();
 182          $this->isClosed = true;
 183      }
 184  
 185      /**
 186       * Return the stream's file document.
 187       */
 188      public function getFile(): object
 189      {
 190          return (object) $this->file;
 191      }
 192  
 193      /**
 194       * Return the stream's size in bytes.
 195       *
 196       * Note: this value will increase as more data is written to the stream.
 197       */
 198      public function getSize(): int
 199      {
 200          return $this->length + strlen($this->buffer);
 201      }
 202  
 203      /**
 204       * Return the current position of the stream.
 205       *
 206       * This is the offset within the stream where the next byte would be
 207       * written. Since seeking is not supported and writes are appended, this is
 208       * always the end of the stream.
 209       *
 210       * @see WritableStream::getSize()
 211       */
 212      public function tell(): int
 213      {
 214          return $this->getSize();
 215      }
 216  
 217      /**
 218       * Inserts binary data into GridFS via chunks.
 219       *
 220       * Data will be buffered internally until chunkSizeBytes are accumulated, at
 221       * which point a chunk document will be inserted and the buffer reset.
 222       *
 223       * @param string $data Binary data to write
 224       */
 225      public function writeBytes(string $data): int
 226      {
 227          if ($this->isClosed) {
 228              // TODO: Should this be an error condition? e.g. BadMethodCallException
 229              return 0;
 230          }
 231  
 232          $bytesRead = 0;
 233  
 234          while ($bytesRead != strlen($data)) {
 235              $initialBufferLength = strlen($this->buffer);
 236              $this->buffer .= substr($data, $bytesRead, $this->chunkSize - $initialBufferLength);
 237              $bytesRead += strlen($this->buffer) - $initialBufferLength;
 238  
 239              if (strlen($this->buffer) == $this->chunkSize) {
 240                  $this->insertChunkFromBuffer();
 241              }
 242          }
 243  
 244          return $bytesRead;
 245      }
 246  
 247      private function abort(): void
 248      {
 249          try {
 250              $this->collectionWrapper->deleteChunksByFilesId($this->file['_id']);
 251          } catch (DriverRuntimeException $e) {
 252              // We are already handling an error if abort() is called, so suppress this
 253          }
 254  
 255          $this->isClosed = true;
 256      }
 257  
 258      /**
 259       * @return mixed
 260       */
 261      private function fileCollectionInsert()
 262      {
 263          $this->file['length'] = $this->length;
 264          $this->file['uploadDate'] = new UTCDateTime();
 265  
 266          if (! $this->disableMD5 && $this->hashCtx) {
 267              $this->file['md5'] = hash_final($this->hashCtx);
 268          }
 269  
 270          try {
 271              $this->collectionWrapper->insertFile($this->file);
 272          } catch (DriverRuntimeException $e) {
 273              $this->abort();
 274  
 275              throw $e;
 276          }
 277  
 278          return $this->file['_id'];
 279      }
 280  
 281      private function insertChunkFromBuffer(): void
 282      {
 283          if (strlen($this->buffer) == 0) {
 284              return;
 285          }
 286  
 287          $data = $this->buffer;
 288          $this->buffer = '';
 289  
 290          $chunk = [
 291              'files_id' => $this->file['_id'],
 292              'n' => $this->chunkOffset,
 293              'data' => new Binary($data, Binary::TYPE_GENERIC),
 294          ];
 295  
 296          if (! $this->disableMD5 && $this->hashCtx) {
 297              hash_update($this->hashCtx, $data);
 298          }
 299  
 300          try {
 301              $this->collectionWrapper->insertChunk($chunk);
 302          } catch (DriverRuntimeException $e) {
 303              $this->abort();
 304  
 305              throw $e;
 306          }
 307  
 308          $this->length += strlen($data);
 309          $this->chunkOffset++;
 310      }
 311  }