See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body