From 132150c00871a3388178cdc57e05da62c086682e Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Mon, 9 Feb 2015 14:34:37 -0500 Subject: [PATCH] storage: add download() method --- lib/storage/file.js | 58 ++++++++++++++++++ package.json | 1 + regression/storage.js | 14 +++++ test/storage/file.js | 134 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+) diff --git a/lib/storage/file.js b/lib/storage/file.js index 72f594b3aac..5f0dc4000b3 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -25,6 +25,8 @@ var ConfigStore = require('configstore'); var crc = require('fast-crc32c'); var crypto = require('crypto'); var duplexify = require('duplexify'); +var fs = require('fs'); +var once = require('once'); var request = require('request'); var streamEvents = require('stream-events'); var through = require('through2'); @@ -619,6 +621,62 @@ File.prototype.delete = function(callback) { }.bind(this)); }; +/** + * Convenience method to download a file into memory or to a local destination. + * + * @param {object=} options - Optional configuration. The arguments match those + * passed to {module:storage/file#createReadStream}. + * @param {string} options.destination - Local file path to write the file's + * contents to. + * @param {function} callback - The callback function. + * + * @example + * //- + * // Download a file into memory. The contents will be available as the second + * // argument in the demonstration below, `contents`. + * //- + * file.download(function(err, contents) {}); + * + * //- + * // Download a file to a local destination. + * //- + * file.download({ + * destination: '/Users/stephen/Desktop/file-backup.txt' + * }, function(err) {}); + */ +File.prototype.download = function(options, callback) { + if (util.is(options, 'function')) { + callback = options; + options = {}; + } + + callback = once(callback); + + var destination = options.destination; + delete options.destination; + + var fileStream = this.createReadStream(options); + + if (destination) { + fileStream + .on('error', callback) + .pipe(fs.createWriteStream(destination)) + .on('error', callback) + .on('finish', callback); + } else { + var fileContents = new Buffer(''); + + fileStream + .on('error', callback) + .on('data', function(chunk) { + fileContents = Buffer.concat([fileContents, chunk]); + }) + .on('complete', function() { + callback(null, fileContents); + }); + } +}; + /** * Get the file's metadata. * diff --git a/package.json b/package.json index 952b46d63ba..27e78ae33c4 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "google-service-account": "^1.0.3", "mime-types": "^2.0.8", "node-uuid": "^1.4.2", + "once": "^1.3.1", "protobufjs": "^3.8.2", "request": "^2.53.0", "stream-events": "^1.0.1", diff --git a/regression/storage.js b/regression/storage.js index 3503a3acfc0..dc8f7efc4d7 100644 --- a/regression/storage.js +++ b/regression/storage.js @@ -362,6 +362,20 @@ describe('storage', function() { }); }); + it('should download a file to memory', function(done) { + var fileContents = fs.readFileSync(files.big.path); + + bucket.upload(files.big.path, function(err, file) { + assert.ifError(err); + + file.download(function(err, remoteContents) { + assert.ifError(err); + assert.equal(fileContents, remoteContents); + done(); + }); + }); + }); + describe('stream write', function() { it('should stream write, then remove file (3mb)', function(done) { var file = bucket.file('LargeFile'); diff --git a/test/storage/file.js b/test/storage/file.js index 9058f8a4631..ba29f478807 100644 --- a/test/storage/file.js +++ b/test/storage/file.js @@ -24,11 +24,13 @@ var crc = require('fast-crc32c'); var crypto = require('crypto'); var duplexify = require('duplexify'); var extend = require('extend'); +var fs = require('fs'); var mockery = require('mockery'); var nodeutil = require('util'); var request = require('request'); var stream = require('stream'); var through = require('through2'); +var tmp = require('tmp'); var url = require('url'); var util = require('../../lib/common/util'); @@ -741,6 +743,138 @@ describe('File', function() { }); }); + describe('download', function() { + var fileReadStream; + + beforeEach(function() { + fileReadStream = new stream.Readable(); + fileReadStream._read = util.noop; + + fileReadStream.on('end', function() { + fileReadStream.emit('complete'); + }); + + file.createReadStream = function() { + return fileReadStream; + }; + }); + + it('should accept just a callback', function(done) { + fileReadStream._read = function() { + done(); + }; + + file.download(assert.ifError); + }); + + it('should accept an options object and callback', function(done) { + fileReadStream._read = function() { + done(); + }; + + file.download({}, assert.ifError); + }); + + it('should pass the provided options to createReadStream', function(done) { + var readOptions = { start: 100, end: 200 }; + + file.createReadStream = function(options) { + assert.deepEqual(options, readOptions); + done(); + return fileReadStream; + }; + + file.download(readOptions, assert.ifError); + }); + + it('should only execute callback once', function(done) { + fileReadStream._read = function() { + this.emit('error', new Error('Error.')); + this.emit('error', new Error('Error.')); + }; + + file.download(function() { + done(); + }); + }); + + describe('into memory', function() { + it('should buffer a file into memory if no destination', function(done) { + var fileContents = 'abcdefghijklmnopqrstuvwxyz'; + + fileReadStream._read = function() { + this.push(fileContents); + this.push(null); + }; + + file.download(function(err, remoteFileContents) { + assert.ifError(err); + + assert.equal(fileContents, remoteFileContents); + done(); + }); + }); + + it('should execute callback with error', function(done) { + var error = new Error('Error.'); + + fileReadStream._read = function() { + this.emit('error', error); + }; + + file.download(function(err) { + assert.equal(err, error); + done(); + }); + }); + }); + + describe('with destination', function() { + it('should write the file to a destination if provided', function(done) { + tmp.setGracefulCleanup(); + tmp.file(function _tempFileCreated(err, tmpFilePath) { + assert.ifError(err); + + var fileContents = 'abcdefghijklmnopqrstuvwxyz'; + + fileReadStream._read = function() { + this.push(fileContents); + this.push(null); + }; + + file.download({ destination: tmpFilePath }, function(err) { + assert.ifError(err); + + fs.readFile(tmpFilePath, function(err, tmpFileContents) { + assert.ifError(err); + + assert.equal(fileContents, tmpFileContents); + done(); + }); + }); + }); + }); + + it('should execute callback with error', function(done) { + tmp.setGracefulCleanup(); + tmp.file(function _tempFileCreated(err, tmpFilePath) { + assert.ifError(err); + + var error = new Error('Error.'); + + fileReadStream._read = function() { + this.emit('error', error); + }; + + file.download({ destination: tmpFilePath }, function(err) { + assert.equal(err, error); + done(); + }); + }); + }); + }); + }); + describe('getMetadata', function() { var metadata = { a: 'b', c: 'd' };