Skip to content

Commit 0b1e0f3

Browse files
committed
Merge pull request #323
2 parents dccf20a + b3a8b00 commit 0b1e0f3

File tree

4 files changed

+139
-122
lines changed

4 files changed

+139
-122
lines changed

src/GridFS/CollectionWrapper.php

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use MongoDB\Driver\Cursor;
2323
use MongoDB\Driver\Manager;
2424
use MongoDB\Driver\ReadPreference;
25-
use IteratorIterator;
2625
use stdClass;
2726

2827
/**
@@ -87,6 +86,27 @@ public function dropCollections()
8786
$this->chunksCollection->drop(['typeMap' => []]);
8887
}
8988

89+
/**
90+
* Finds GridFS chunk documents for a given file ID and optional offset.
91+
*
92+
* @param mixed $id File ID
93+
* @param integer $fromChunk Starting chunk (inclusive)
94+
* @return Cursor
95+
*/
96+
public function findChunksByFileId($id, $fromChunk = 0)
97+
{
98+
return $this->chunksCollection->find(
99+
[
100+
'files_id' => $id,
101+
'n' => ['$gte' => $fromChunk],
102+
],
103+
[
104+
'sort' => ['n' => 1],
105+
'typeMap' => ['root' => 'stdClass'],
106+
]
107+
);
108+
}
109+
90110
/**
91111
* Finds a GridFS file document for a given filename and revision.
92112
*
@@ -177,25 +197,6 @@ public function getBucketName()
177197
return $this->bucketName;
178198
}
179199

180-
/**
181-
* Returns a chunks iterator for a given file ID.
182-
*
183-
* @param mixed $id
184-
* @return IteratorIterator
185-
*/
186-
public function getChunksIteratorByFilesId($id)
187-
{
188-
$cursor = $this->chunksCollection->find(
189-
['files_id' => $id],
190-
[
191-
'sort' => ['n' => 1],
192-
'typeMap' => ['root' => 'stdClass'],
193-
]
194-
);
195-
196-
return new IteratorIterator($cursor);
197-
}
198-
199200
/**
200201
* Return the database name.
201202
*

src/GridFS/ReadableStream.php

Lines changed: 102 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use MongoDB\Exception\InvalidArgumentException;
2121
use MongoDB\GridFS\Exception\CorruptFileException;
22+
use IteratorIterator;
2223
use stdClass;
2324

2425
/**
@@ -29,18 +30,15 @@
2930
class ReadableStream
3031
{
3132
private $buffer;
32-
private $bufferEmpty;
33-
private $bufferFresh;
34-
private $bytesSeen = 0;
33+
private $bufferOffset = 0;
3534
private $chunkSize;
3635
private $chunkOffset = 0;
3736
private $chunksIterator;
3837
private $collectionWrapper;
38+
private $expectedLastChunkSize = 0;
3939
private $file;
40-
private $firstCheck = true;
41-
private $iteratorEmpty = false;
4240
private $length;
43-
private $numChunks;
41+
private $numChunks = 0;
4442

4543
/**
4644
* Constructs a readable GridFS stream.
@@ -64,13 +62,15 @@ public function __construct(CollectionWrapper $collectionWrapper, stdClass $file
6462
}
6563

6664
$this->file = $file;
67-
$this->chunkSize = $file->chunkSize;
68-
$this->length = $file->length;
65+
$this->chunkSize = (integer) $file->chunkSize;
66+
$this->length = (integer) $file->length;
6967

70-
$this->chunksIterator = $collectionWrapper->getChunksIteratorByFilesId($file->_id);
7168
$this->collectionWrapper = $collectionWrapper;
72-
$this->numChunks = ceil($this->length / $this->chunkSize);
73-
$this->initEmptyBuffer();
69+
70+
if ($this->length > 0) {
71+
$this->numChunks = (integer) ceil($this->length / $this->chunkSize);
72+
$this->expectedLastChunkSize = ($this->length - (($this->numChunks - 1) * $this->chunkSize));
73+
}
7474
}
7575

7676
/**
@@ -90,56 +90,7 @@ public function __debugInfo()
9090

9191
public function close()
9292
{
93-
fclose($this->buffer);
94-
}
95-
96-
/**
97-
* Read bytes from the stream.
98-
*
99-
* Note: this method may return a string smaller than the requested length
100-
* if data is not available to be read.
101-
*
102-
* @param integer $numBytes Number of bytes to read
103-
* @return string
104-
* @throws InvalidArgumentException if $numBytes is negative
105-
*/
106-
public function downloadNumBytes($numBytes)
107-
{
108-
if ($numBytes < 0) {
109-
throw new InvalidArgumentException(sprintf('$numBytes must be >= zero; given: %d', $numBytes));
110-
}
111-
112-
if ($numBytes == 0) {
113-
return '';
114-
}
115-
116-
if ($this->bufferFresh) {
117-
rewind($this->buffer);
118-
$this->bufferFresh = false;
119-
}
120-
121-
// TODO: Should we be checking for fread errors here?
122-
$output = fread($this->buffer, $numBytes);
123-
124-
if (strlen($output) == $numBytes) {
125-
return $output;
126-
}
127-
128-
$this->initEmptyBuffer();
129-
130-
$bytesLeft = $numBytes - strlen($output);
131-
132-
while (strlen($output) < $numBytes && $this->advanceChunks()) {
133-
$bytesLeft = $numBytes - strlen($output);
134-
$output .= substr($this->chunksIterator->current()->data->getData(), 0, $bytesLeft);
135-
}
136-
137-
if ( ! $this->iteratorEmpty && $this->length > 0 && $bytesLeft < strlen($this->chunksIterator->current()->data->getData())) {
138-
fwrite($this->buffer, substr($this->chunksIterator->current()->data->getData(), $bytesLeft));
139-
$this->bufferEmpty = false;
140-
}
141-
142-
return $output;
93+
// Nothing to do
14394
}
14495

