Skip to content

Commit b52b28b

Browse files
committed
Merge pull request #381 from stephenplusplus/spp--storage-download
storage: add download() method
2 parents 1b1f6b3 + 132150c commit b52b28b

File tree

4 files changed

+207
-0
lines changed

4 files changed

+207
-0
lines changed

lib/storage/file.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ var ConfigStore = require('configstore');
2525
var crc = require('fast-crc32c');
2626
var crypto = require('crypto');
2727
var duplexify = require('duplexify');
28+
var fs = require('fs');
29+
var once = require('once');
2830
var request = require('request');
2931
var streamEvents = require('stream-events');
3032
var through = require('through2');
@@ -619,6 +621,62 @@ File.prototype.delete = function(callback) {
619621
}.bind(this));
620622
};
621623

624+
/**
625+
* Convenience method to download a file into memory or to a local destination.
626+
*
627+
* @param {object=} options - Optional configuration. The arguments match those
628+
* passed to {module:storage/file#createReadStream}.
629+
* @param {string} options.destination - Local file path to write the file's
630+
* contents to.
631+
* @param {function} callback - The callback function.
632+
*
633+
* @example
634+
* //-
635+
* // Download a file into memory. The contents will be available as the second
636+
* // argument in the demonstration below, `contents`.
637+
* //-
638+
* file.download(function(err, contents) {});
639+
*
640+
* //-
641+
* // Download a file to a local destination.
642+
* //-
643+
* file.download({
644+
* destination: '/Users/stephen/Desktop/file-backup.txt'
645+
* }, function(err) {});
646+
*/
647+
File.prototype.download = function(options, callback) {
648+
if (util.is(options, 'function')) {
649+
callback = options;
650+
options = {};
651+
}
652+
653+
callback = once(callback);
654+
655+
var destination = options.destination;
656+
delete options.destination;
657+
658+
var fileStream = this.createReadStream(options);
659+
660+
if (destination) {
661+
fileStream
662+
.on('error', callback)
663+
.pipe(fs.createWriteStream(destination))
664+
.on('error', callback)
665+
.on('finish', callback);
666+
} else {
667+
var fileContents = new Buffer('');
668+
669+
fileStream
670+
.on('error', callback)
671+
.on('data', function(chunk) {
672+
fileContents = Buffer.concat([fileContents, chunk]);
673+
})
674+
.on('complete', function() {
675+
callback(null, fileContents);
676+
});
677+
}
678+
};
679+
622680
/**
623681
* Get the file's metadata.
624682
*

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"google-service-account": "^1.0.3",
5757
"mime-types": "^2.0.8",
5858
"node-uuid": "^1.4.2",
59+
"once": "^1.3.1",
5960
"protobufjs": "^3.8.2",
6061
"request": "^2.53.0",
6162
"stream-events": "^1.0.1",

regression/storage.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,20 @@ describe('storage', function() {
362362
});
363363
});
364364

365+
it('should download a file to memory', function(done) {
366+
var fileContents = fs.readFileSync(files.big.path);
367+
368+
bucket.upload(files.big.path, function(err, file) {
369+
assert.ifError(err);
370+
371+
file.download(function(err, remoteContents) {
372+
assert.ifError(err);
373+
assert.equal(fileContents, remoteContents);
374+
done();
375+
});
376+
});
377+
});
378+
365379
describe('stream write', function() {
366380
it('should stream write, then remove file (3mb)', function(done) {
367381
var file = bucket.file('LargeFile');

test/storage/file.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ var crc = require('fast-crc32c');
2424
var crypto = require('crypto');
2525
var duplexify = require('duplexify');
2626
var extend = require('extend');
27+
var fs = require('fs');
2728
var mockery = require('mockery');
2829
var nodeutil = require('util');
2930
var request = require('request');
3031
var stream = require('stream');
3132
var through = require('through2');
33+
var tmp = require('tmp');
3234
var url = require('url');
3335
var util = require('../../lib/common/util');
3436

@@ -741,6 +743,138 @@ describe('File', function() {
741743
});
742744
});
743745

746+
describe('download', function() {
747+
var fileReadStream;
748+
749+
beforeEach(function() {
750+
fileReadStream = new stream.Readable();
751+
fileReadStream._read = util.noop;
752+
753+
fileReadStream.on('end', function() {
754+
fileReadStream.emit('complete');
755+
});
756+
757+
file.createReadStream = function() {
758+
return fileReadStream;
759+
};
760+
});
761+
762+
it('should accept just a callback', function(done) {
763+
fileReadStream._read = function() {
764+
done();
765+
};
766+
767+
file.download(assert.ifError);
768+
});
769+
770+
it('should accept an options object and callback', function(done) {
771+
fileReadStream._read = function() {
772+
done();
773+
};
774+
775+
file.download({}, assert.ifError);
776+
});
777+
778+
it('should pass the provided options to createReadStream', function(done) {
779+
var readOptions = { start: 100, end: 200 };
780+
781+
file.createReadStream = function(options) {
782+
assert.deepEqual(options, readOptions);
783+
done();
784+
return fileReadStream;
785+
};
786+
787+
file.download(readOptions, assert.ifError);
788+
});
789+
790+
it('should only execute callback once', function(done) {
791+
fileReadStream._read = function() {
792+
this.emit('error', new Error('Error.'));
793+
this.emit('error', new Error('Error.'));
794+
};
795+
796+
file.download(function() {
797+
done();
798+
});
799+
});
800+
801+
describe('into memory', function() {
802+
it('should buffer a file into memory if no destination', function(done) {
803+
var fileContents = 'abcdefghijklmnopqrstuvwxyz';
804+
805+
fileReadStream._read = function() {
806+
this.push(fileContents);
807+
this.push(null);
808+
};
809+
810+
file.download(function(err, remoteFileContents) {
811+
assert.ifError(err);
812+
813+
assert.equal(fileContents, remoteFileContents);
814+
done();
815+
});
816+
});
817+
818+
it('should execute callback with error', function(done) {
819+
var error = new Error('Error.');
820+
821+
fileReadStream._read = function() {
822+
this.emit('error', error);
823+
};
824+
825+
file.download(function(err) {
826+
assert.equal(err, error);
827+
done();
828+
});
829+
});
830+
});
831+
832+
describe('with destination', function() {
833+
it('should write the file to a destination if provided', function(done) {
834+
tmp.setGracefulCleanup();
835+
tmp.file(function _tempFileCreated(err, tmpFilePath) {
836+
assert.ifError(err);
837+
838+
var fileContents = 'abcdefghijklmnopqrstuvwxyz';
839+
840+
fileReadStream._read = function() {
841+
this.push(fileContents);
842+
this.push(null);
843+
};
844+
845+
file.download({ destination: tmpFilePath }, function(err) {
846+
assert.ifError(err);
847+
848+
fs.readFile(tmpFilePath, function(err, tmpFileContents) {
849+
assert.ifError(err);
850+
851+
assert.equal(fileContents, tmpFileContents);
852+
done();
853+
});
854+
});
855+
});
856+
});
857+
858+
it('should execute callback with error', function(done) {
859+
tmp.setGracefulCleanup();
860+
tmp.file(function _tempFileCreated(err, tmpFilePath) {
861+
assert.ifError(err);
862+
863+
var error = new Error('Error.');
864+
865+
fileReadStream._read = function() {
866+
this.emit('error', error);
867+
};
868+
869+
file.download({ destination: tmpFilePath }, function(err) {
870+
assert.equal(err, error);
871+
done();
872+
});
873+
});
874+
});
875+
});
876+
});
877+
744878
describe('getMetadata', function() {
745879
var metadata = { a: 'b', c: 'd' };
746880

0 commit comments

Comments
 (0)