19
19
20
20
use MongoDB \Exception \InvalidArgumentException ;
21
21
use MongoDB \GridFS \Exception \CorruptFileException ;
22
+ use IteratorIterator ;
22
23
use stdClass ;
23
24
24
25
/**
29
30
class ReadableStream
30
31
{
31
32
private $ buffer ;
32
- private $ bufferEmpty ;
33
- private $ bufferFresh ;
34
- private $ bytesSeen = 0 ;
33
+ private $ bufferOffset = 0 ;
35
34
private $ chunkSize ;
36
35
private $ chunkOffset = 0 ;
37
36
private $ chunksIterator ;
38
37
private $ collectionWrapper ;
38
+ private $ expectedLastChunkSize = 0 ;
39
39
private $ file ;
40
- private $ firstCheck = true ;
41
- private $ iteratorEmpty = false ;
42
40
private $ length ;
43
- private $ numChunks ;
41
+ private $ numChunks = 0 ;
44
42
45
43
/**
46
44
* Constructs a readable GridFS stream.
@@ -64,13 +62,15 @@ public function __construct(CollectionWrapper $collectionWrapper, stdClass $file
64
62
}
65
63
66
64
$ 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 ;
69
67
70
- $ this ->chunksIterator = $ collectionWrapper ->getChunksIteratorByFilesId ($ file ->_id );
71
68
$ 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
+ }
74
74
}
75
75
76
76
/**
@@ -90,56 +90,7 @@ public function __debugInfo()
90
90
91
91
public function close ()
92
92
{
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
143
94
}
144
95
145
96
/**
@@ -162,58 +113,123 @@ public function getSize()
162
113
return $ this ->length ;
163
114
}
164
115
116
+ /**
117
+ * Return whether the current read position is at the end of the stream.
118
+ *
119
+ * @return boolean
120
+ */
165
121
public function isEOF ()
166
122
{
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 ;
168
128
}
169
129
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 )
171
141
{
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
+ }
174
145
175
- return false ;
146
+ if ($ this ->chunksIterator === null ) {
147
+ $ this ->initChunksIterator ();
148
+ }
149
+
150
+ if ($ this ->buffer === null && ! $ this ->initBufferFromCurrentChunk ()) {
151
+ return '' ;
176
152
}
177
153
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 ;
183
179
}
184
180
185
181
if ( ! $ this ->chunksIterator ->valid ()) {
186
182
throw CorruptFileException::missingChunk ($ this ->chunkOffset );
187
183
}
188
184
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 );
191
189
}
192
190
193
- $ actualChunkSize = strlen ( $ this -> chunksIterator -> current ()-> data ->getData () );
191
+ $ this -> buffer = $ currentChunk -> data ->getData ();
194
192
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
197
197
: $ this ->chunkSize ;
198
198
199
- if ($ actualChunkSize != $ expectedChunkSize ) {
199
+ if ($ actualChunkSize !== $ expectedChunkSize ) {
200
200
throw CorruptFileException::unexpectedSize ($ actualChunkSize , $ expectedChunkSize );
201
201
}
202
202
203
- $ this ->bytesSeen += $ actualChunkSize ;
204
- $ this ->chunkOffset ++;
205
-
206
203
return true ;
207
204
}
208
205
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 ()
210
213
{
211
- if (isset ( $ this ->buffer ) ) {
212
- fclose ( $ this -> buffer ) ;
214
+ if ($ this ->chunkOffset === $ this -> numChunks - 1 ) {
215
+ return false ;
213
216
}
214
217
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 ();
218
234
}
219
235
}
0 commit comments