1919
2020use MongoDB \Exception \InvalidArgumentException ;
2121use MongoDB \GridFS \Exception \CorruptFileException ;
22+ use IteratorIterator ;
2223use stdClass ;
2324
2425/**
2930class 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}
0 commit comments