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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body