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