@@ -211,6 +211,19 @@ File.prototype.copy = function(destination, callback) {
211211 * piped to a writable stream or listened to for 'data' events to read a file's
212212 * contents.
213213 *
214+ * In the unlikely event there is a mismatch between what you downloaded and the
215+ * version in your Bucket, your error handler will receive an error with code
216+ * "CONTENT_DOWNLOAD_MISMATCH". If you receive this error, the best recourse is
217+ * to try downloading the file again.
218+ *
219+ * @param {object= } options - Configuration object.
220+ * @param {string|boolean } options.validation - Possible values: `"md5"`,
221+ * `"crc32c"`, or `false`. By default, data integrity is validated with an
222+ * MD5 checksum for maximum reliability, falling back to CRC32c when an MD5
223+ * hash wasn't returned from the API. CRC32c will provide better performance
224+ * with less reliability. You may also choose to skip validation completely,
225+ * however this is **not recommended**.
226+ *
214227 * @example
215228 * //-
216229 * // <h4>Downloading a File</h4>
@@ -226,35 +239,133 @@ File.prototype.copy = function(destination, callback) {
226239 * .pipe(fs.createWriteStream('/Users/stephen/Photos/image.png'))
227240 * .on('error', function(err) {});
228241 */
229- File . prototype . createReadStream = function ( ) {
230- var storage = this . bucket . storage ;
231- var dup = duplexify ( ) ;
232- function createAuthorizedReq ( uri ) {
233- var reqOpts = { uri : uri } ;
234- storage . makeAuthorizedRequest_ ( reqOpts , {
235- onAuthorized : function ( err , authorizedReqOpts ) {
236- if ( err ) {
237- dup . emit ( 'error' , err ) ;
238- dup . end ( ) ;
239- return ;
240- }
241- dup . setReadable ( request ( authorizedReqOpts ) ) ;
242- }
243- } ) ;
242+ File . prototype . createReadStream = function ( options ) {
243+ options = options || { } ;
244+
245+ var that = this ;
246+ var throughStream = through ( ) ;
247+
248+ var validations = [ 'crc32c' , 'md5' ] ;
249+ var validation ;
250+
251+ if ( util . is ( options . validation , 'string' ) ) {
252+ options . validation = options . validation . toLowerCase ( ) ;
253+
254+ if ( validations . indexOf ( options . validation ) > - 1 ) {
255+ validation = options . validation ;
256+ } else {
257+ validation = 'all' ;
258+ }
259+ }
260+
261+ if ( util . is ( options . validation , 'undefined' ) ) {
262+ validation = 'all' ;
244263 }
264+
265+ var crc32c = validation === 'crc32c' || validation === 'all' ;
266+ var md5 = validation === 'md5' || validation === 'all' ;
267+
245268 if ( this . metadata . mediaLink ) {
246269 createAuthorizedReq ( this . metadata . mediaLink ) ;
247270 } else {
248271 this . getMetadata ( function ( err , metadata ) {
249272 if ( err ) {
250- dup . emit ( 'error' , err ) ;
251- dup . end ( ) ;
273+ throughStream . emit ( 'error' , err ) ;
274+ throughStream . end ( ) ;
252275 return ;
253276 }
277+
254278 createAuthorizedReq ( metadata . mediaLink ) ;
255279 } ) ;
256280 }
257- return dup ;
281+
282+ return throughStream ;
283+
284+ // Authenticate the request, then pipe the remote API request to the stream
285+ // returned to the user.
286+ function createAuthorizedReq ( uri ) {
287+ var reqOpts = {
288+ uri : uri
289+ } ;
290+
291+ that . bucket . storage . makeAuthorizedRequest_ ( reqOpts , {
292+ onAuthorized : function ( err , authorizedReqOpts ) {
293+ if ( err ) {
294+ throughStream . emit ( 'error' , err ) ;
295+ throughStream . end ( ) ;
296+ return ;
297+ }
298+
299+ // For data integrity, hash the contents of the stream as we receive it
300+ // from the server.
301+ var localCrc32cHash ;
302+ var localMd5Hash = crypto . createHash ( 'md5' ) ;
303+
304+ request ( authorizedReqOpts )
305+ . on ( 'error' , function ( err ) {
306+ throughStream . emit ( 'error' , err ) ;
307+ throughStream . end ( ) ;
308+ } )
309+
310+ . on ( 'data' , function ( chunk ) {
311+ if ( crc32c ) {
312+ localCrc32cHash = crc . calculate ( chunk , localCrc32cHash ) ;
313+ }
314+
315+ if ( md5 ) {
316+ localMd5Hash . update ( chunk ) ;
317+ }
318+ } )
319+
320+ . on ( 'complete' , function ( res ) {
321+ var failed = false ;
322+ var crcFail = true ;
323+ var md5Fail = true ;
324+
325+ var hashes = { } ;
326+ res . headers [ 'x-goog-hash' ] . split ( ',' ) . forEach ( function ( hash ) {
327+ var hashType = hash . split ( '=' ) [ 0 ] ;
328+ hashes [ hashType ] = hash . substr ( hash . indexOf ( '=' ) + 1 ) ;
329+ } ) ;
330+
331+ var remoteMd5 = hashes . md5 ;
332+ var remoteCrc = hashes . crc32c && hashes . crc32c . substr ( 4 ) ;
333+
334+ if ( crc32c ) {
335+ crcFail =
336+ new Buffer ( [ localCrc32cHash ] ) . toString ( 'base64' ) !== remoteCrc ;
337+ failed = crcFail ;
338+ }
339+
340+ if ( md5 ) {
341+ md5Fail = localMd5Hash . digest ( 'base64' ) !== remoteMd5 ;
342+ failed = md5Fail ;
343+ }
344+
345+ if ( validation === 'all' ) {
346+ failed = remoteMd5 ? md5Fail : crcFail ;
347+ }
348+
349+ if ( failed ) {
350+ var error = new Error ( [
351+ 'The downloaded data did not match the data from the server.' ,
352+ 'To be sure the content is the same, you should download the' ,
353+ 'file again.'
354+ ] . join ( ' ' ) ) ;
355+ error . code = 'CONTENT_DOWNLOAD_MISMATCH' ;
356+
357+ throughStream . emit ( 'error' , error ) ;
358+ } else {
359+ throughStream . emit ( 'complete' ) ;
360+ }
361+
362+ throughStream . end ( ) ;
363+ } )
364+
365+ . pipe ( throughStream ) ;
366+ }
367+ } ) ;
368+ }
258369} ;
259370
260371/**
@@ -688,7 +799,7 @@ File.prototype.startResumableUpload_ = function(stream, metadata) {
688799 method : 'PUT' ,
689800 uri : resumableUri
690801 } , {
691- onAuthorized : function ( err , reqOpts ) {
802+ onAuthorized : function ( err , reqOpts ) {
692803 if ( err ) {
693804 handleError ( err ) ;
694805 return ;
0 commit comments