4
4
5
5
use MongoDB \Exception \InvalidArgumentException ;
6
6
use MongoDB \GridFS \Exception \CorruptFileException ;
7
+ use IteratorIterator ;
7
8
use stdClass ;
8
9
9
10
/**
14
15
class ReadableStream
15
16
{
16
17
private $ buffer ;
17
- private $ bufferEmpty ;
18
- private $ bufferFresh ;
19
- private $ bytesSeen = 0 ;
18
+ private $ bufferOffset = 0 ;
20
19
private $ chunkSize ;
21
20
private $ chunkOffset = 0 ;
22
21
private $ chunksIterator ;
23
22
private $ collectionWrapper ;
23
+ private $ expectedLastChunkSize = 0 ;
24
24
private $ file ;
25
- private $ firstCheck = true ;
26
- private $ iteratorEmpty = false ;
27
25
private $ length ;
28
- private $ numChunks ;
26
+ private $ numChunks = 0 ;
29
27
30
28
/**
31
29
* Constructs a readable GridFS stream.
@@ -49,13 +47,15 @@ public function __construct(CollectionWrapper $collectionWrapper, stdClass $file
49
47
}
50
48
51
49
$ this ->file = $ file ;
52
- $ this ->chunkSize = $ file ->chunkSize ;
53
- $ this ->length = $ file ->length ;
50
+ $ this ->chunkSize = ( integer ) $ file ->chunkSize ;
51
+ $ this ->length = ( integer ) $ file ->length ;
54
52
55
- $ this ->chunksIterator = $ collectionWrapper ->getChunksIteratorByFilesId ($ file ->_id );
56
53
$ this ->collectionWrapper = $ collectionWrapper ;
57
- $ this ->numChunks = ceil ($ this ->length / $ this ->chunkSize );
58
- $ this ->initEmptyBuffer ();
54
+
55
+ if ($ this ->length > 0 ) {
56
+ $ this ->numChunks = (integer ) ceil ($ this ->length / $ this ->chunkSize );
57
+ $ this ->expectedLastChunkSize = ($ this ->length - (($ this ->numChunks - 1 ) * $ this ->chunkSize ));
58
+ }
59
59
}
60
60
61
61
/**
@@ -75,56 +75,7 @@ public function __debugInfo()
75
75
76
76
public function close ()
77
77
{
78
- fclose ($ this ->buffer );
79
- }
80
-
81
- /**
82
- * Read bytes from the stream.
83
- *
84
- * Note: this method may return a string smaller than the requested length
85
- * if data is not available to be read.
86
- *
87
- * @param integer $numBytes Number of bytes to read
88
- * @return string
89
- * @throws InvalidArgumentException if $numBytes is negative
90
- */
91
- public function downloadNumBytes ($ numBytes )
92
- {
93
- if ($ numBytes < 0 ) {
94
- throw new InvalidArgumentException (sprintf ('$numBytes must be >= zero; given: %d ' , $ numBytes ));
95
- }
96
-
97
- if ($ numBytes == 0 ) {
98
- return '' ;
99
- }
100
-
101
- if ($ this ->bufferFresh ) {
102
- rewind ($ this ->buffer );
103
- $ this ->bufferFresh = false ;
104
- }
105
-
106
- // TODO: Should we be checking for fread errors here?
107
- $ output = fread ($ this ->buffer , $ numBytes );
108
-
109
- if (strlen ($ output ) == $ numBytes ) {
110
- return $ output ;
111
- }
112
-
113
- $ this ->initEmptyBuffer ();
114
-
115
- $ bytesLeft = $ numBytes - strlen ($ output );
116
-
117
- while (strlen ($ output ) < $ numBytes && $ this ->advanceChunks ()) {
118
- $ bytesLeft = $ numBytes - strlen ($ output );
119
- $ output .= substr ($ this ->chunksIterator ->current ()->data ->getData (), 0 , $ bytesLeft );
120
- }
121
-
122
- if ( ! $ this ->iteratorEmpty && $ this ->length > 0 && $ bytesLeft < strlen ($ this ->chunksIterator ->current ()->data ->getData ())) {
123
- fwrite ($ this ->buffer , substr ($ this ->chunksIterator ->current ()->data ->getData (), $ bytesLeft ));
124
- $ this ->bufferEmpty = false ;
125
- }
126
-
127
- return $ output ;
78
+ // Nothing to do
128
79
}
129
80
130
81
/**
@@ -147,58 +98,123 @@ public function getSize()
147
98
return $ this ->length ;
148
99
}
149
100
101
+ /**
102
+ * Return whether the current read position is at the end of the stream.
103
+ *
104
+ * @return boolean
105
+ */
150
106
public function isEOF ()
151
107
{
152
- return ($ this ->iteratorEmpty && $ this ->bufferEmpty );
108
+ if ($ this ->chunkOffset === $ this ->numChunks - 1 ) {
109
+ return $ this ->bufferOffset >= $ this ->expectedLastChunkSize ;
110
+ }
111
+
112
+ return $ this ->chunkOffset >= $ this ->numChunks ;
153
113
}
154
114
155
- private function advanceChunks ()
115
+ /**
116
+ * Read bytes from the stream.
117
+ *
118
+ * Note: this method may return a string smaller than the requested length
119
+ * if data is not available to be read.
120
+ *
121
+ * @param integer $length Number of bytes to read
122
+ * @return string
123
+ * @throws InvalidArgumentException if $length is negative
124
+ */
125
+ public function readBytes ($ length )
156
126
{
157
- if ($ this ->chunkOffset >= $ this ->numChunks ) {
158
- $ this ->iteratorEmpty = true ;
127
+ if ($ length < 0 ) {
128
+ throw new InvalidArgumentException (sprintf ('$length must be >= 0; given: %d ' , $ length ));
129
+ }
159
130
160
- return false ;
131
+ if ($ this ->chunksIterator === null ) {
132
+ $ this ->initChunksIterator ();
133
+ }
134
+
135
+ if ($ this ->buffer === null && ! $ this ->initBufferFromCurrentChunk ()) {
136
+ return '' ;
161
137
}
162
138
163
- if ($ this ->firstCheck ) {
164
- $ this ->chunksIterator ->rewind ();
165
- $ this ->firstCheck = false ;
166
- } else {
167
- $ this ->chunksIterator ->next ();
139
+ $ data = '' ;
140
+
141
+ while (strlen ($ data ) < $ length ) {
142
+ if ($ this ->bufferOffset >= strlen ($ this ->buffer ) && ! $ this ->initBufferFromNextChunk ()) {
143
+ break ;
144
+ }
145
+
146
+ $ initialDataLength = strlen ($ data );
147
+ $ data .= substr ($ this ->buffer , $ this ->bufferOffset , $ length - $ initialDataLength );
148
+ $ this ->bufferOffset += strlen ($ data ) - $ initialDataLength ;
149
+ }
150
+
151
+ return $ data ;
152
+ }
153
+
154
+ /**
155
+ * Initialize the buffer to the current chunk's data.
156
+ *
157
+ * @return boolean Whether there was a current chunk to read
158
+ * @throws CorruptFileException if an expected chunk could not be read successfully
159
+ */
160
+ private function initBufferFromCurrentChunk ()
161
+ {
162
+ if ($ this ->chunkOffset === 0 && $ this ->numChunks === 0 ) {
163
+ return false ;
168
164
}
169
165
170
166
if ( ! $ this ->chunksIterator ->valid ()) {
171
167
throw CorruptFileException::missingChunk ($ this ->chunkOffset );
172
168
}
173
169
174
- if ($ this ->chunksIterator ->current ()->n != $ this ->chunkOffset ) {
175
- throw CorruptFileException::unexpectedIndex ($ this ->chunksIterator ->current ()->n , $ this ->chunkOffset );
170
+ $ currentChunk = $ this ->chunksIterator ->current ();
171
+
172
+ if ($ currentChunk ->n !== $ this ->chunkOffset ) {
173
+ throw CorruptFileException::unexpectedIndex ($ currentChunk ->n , $ this ->chunkOffset );
176
174
}
177
175
178
- $ actualChunkSize = strlen ( $ this -> chunksIterator -> current ()-> data ->getData () );
176
+ $ this -> buffer = $ currentChunk -> data ->getData ();
179
177
180
- $ expectedChunkSize = ($ this ->chunkOffset == $ this ->numChunks - 1 )
181
- ? ($ this ->length - $ this ->bytesSeen )
178
+ $ actualChunkSize = strlen ($ this ->buffer );
179
+
180
+ $ expectedChunkSize = ($ this ->chunkOffset === $ this ->numChunks - 1 )
181
+ ? $ this ->expectedLastChunkSize
182
182
: $ this ->chunkSize ;
183
183
184
- if ($ actualChunkSize != $ expectedChunkSize ) {
184
+ if ($ actualChunkSize !== $ expectedChunkSize ) {
185
185
throw CorruptFileException::unexpectedSize ($ actualChunkSize , $ expectedChunkSize );
186
186
}
187
187
188
- $ this ->bytesSeen += $ actualChunkSize ;
189
- $ this ->chunkOffset ++;
190
-
191
188
return true ;
192
189
}
193
190
194
- private function initEmptyBuffer ()
191
+ /**
192
+ * Advance to the next chunk and initialize the buffer to its data.
193
+ *
194
+ * @return boolean Whether there was a next chunk to read
195
+ * @throws CorruptFileException if an expected chunk could not be read successfully
196
+ */
197
+ private function initBufferFromNextChunk ()
195
198
{
196
- if (isset ( $ this ->buffer ) ) {
197
- fclose ( $ this -> buffer ) ;
199
+ if ($ this ->chunkOffset === $ this -> numChunks - 1 ) {
200
+ return false ;
198
201
}
199
202
200
- $ this ->buffer = fopen ("php://memory " , "w+b " );
201
- $ this ->bufferEmpty = true ;
202
- $ this ->bufferFresh = true ;
203
+ $ this ->bufferOffset = 0 ;
204
+ $ this ->chunkOffset ++;
205
+ $ this ->chunksIterator ->next ();
206
+
207
+ return $ this ->initBufferFromCurrentChunk ();
208
+ }
209
+
210
+ /**
211
+ * Initializes the chunk iterator starting from the current offset.
212
+ */
213
+ private function initChunksIterator ()
214
+ {
215
+ $ cursor = $ this ->collectionWrapper ->findChunksByFileId ($ this ->file ->_id , $ this ->chunkOffset );
216
+
217
+ $ this ->chunksIterator = new IteratorIterator ($ cursor );
218
+ $ this ->chunksIterator ->rewind ();
203
219
}
204
220
}
0 commit comments