14596
/**
@@ -162,58 +113,123 @@ public function getSize()
162113
return $this->length;
163114
}
164115

116+
/**
117+
* Return whether the current read position is at the end of the stream.
118+
*
119+
* @return boolean
120+
*/
165121
public function isEOF()
166122
{
167-
return ($this->iteratorEmpty && $this->bufferEmpty);
123+
if ($this->chunkOffset === $this->numChunks - 1) {
124+
return $this->bufferOffset >= $this->expectedLastChunkSize;
125+
}
126+
127+
return $this->chunkOffset >= $this->numChunks;
168128
}
169129

170-
private function advanceChunks()
130+
/**
131+
* Read bytes from the stream.
132+
*
133+
* Note: this method may return a string smaller than the requested length
134+
* if data is not available to be read.
135+
*
136+
* @param integer $length Number of bytes to read
137+
* @return string
138+
* @throws InvalidArgumentException if $length is negative
139+
*/
140+
public function readBytes($length)
171141
{
172-
if ($this->chunkOffset >= $this->numChunks) {
173-
$this->iteratorEmpty = true;
142+
if ($length < 0) {
143+
throw new InvalidArgumentException(sprintf('$length must be >= 0; given: %d', $length));
144+
}
174145

175-
return false;
146+
if ($this->chunksIterator === null) {
147+
$this->initChunksIterator();
148+
}
149+
150+
if ($this->buffer === null && ! $this->initBufferFromCurrentChunk()) {
151+
return '';
176152
}
177153

178-
if ($this->firstCheck) {
179-
$this->chunksIterator->rewind();
180-
$this->firstCheck = false;
181-
} else {
182-
$this->chunksIterator->next();
154+
$data = '';
155+
156+
while (strlen($data) < $length) {
157+
if ($this->bufferOffset >= strlen($this->buffer) && ! $this->initBufferFromNextChunk()) {
158+
break;
159+
}
160+
161+
$initialDataLength = strlen($data);
162+
$data .= substr($this->buffer, $this->bufferOffset, $length - $initialDataLength);
163+
$this->bufferOffset += strlen($data) - $initialDataLength;
164+
}
165+
166+
return $data;
167+
}
168+
169+
/**
170+
* Initialize the buffer to the current chunk's data.
171+
*
172+
* @return boolean Whether there was a current chunk to read
173+
* @throws CorruptFileException if an expected chunk could not be read successfully
174+
*/
175+
private function initBufferFromCurrentChunk()
176+
{
177+
if ($this->chunkOffset === 0 && $this->numChunks === 0) {
178+
return false;
183179
}
184180

185181
if ( ! $this->chunksIterator->valid()) {
186182
throw CorruptFileException::missingChunk($this->chunkOffset);
187183
}
188184

189-
if ($this->chunksIterator->current()->n != $this->chunkOffset) {
190-
throw CorruptFileException::unexpectedIndex($this->chunksIterator->current()->n, $this->chunkOffset);
185+
$currentChunk = $this->chunksIterator->current();
186+
187+
if ($currentChunk->n !== $this->chunkOffset) {
188+
throw CorruptFileException::unexpectedIndex($currentChunk->n, $this->chunkOffset);
191189
}
192190

193-
$actualChunkSize = strlen($this->chunksIterator->current()->data->getData());
191+
$this->buffer = $currentChunk->data->getData();
194192

195-
$expectedChunkSize = ($this->chunkOffset == $this->numChunks - 1)
196-
? ($this->length - $this->bytesSeen)
193+
$actualChunkSize = strlen($this->buffer);
194+
195+
$expectedChunkSize = ($this->chunkOffset === $this->numChunks - 1)
196+
? $this->expectedLastChunkSize
197197
: $this->chunkSize;
198198

199-
if ($actualChunkSize != $expectedChunkSize) {
199+
if ($actualChunkSize !== $expectedChunkSize) {
200200
throw CorruptFileException::unexpectedSize($actualChunkSize, $expectedChunkSize);
201201
}
202202

203-
$this->bytesSeen += $actualChunkSize;
204-
$this->chunkOffset++;
205-
206203
return true;
207204
}
208205

209-
private function initEmptyBuffer()
206+
/**
207+
* Advance to the next chunk and initialize the buffer to its data.
208+
*
209+
* @return boolean Whether there was a next chunk to read
210+
* @throws CorruptFileException if an expected chunk could not be read successfully
211+
*/
212+
private function initBufferFromNextChunk()
210213
{
211-
if (isset($this->buffer)) {
212-
fclose($this->buffer);
214+
if ($this->chunkOffset === $this->numChunks - 1) {
215+
return false;
213216
}
214217

215-
$this->buffer = fopen("php://memory", "w+b");
216-
$this->bufferEmpty = true;
217-
$this->bufferFresh = true;
218+
$this->bufferOffset = 0;
219+
$this->chunkOffset++;
220+
$this->chunksIterator->next();
221+
222+
return $this->initBufferFromCurrentChunk();
223+
}
224+
225+
/**
226+
* Initializes the chunk iterator starting from the current offset.
227+
*/
228+
private function initChunksIterator()
229+
{
230+
$cursor = $this->collectionWrapper->findChunksByFileId($this->file->_id, $this->chunkOffset);
231+
232+
$this->chunksIterator = new IteratorIterator($cursor);
233+
$this->chunksIterator->rewind();
218234
}
219235
}

src/GridFS/StreamWrapper.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,17 @@ public function stream_open($path, $mode, $options, &$openedPath)
119119
* if data is not available to be read.
120120
*
121121
* @see http://php.net/manual/en/streamwrapper.stream-read.php
122-
* @param integer $count Number of bytes to read
122+
* @param integer $length Number of bytes to read
123123
* @return string
124124
*/
125-
public function stream_read($count)
125+
public function stream_read($length)
126126
{
127127
if ( ! $this->stream instanceof ReadableStream) {
128128
return '';
129129
}
130130

131131
try {
132-
return $this->stream->downloadNumBytes($count);
132+
return $this->stream->readBytes($length);
133133
} catch (Exception $e) {
134134
trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), \E_USER_WARNING);
135135
return false;

0 commit comments

Comments
 (0)