diff --git a/packages/storage/README.md b/packages/storage/README.md index 3b294672eca..08fb071cfbb 100644 --- a/packages/storage/README.md +++ b/packages/storage/README.md @@ -48,6 +48,16 @@ remoteReadStream.pipe(localWriteStream); var localReadStream = fs.createReadStream('/photos/zoo/zebra.jpg'); var remoteWriteStream = bucket.file('zebra.jpg').createWriteStream(); localReadStream.pipe(remoteWriteStream); + +// Promises are also supported by omitting callbacks. +bucket.upload('/photos/zoo/zebra.jpg').then(function(data) { + var file = data[0]; +}); + +// It's also possible to integrate with third-party Promise libraries. +var gcs = require('@google-cloud/storage')({ + promise: require('bluebird') +}); ``` diff --git a/packages/storage/package.json b/packages/storage/package.json index d16bc91355d..e3a0f8d0444 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -50,7 +50,7 @@ "storage" ], "dependencies": { - "@google-cloud/common": "^0.6.0", + "@google-cloud/common": "^0.7.0", "arrify": "^1.0.0", "async": "^2.0.1", "concat-stream": "^1.5.0", diff --git a/packages/storage/src/acl.js b/packages/storage/src/acl.js index 109bad85384..c48e7f0ca8a 100644 --- a/packages/storage/src/acl.js +++ b/packages/storage/src/acl.js @@ -21,6 +21,7 @@ 'use strict'; var arrify = require('arrify'); +var common = require('@google-cloud/common'); var is = require('is'); var util = require('util'); @@ -107,6 +108,14 @@ function Acl(options) { * entity: 'user-email@example.com', * role: gcs.acl.OWNER_ROLE * }, function(err, aclObject) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myFile.acl.owners.addUser('email@example.com').then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ Acl.prototype.owners = {}; @@ -144,6 +153,14 @@ Acl.prototype.owners = {}; * entity: 'user-email@example.com', * role: gcs.acl.READER_ROLE * }, function(err, aclObject) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myFile.acl.readers.addUser('email@example.com').then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ Acl.prototype.readers = {}; @@ -181,6 +198,14 @@ Acl.prototype.readers = {}; * entity: 'user-email@example.com', * role: gcs.acl.WRITER_ROLE * }, function(err, aclObject) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myFile.acl.writers.addUser('email@example.com').then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ Acl.prototype.writers = {}; @@ -204,10 +229,12 @@ util.inherits(Acl, AclRoleAccessorMethods); * @param {object} callback.apiResponse - The full API response. * * @example - * myBucket.acl.add({ + * var options = { * entity: 'user-useremail@example.com', * role: gcs.acl.OWNER_ROLE - * }, function(err, aclObject, apiResponse) {}); + * }; + * + * myBucket.acl.add(options, function(err, aclObject, apiResponse) {}); * * //- * // For file ACL operations, you can also specify a `generation` property. @@ -219,6 +246,14 @@ util.inherits(Acl, AclRoleAccessorMethods); * role: gcs.acl.OWNER_ROLE, * generation: 1 * }, function(err, aclObject, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myBucket.acl.add(options).then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ Acl.prototype.add = function(options, callback) { var self = this; @@ -273,6 +308,13 @@ Acl.prototype.add = function(options, callback) { * entity: 'user-useremail@example.com', * generation: 1 * }, function(err, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myFile.acl.delete().then(function(data) { + * var apiResponse = data[0]; + * }); */ Acl.prototype.delete = function(options, callback) { var query = {}; @@ -333,6 +375,14 @@ Acl.prototype.delete = function(options, callback) { * entity: 'user-useremail@example.com', * generation: 1 * }, function(err, aclObject, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myBucket.acl.get().then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ Acl.prototype.get = function(options, callback) { var self = this; @@ -389,10 +439,12 @@ Acl.prototype.get = function(options, callback) { * @param {object} callback.apiResponse - The full API response. * * @example - * myBucket.acl.update({ + * var options = { * entity: 'user-useremail@example.com', * role: gcs.acl.WRITER_ROLE - * }, function(err, aclObject, apiResponse) {}); + * }; + * + * myBucket.acl.update(options, function(err, aclObject, apiResponse) {}); * * //- * // For file ACL operations, you can also specify a `generation` property. @@ -402,6 +454,14 @@ Acl.prototype.get = function(options, callback) { * role: gcs.acl.WRITER_ROLE, * generation: 1 * }, function(err, aclObject, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myFile.acl.update(options).then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ Acl.prototype.update = function(options, callback) { var self = this; @@ -463,6 +523,13 @@ Acl.prototype.request = function(reqOpts, callback) { this.request_(reqOpts, callback); }; +/*! Developer Documentation + * + * All async methods (except for streams) will return a Promise in the event + * that a callback is omitted. + */ +common.util.promisifyAll(Acl); + module.exports = Acl; /** @@ -539,7 +606,7 @@ AclRoleAccessorMethods.prototype._assignAccessMethods = function(role) { callback = entityId; } - self[accessMethod]({ + return self[accessMethod]({ entity: apiEntity, role: role }, callback); diff --git a/packages/storage/src/bucket.js b/packages/storage/src/bucket.js index 353cc79a1ff..7584bed2c9e 100644 --- a/packages/storage/src/bucket.js +++ b/packages/storage/src/bucket.js @@ -81,6 +81,14 @@ function Bucket(storage, name) { * // The zone was created successfully. * } * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.create().then(function(data) { + * var zone = data[0]; + * var apiResponse = data[1]; + * }); */ create: true, @@ -96,6 +104,13 @@ function Bucket(storage, name) { * * @example * bucket.delete(function(err, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.delete().then(function(data) { + * var apiResponse = data[0]; + * }); */ delete: true, @@ -109,6 +124,13 @@ function Bucket(storage, name) { * * @example * bucket.exists(function(err, exists) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.exists().then(function(data) { + * var exists = data[0]; + * }); */ exists: true, @@ -128,6 +150,14 @@ function Bucket(storage, name) { * bucket.get(function(err, bucket, apiResponse) { * // `bucket.metadata` has been populated. * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.get().then(function(data) { + * var bucket = data[0]; + * var apiResponse = data[1]; + * }); */ get: true, @@ -146,6 +176,14 @@ function Bucket(storage, name) { * * @example * bucket.getMetadata(function(err, metadata, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.getMetadata().then(function(data) { + * var metadata = data[0]; + * var apiResponse = data[1]; + * }); */ getMetadata: true, @@ -164,12 +202,14 @@ function Bucket(storage, name) { * //- * // Set website metadata field on the bucket. * //- - * bucket.setMetadata({ + * var metadata = { * website: { * mainPageSuffix: 'http://example.com', * notFoundPage: 'http://example.com/404.html' * } - * }, function(err, apiResponse) {}); + * }; + * + * bucket.setMetadata(metadata, function(err, apiResponse) {}); * * //- * // Enable versioning for your bucket. @@ -179,6 +219,13 @@ function Bucket(storage, name) { * enabled: true * } * }, function(err, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.setMetadata(metadata).then(function(data) { + * var apiResponse = data[0]; + * }); */ setMetadata: true }; @@ -225,10 +272,21 @@ function Bucket(storage, name) { * // Make a bucket's contents publicly readable. * //- * var myBucket = gcs.bucket('my-bucket'); - * myBucket.acl.add({ + * + * var options = { * entity: 'allUsers', * role: gcs.acl.READER_ROLE - * }, function(err, aclObject) {}); + * }; + * + * myBucket.acl.add(options, function(err, aclObject) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * myBucket.acl.add(options).then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ this.acl = new Acl({ request: this.request.bind(this), @@ -274,17 +332,24 @@ util.inherits(Bucket, common.ServiceObject); * @example * var logBucket = gcs.bucket('log-bucket'); * - * var logs2013 = logBucket.file('2013-logs.txt'); - * var logs2014 = logBucket.file('2014-logs.txt'); + * var sources = [ + * logBucket.file('2013-logs.txt'), + * logBucket.file('2014-logs.txt') + * ]; * * var allLogs = logBucket.file('all-logs.txt'); * - * logBucket.combine([ - * logs2013, - * logs2014 - * ], allLogs, function(err, newFile, apiResponse) { + * logBucket.combine(sources, allLogs, function(err, newFile, apiResponse) { * // newFile === allLogs * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * logBucket.combine(sources, allLogs).then(function(data) { + * var newFile = data[0]; + * var apiResponse = data[1]; + * }); */ Bucket.prototype.combine = function(sources, destination, callback) { if (!is.array(sources) || sources.length < 2) { @@ -381,6 +446,14 @@ Bucket.prototype.combine = function(sources, destination, callback) { * // Channel created successfully. * } * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.createChannel(id, config).then(function(data) { + * var channel = data[0]; + * var apiResponse = data[1]; + * }); */ Bucket.prototype.createChannel = function(id, config, callback) { var self = this; @@ -469,6 +542,11 @@ Bucket.prototype.createChannel = function(id, config, callback) { * // All files in the `images` directory have been deleted. * } * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.deleteFiles().then(function() {}); */ Bucket.prototype.deleteFiles = function(query, callback) { if (is.fn(query)) { @@ -607,25 +685,11 @@ Bucket.prototype.file = function(name, options) { * }, callback); * * //- - * // Get the files from your bucket as a readable object stream. - * //- - * bucket.getFiles() - * .on('error', console.error) - * .on('data', function(file) { - * // file is a File object. - * }) - * .on('end', function() { - * // All files retrieved. - * }); - * - * //- - * // If you anticipate many results, you can end a stream early to prevent - * // unnecessary processing and API requests. + * // If the callback is omitted, we'll return a Promise. * //- - * bucket.getFiles() - * .on('data', function(file) { - * this.end(); - * }); + * bucket.getFiles().then(function(data) { + * var files = data[0]; + * }); */ Bucket.prototype.getFiles = function(query, callback) { var self = this; @@ -668,6 +732,35 @@ Bucket.prototype.getFiles = function(query, callback) { }); }; +/** + * Get {module:storage/file} objects for the files currently in the bucket as a + * readable object stream. + * + * @param {object=} query - Configuration object. See + * {module:storage/bucket#getFiles} for a complete list of options. + * @return {stream} + * + * @example + * bucket.getFilesStream() + * .on('error', console.error) + * .on('data', function(file) { + * // file is a File object. + * }) + * .on('end', function() { + * // All files retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * bucket.getFilesStream() + * .on('data', function(file) { + * this.end(); + * }); + */ +Bucket.prototype.getFilesStream = common.paginator.streamify('getFiles'); + /** * Make the bucket listing private. * @@ -732,6 +825,13 @@ Bucket.prototype.getFiles = function(query, callback) { * // `files`: * // Array of files successfully made private in the bucket. * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.makePrivate(opts).then(function(data) { + * var files = data[0]; + * }); */ Bucket.prototype.makePrivate = function(options, callback) { var self = this; @@ -848,6 +948,13 @@ Bucket.prototype.makePrivate = function(options, callback) { * // `files`: * // Array of files successfully made public in the bucket. * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.makePublic(opts).then(function(data) { + * var files = data[0]; + * }); */ Bucket.prototype.makePublic = function(options, callback) { var self = this; @@ -1049,6 +1156,13 @@ Bucket.prototype.makePublic = function(options, callback) { * var file = bucket.file('img.png'); * file.setEncryptionKey(encryptionKey); * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * bucket.upload('local-image.png').then(function(data) { + * var file = data[0]; + * }); */ Bucket.prototype.upload = function(localPath, options, callback) { if (is.fn(options)) { @@ -1175,9 +1289,17 @@ Bucket.prototype.makeAllFilesPublicPrivate_ = function(options, callback) { /*! Developer Documentation * - * This method can be used with either a callback or as a readable object - * stream. `streamRouter` is used to add this dual behavior. + * These methods can be auto-paginated. + */ +common.paginator.extend(Bucket, 'getFiles'); + +/*! Developer Documentation + * + * All async methods (except for streams) will return a Promise in the event + * that a callback is omitted. */ -common.streamRouter.extend(Bucket, 'getFiles'); +common.util.promisifyAll(Bucket, { + exclude: ['file'] +}); module.exports = Bucket; diff --git a/packages/storage/src/channel.js b/packages/storage/src/channel.js index b31600e95da..393adc34c28 100644 --- a/packages/storage/src/channel.js +++ b/packages/storage/src/channel.js @@ -76,6 +76,13 @@ util.inherits(Channel, common.ServiceObject); * // Channel stopped successfully. * } * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * channel.stop().then(function(data) { + * var apiResponse = data[0]; + * }); */ Channel.prototype.stop = function(callback) { callback = callback || common.util.noop; @@ -89,4 +96,11 @@ Channel.prototype.stop = function(callback) { }); }; +/*! Developer Documentation + * + * All async methods (except for streams) will return a Promise in the event + * that a callback is omitted. + */ +common.util.promisifyAll(Channel); + module.exports = Channel; diff --git a/packages/storage/src/file.js b/packages/storage/src/file.js index 2fabfdcfe3f..51265e16326 100644 --- a/packages/storage/src/file.js +++ b/packages/storage/src/file.js @@ -127,6 +127,13 @@ function File(bucket, name, options) { * * @example * file.delete(function(err, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.delete().then(function(data) { + * var apiResponse = data[0]; + * }); */ delete: { reqOpts: { @@ -144,6 +151,13 @@ function File(bucket, name, options) { * * @example * file.exists(function(err, exists) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.exists().then(function(data) { + * var exists = data[0]; + * }); */ exists: true, @@ -154,6 +168,14 @@ function File(bucket, name, options) { * file.get(function(err, file, apiResponse) { * // file.metadata` has been populated. * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.get().then(function(data) { + * var file = data[0]; + * var apiResponse = data[1]; + * }); */ get: true, @@ -170,6 +192,14 @@ function File(bucket, name, options) { * * @example * file.getMetadata(function(err, metadata, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.getMetadata().then(function(data) { + * var metadata = data[0]; + * var apiResponse = data[1]; + * }); */ getMetadata: { reqOpts: { @@ -197,13 +227,15 @@ function File(bucket, name, options) { * @param {object} callback.apiResponse - The full API response. * * @example - * file.setMetadata({ + * var metadata = { * contentType: 'application/x-font-ttf', * metadata: { * my: 'custom', * properties: 'go here' * } - * }, function(err, apiResponse) {}); + * }; + * + * file.setMetadata(metadata, function(err, apiResponse) {}); * * // Assuming current metadata = { hello: 'world', unsetMe: 'will do' } * file.setMetadata({ @@ -215,6 +247,13 @@ function File(bucket, name, options) { * }, function(err, apiResponse) { * // metadata should now be { abc: '123', hello: 'goodbye' } * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.setMetadata(metadata).then(function(data) { + * var apiResponse = data[0]; + * }); */ setMetadata: { reqOpts: { @@ -256,10 +295,20 @@ function File(bucket, name, options) { * //- * // Make a file publicly readable. * //- - * file.acl.add({ + * var options = { * entity: 'allUsers', * role: gcs.acl.READER_ROLE - * }, function(err, aclObject) {}); + * }; + * + * file.acl.add(options, function(err, aclObject) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.acl.add(options).then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ this.acl = new Acl({ request: this.request.bind(this), @@ -361,6 +410,14 @@ util.inherits(File, common.ServiceObject); * // Note: * // The `copiedFile` parameter is equal to `anotherFile`. * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.copy(newLocation).then(function(data) { + * var newFile = data[0]; + * var apiResponse = data[1]; + * }); */ File.prototype.copy = function(destination, options, callback) { var self = this; @@ -569,7 +626,7 @@ File.prototype.createReadStream = function(options) { }; } - var requestStream = self.request(reqOpts); + var requestStream = self.requestStream(reqOpts); var validateStream; // We listen to the response event from the request stream so that we can... @@ -722,6 +779,13 @@ File.prototype.createReadStream = function(options) { * // `uri` can be used to PUT data to. * } * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.createResumableUpload().then(function(data) { + * var uri = data[0]; + * }); */ File.prototype.createResumableUpload = function(options, callback) { if (is.fn(options)) { @@ -749,12 +813,7 @@ File.prototype.createResumableUpload = function(options, callback) { * A File object can also be used to create files for the first time. * * Resumable uploads are automatically enabled and must be shut off explicitly - * by setting `options.resumable` to `false`. We use - * [`gcs-resumable-upload`](https://github.com/stephenplusplus/gcs-resumable-upload), - * which persists a config file with metadata about the upload request (see - * ["How it works"](https://github.com/stephenplusplus/gcs-resumable-upload#how-it-works)). - * If you start running into unexplainable errors, try clearing this cache file - * located at `~/.config/configstore/gcs-resumable-upload.json`. + * by setting `options.resumable` to `false`. * *

* There is some overhead when using a resumable upload that can cause @@ -1014,6 +1073,13 @@ File.prototype.createWriteStream = function(options) { * file.download({ * destination: '/Users/me/Desktop/file-backup.txt' * }, function(err) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.download().then(function(data) { + * var contents = data[0]; + * }); */ File.prototype.download = function(options, callback) { if (is.fn(options)) { @@ -1148,6 +1214,13 @@ File.prototype.setEncryptionKey = function(encryptionKey) { * // policy.base64: the policy document in base64. * // policy.signature: the policy signature in base64. * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.getSignedPolicy(options).then(function(data) { + * var policy = data[0]; + * }); */ File.prototype.getSignedPolicy = function(options, callback) { var expires = new Date(options.expires); @@ -1292,10 +1365,12 @@ File.prototype.getSignedPolicy = function(options, callback) { * //- * var request = require('request'); * - * file.getSignedUrl({ + * var config = { * action: 'read', * expires: '03-17-2025' - * }, function(err, url) { + * }; + * + * file.getSignedUrl(config, function(err, url) { * if (err) { * console.error(err); * return; @@ -1332,6 +1407,13 @@ File.prototype.getSignedPolicy = function(options, callback) { * }); * }); * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.getSignedUrl(config).then(function(data) { + * var url = data[0]; + * }); */ File.prototype.getSignedUrl = function(config, callback) { var self = this; @@ -1447,6 +1529,13 @@ File.prototype.getSignedUrl = function(config, callback) { * // Set the file private so only the owner can see and modify it. * //- * file.makePrivate({ strict: true }, function(err) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.makePrivate().then(function(data) { + * var apiResponse = data[0]; + * }); */ File.prototype.makePrivate = function(options, callback) { var self = this; @@ -1496,6 +1585,13 @@ File.prototype.makePrivate = function(options, callback) { * * @example * file.makePublic(function(err, apiResponse) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.makePublic().then(function(data) { + * var apiResponse = data[0]; + * }); */ File.prototype.makePublic = function(callback) { callback = callback || common.util.noop; @@ -1605,6 +1701,14 @@ File.prototype.makePublic = function(callback) { * // Note: * // The `destinationFile` parameter is equal to `anotherFile`. * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.move('my-image-new.png').then(function(data) { + * var destinationFile = data[0]; + * var apiResponse = data[1]; + * }); */ File.prototype.move = function(destination, callback) { var self = this; @@ -1636,11 +1740,18 @@ File.prototype.move = function(destination, callback) { * @param {?error} callback.err - An error returned while making this request * * @example - * file.save('This is the contents of the file.', function(err) { + * var contents = 'This is the contents of the file.'; + * + * file.save(contents, function(err) { * if (!err) { * // File written successfully. * } * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.save(contents).then(function() {}); */ File.prototype.save = function(data, options, callback) { if (is.fn(options)) { @@ -1756,4 +1867,13 @@ File.prototype.startSimpleUpload_ = function(dup, options) { }); }; +/*! Developer Documentation + * + * All async methods (except for streams) will return a Promise in the event + * that a callback is omitted. + */ +common.util.promisifyAll(File, { + exclude: ['setEncryptionKey'] +}); + module.exports = File; diff --git a/packages/storage/src/index.js b/packages/storage/src/index.js index 7531f884ed4..d59e22a3f30 100644 --- a/packages/storage/src/index.js +++ b/packages/storage/src/index.js @@ -111,18 +111,17 @@ util.inherits(Storage, common.Service); * //- * // Make all of the files currently in a bucket publicly readable. * //- - * albums.acl.add({ + * var options = { * entity: 'allUsers', * role: gcs.acl.READER_ROLE - * }, function(err, aclObject) {}); + * }; + * + * albums.acl.add(options, function(err, aclObject) {}); * * //- * // Make any new objects added to a bucket publicly readable. * //- - * albums.acl.default.add({ - * entity: 'allUsers', - * role: gcs.acl.READER_ROLE - * }, function(err, aclObject) {}); + * albums.acl.default.add(options, function(err, aclObject) {}); * * //- * // Grant a user ownership permissions to a bucket. @@ -131,6 +130,14 @@ util.inherits(Storage, common.Service); * entity: 'user-useremail@example.com', * role: gcs.acl.OWNER_ROLE * }, function(err, aclObject) {}); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * albums.acl.add(options).then(function(data) { + * var aclObject = data[0]; + * var apiResponse = data[1]; + * }); */ Storage.acl = { OWNER_ROLE: 'OWNER', @@ -227,6 +234,14 @@ Storage.prototype.channel = function(id, resourceId) { * }; * * gcs.createBucket('new-bucket', metadata, callback); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * gcs.createBucket('new-bucket').then(function(data) { + * var bucket = data[0]; + * var apiResponse = data[1]; + * }); */ Storage.prototype.createBucket = function(name, metadata, callback) { var self = this; @@ -289,7 +304,7 @@ Storage.prototype.createBucket = function(name, metadata, callback) { * return. * @param {string} query.pageToken - A previously-returned page token * representing part of the larger set of results to view. - * @param {function=} callback - The callback function. + * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this request * @param {module:storage/bucket[]} callback.buckets - List of all buckets from * your project. @@ -326,25 +341,11 @@ Storage.prototype.createBucket = function(name, metadata, callback) { * }, callback); * * //- - * // Get the buckets from your project as a readable object stream. - * //- - * gcs.getBuckets() - * .on('error', console.error) - * .on('data', function(bucket) { - * // bucket is a Bucket object. - * }) - * .on('end', function() { - * // All buckets retrieved. - * }); - * - * //- - * // If you anticipate many results, you can end a stream early to prevent - * // unnecessary processing and API requests. + * // If the callback is omitted, we'll return a Promise. * //- - * gcs.getBuckets() - * .on('data', function(bucket) { - * this.end(); - * }); + * gcs.getBuckets().then(function(data) { + * var buckets = data[0]; + * }); */ Storage.prototype.getBuckets = function(query, callback) { var self = this; @@ -380,12 +381,49 @@ Storage.prototype.getBuckets = function(query, callback) { }); }; +/** + * Get {module:storage/bucket} objects for all of the buckets in your project as + * a readable object stream. + * + * @param {object=} query - Configuration object. See + * {module:storage#getBuckets} for a complete list of options. + * @return {stream} + * + * @example + * gcs.getBucketsStream() + * .on('error', console.error) + * .on('data', function(bucket) { + * // bucket is a Bucket object. + * }) + * .on('end', function() { + * // All buckets retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gcs.getBucketsStream() + * .on('data', function(bucket) { + * this.end(); + * }); + */ +Storage.prototype.getBucketsStream = common.paginator.streamify('getBuckets'); + +/*! Developer Documentation + * + * These methods can be auto-paginated. + */ +common.paginator.extend(Storage, 'getBuckets'); + /*! Developer Documentation * - * This method can be used with either a callback or as a readable object - * stream. `streamRouter` is used to add this dual behavior. + * All async methods (except for streams) will return a Promise in the event + * that a callback is omitted. */ -common.streamRouter.extend(Storage, 'getBuckets'); +common.util.promisifyAll(Storage, { + exclude: ['bucket', 'channel'] +}); Storage.Bucket = Bucket; Storage.Channel = Channel; diff --git a/packages/storage/system-test/storage.js b/packages/storage/system-test/storage.js index a0de22af6b5..6967bd6e6b0 100644 --- a/packages/storage/system-test/storage.js +++ b/packages/storage/system-test/storage.js @@ -507,7 +507,7 @@ describe('storage', function() { it('should get buckets as a stream', function(done) { var bucketEmitted = false; - storage.getBuckets() + storage.getBucketsStream() .on('error', done) .on('data', function(bucket) { bucketEmitted = bucket instanceof Bucket; @@ -1022,7 +1022,7 @@ describe('storage', function() { it('should get files as a stream', function(done) { var fileEmitted = false; - bucket.getFiles() + bucket.getFilesStream() .on('error', done) .on('data', function(file) { fileEmitted = file instanceof File; diff --git a/packages/storage/test/acl.js b/packages/storage/test/acl.js index 3f88cb09b74..4841bb83438 100644 --- a/packages/storage/test/acl.js +++ b/packages/storage/test/acl.js @@ -18,24 +18,48 @@ var assert = require('assert'); var async = require('async'); +var extend = require('extend'); +var proxyquire = require('proxyquire'); var util = require('@google-cloud/common').util; -var Acl = require('../src/acl.js'); +var promisified = false; +var fakeUtil = extend({}, util, { + promisifyAll: function(Class) { + if (Class.name === 'Acl') { + promisified = true; + } + } +}); + var Storage = require('../'); +var Acl; describe('storage/acl', function() { var acl; + var ERROR = new Error('Error.'); var MAKE_REQ = util.noop; var PATH_PREFIX = '/acl'; var ROLE = Storage.acl.OWNER_ROLE; var ENTITY = 'user-user@example.com'; + before(function() { + Acl = proxyquire('../src/acl.js', { + '@google-cloud/common': { + util: fakeUtil + } + }); + }); + beforeEach(function() { acl = new Acl({ request: MAKE_REQ, pathPrefix: PATH_PREFIX }); }); describe('initialization', function() { + it('should promisify all the things', function() { + assert(promisified); + }); + it('should assign makeReq and pathPrefix', function() { assert.strictEqual(acl.pathPrefix, PATH_PREFIX); assert.strictEqual(acl.request_, MAKE_REQ); @@ -478,5 +502,18 @@ describe('storage/AclRoleAccessorMethods', function() { } ], done); }); + + it('should return the parent methods return value', function() { + var fakeReturn = {}; + + aclEntity.add = function() { + return fakeReturn; + }; + + aclEntity._assignAccessMethods('fakerole'); + + var value = aclEntity.fakeroles.addUser('email@example.com'); + assert.strictEqual(value, fakeReturn); + }); }); }); diff --git a/packages/storage/test/bucket.js b/packages/storage/test/bucket.js index 249100b50bc..d3229c7961c 100644 --- a/packages/storage/test/bucket.js +++ b/packages/storage/test/bucket.js @@ -69,8 +69,20 @@ fakeAsync.eachLimit = function() { (eachLimitOverride || async.eachLimit).apply(null, arguments); }; +var promisified = false; +var fakeUtil = extend({}, util, { + promisifyAll: function(Class, options) { + if (Class.name !== 'Bucket') { + return; + } + + promisified = true; + assert.deepEqual(options.exclude, ['file']); + } +}); + var extended = false; -var fakeStreamRouter = { +var fakePaginator = { extend: function(Class, methods) { if (Class.name !== 'Bucket') { return; @@ -80,6 +92,9 @@ var fakeStreamRouter = { assert.equal(Class.name, 'Bucket'); assert.deepEqual(methods, ['getFiles']); extended = true; + }, + streamify: function(methodName) { + return methodName; } }; @@ -109,7 +124,8 @@ describe('Bucket', function() { request: fakeRequest, '@google-cloud/common': { ServiceObject: FakeServiceObject, - streamRouter: fakeStreamRouter + paginator: fakePaginator, + util: fakeUtil }, './acl.js': FakeAcl, './file.js': FakeFile @@ -124,7 +140,15 @@ describe('Bucket', function() { describe('instantiation', function() { it('should extend the correct methods', function() { - assert(extended); // See `fakeStreamRouter.extend` + assert(extended); // See `fakePaginator.extend` + }); + + it('should streamify the correct methods', function() { + assert.strictEqual(bucket.getFilesStream, 'getFiles'); + }); + + it('should promisify all the things', function() { + assert(promisified); }); it('should localize the name', function() { diff --git a/packages/storage/test/channel.js b/packages/storage/test/channel.js index ace4f4c34a6..f00628ceb94 100644 --- a/packages/storage/test/channel.js +++ b/packages/storage/test/channel.js @@ -21,9 +21,20 @@ 'use strict'; var assert = require('assert'); +var extend = require('extend'); var nodeutil = require('util'); var proxyquire = require('proxyquire'); var ServiceObject = require('@google-cloud/common').ServiceObject; +var util = require('@google-cloud/common').util; + +var promisified = false; +var fakeUtil = extend({}, util, { + promisifyAll: function(Class) { + if (Class.name === 'Channel') { + promisified = true; + } + } +}); function FakeServiceObject() { this.calledWith_ = arguments; @@ -43,7 +54,8 @@ describe('Channel', function() { before(function() { Channel = proxyquire('../src/channel.js', { '@google-cloud/common': { - ServiceObject: FakeServiceObject + ServiceObject: FakeServiceObject, + util: fakeUtil } }); }); @@ -64,6 +76,10 @@ describe('Channel', function() { assert.deepEqual(calledWith.methods, {}); }); + it('should promisify all the things', function() { + assert(promisified); + }); + it('should set the default metadata', function() { assert.deepEqual(channel.metadata, { id: ID, diff --git a/packages/storage/test/file.js b/packages/storage/test/file.js index 6f5b5432ad1..8b685e8df22 100644 --- a/packages/storage/test/file.js +++ b/packages/storage/test/file.js @@ -34,6 +34,7 @@ var util = require('@google-cloud/common').util; var Bucket = require('../src/bucket.js'); +var promisified = false; var makeWritableStreamOverride; var handleRespOverride; var fakeUtil = extend({}, util, { @@ -44,6 +45,15 @@ var fakeUtil = extend({}, util, { makeWritableStream: function() { var args = arguments; (makeWritableStreamOverride || util.makeWritableStream).apply(null, args); + }, + + promisifyAll: function(Class, options) { + if (Class.name !== 'File') { + return; + } + + promisified = true; + assert.deepEqual(options.exclude, ['setEncryptionKey']); } }); @@ -135,6 +145,10 @@ describe('File', function() { }); describe('initialization', function() { + it('should promisify all the things', function() { + assert(promisified); + }); + it('should assign file name', function() { assert.equal(file.name, FILE_NAME); }); @@ -545,7 +559,7 @@ describe('File', function() { it('should send query.generation if File has one', function(done) { var versionedFile = new File(BUCKET, 'file.txt', { generation: 1 }); - versionedFile.request = function(rOpts) { + versionedFile.requestStream = function(rOpts) { assert.equal(rOpts.qs.generation, 1); setImmediate(done); return duplexify(); @@ -555,7 +569,7 @@ describe('File', function() { }); it('should end request stream on error', function(done) { - file.request = getFakeSuccessfulRequest('body', { body: null }); + file.requestStream = getFakeSuccessfulRequest('body', { body: null }); var readStream = file.createReadStream(); @@ -564,8 +578,8 @@ describe('File', function() { // Let the error handler from createReadStream assign. setImmediate(function() { readStream.emit('error'); - assert(file.request.wasRequestAborted()); - assert(file.request.wasRequestDestroyed()); + assert(file.requestStream.wasRequestAborted()); + assert(file.requestStream.wasRequestDestroyed()); done(); }); }); @@ -573,7 +587,7 @@ describe('File', function() { it('should confirm the abort method exists', function(done) { var reqStream = through(); - file.request = function() { + file.requestStream = function() { return reqStream; }; @@ -596,7 +610,7 @@ describe('File', function() { o: encodeURIComponent(file.name) }); - file.request = function(opts) { + file.requestStream = function(opts) { assert.equal(opts.uri, expectedPath); setImmediate(function() { done(); @@ -608,7 +622,7 @@ describe('File', function() { }); it('should accept gzip encoding', function(done) { - file.request = function(opts) { + file.requestStream = function(opts) { assert.strictEqual(opts.gzip, true); setImmediate(function() { done(); @@ -623,7 +637,7 @@ describe('File', function() { var ERROR = new Error('Error.'); beforeEach(function() { - file.request = function(opts) { + file.requestStream = function(opts) { var stream = (requestOverride || request)(opts); setImmediate(function() { @@ -645,13 +659,13 @@ describe('File', function() { }); }); - describe('request', function() { + describe('requestStream', function() { it('should get readable stream from request', function(done) { var fakeRequest = { a: 'b', c: 'd' }; requestOverride = getFakeRequest(); - file.request = function() { + file.requestStream = function() { setImmediate(function() { assert.deepEqual(requestOverride.getRequestOptions(), fakeRequest); done(); @@ -664,7 +678,7 @@ describe('File', function() { }); it('should emit response event from request', function(done) { - file.request = getFakeSuccessfulRequest('body'); + file.requestStream = getFakeSuccessfulRequest('body'); file.createReadStream({ validation: false }) .on('response', function() { @@ -683,7 +697,7 @@ describe('File', function() { done(); }; - file.request = function() { + file.requestStream = function() { var stream = through(); setImmediate(function() { stream.emit('response', response); @@ -698,7 +712,7 @@ describe('File', function() { var requestStream = through(); var readStream = file.createReadStream(); - file.request = function() { + file.requestStream = function() { setImmediate(function() { // Must be a stream. Doesn't matter for the tests, though. requestStream.emit('response', through()); @@ -734,7 +748,7 @@ describe('File', function() { done(); }; - file.request = function() { + file.requestStream = function() { var stream = through(); setImmediate(function() { stream.emit('complete', response); @@ -749,7 +763,7 @@ describe('File', function() { var ERROR = new Error('Error.'); beforeEach(function() { - file.request = getFakeFailedRequest(ERROR); + file.requestStream = getFakeFailedRequest(ERROR); }); it('should emit the error', function(done) { @@ -790,7 +804,7 @@ describe('File', function() { it('should destroy the stream on error', function(done) { var error = new Error('Error.'); - file.request = getFakeSuccessfulRequest('data'); + file.requestStream = getFakeSuccessfulRequest('data'); handleRespOverride = function(err, resp, body, callback) { callback(error); @@ -801,7 +815,8 @@ describe('File', function() { assert.strictEqual(err, error); setImmediate(function() { - assert.strictEqual(file.request.wasRequestDestroyed(), true); + assert + .strictEqual(file.requestStream.wasRequestDestroyed(), true); done(); }); }) @@ -809,7 +824,8 @@ describe('File', function() { }); it('should validate with crc32c', function(done) { - file.request = getFakeSuccessfulRequest(data, fakeResponse.crc32c); + file.requestStream = + getFakeSuccessfulRequest(data, fakeResponse.crc32c); file.createReadStream({ validation: 'crc32c' }) .on('error', done) @@ -818,7 +834,7 @@ describe('File', function() { }); it('should emit an error if crc32c validation fails', function(done) { - file.request = getFakeSuccessfulRequest( + file.requestStream = getFakeSuccessfulRequest( 'bad-data', fakeResponse.crc32c ); @@ -832,7 +848,7 @@ describe('File', function() { }); it('should validate with md5', function(done) { - file.request = getFakeSuccessfulRequest(data, fakeResponse.md5); + file.requestStream = getFakeSuccessfulRequest(data, fakeResponse.md5); file.createReadStream({ validation: 'md5' }) .on('error', done) @@ -841,7 +857,8 @@ describe('File', function() { }); it('should emit an error if md5 validation fails', function(done) { - file.request = getFakeSuccessfulRequest('bad-data', fakeResponse.md5); + file.requestStream = + getFakeSuccessfulRequest('bad-data', fakeResponse.md5); file.createReadStream({ validation: 'md5' }) .on('error', function(err) { @@ -852,7 +869,7 @@ describe('File', function() { }); it('should default to md5 validation', function(done) { - file.request = getFakeSuccessfulRequest(data, { + file.requestStream = getFakeSuccessfulRequest(data, { headers: { 'x-goog-hash': 'md5=fakefakefake' } }); @@ -865,7 +882,7 @@ describe('File', function() { }); it('should ignore a data mismatch if validation: false', function(done) { - file.request = getFakeSuccessfulRequest(data, { + file.requestStream = getFakeSuccessfulRequest(data, { headers: { 'x-goog-hash': 'md5=fakefakefake' } }); @@ -877,7 +894,7 @@ describe('File', function() { describe('destroying the through stream', function() { it('should destroy after failed validation', function(done) { - file.request = getFakeSuccessfulRequest( + file.requestStream = getFakeSuccessfulRequest( 'bad-data', fakeResponse.crc32c ); @@ -896,7 +913,7 @@ describe('File', function() { it('should accept a start range', function(done) { var startOffset = 100; - file.request = function(opts) { + file.requestStream = function(opts) { setImmediate(function() { assert.equal(opts.headers.Range, 'bytes=' + startOffset + '-'); done(); @@ -910,7 +927,7 @@ describe('File', function() { it('should accept an end range and set start to 0', function(done) { var endOffset = 100; - file.request = function(opts) { + file.requestStream = function(opts) { setImmediate(function() { assert.equal(opts.headers.Range, 'bytes=0-' + endOffset); done(); @@ -925,7 +942,7 @@ describe('File', function() { var startOffset = 100; var endOffset = 101; - file.request = function(opts) { + file.requestStream = function(opts) { setImmediate(function() { var expectedRange = 'bytes=' + startOffset + '-' + endOffset; assert.equal(opts.headers.Range, expectedRange); @@ -941,7 +958,7 @@ describe('File', function() { var startOffset = 0; var endOffset = 0; - file.request = function(opts) { + file.requestStream = function(opts) { setImmediate(function() { var expectedRange = 'bytes=0-0'; assert.equal(opts.headers.Range, expectedRange); @@ -954,7 +971,7 @@ describe('File', function() { }); it('should end the through stream', function(done) { - file.request = getFakeSuccessfulRequest('body', { body: null }); + file.requestStream = getFakeSuccessfulRequest('body', { body: null }); var readStream = file.createReadStream({ start: 100 }); readStream.end = done; @@ -966,7 +983,7 @@ describe('File', function() { it('should make a request for the tail bytes', function(done) { var endOffset = -10; - file.request = function(opts) { + file.requestStream = function(opts) { setImmediate(function() { assert.equal(opts.headers.Range, 'bytes=' + endOffset); done(); diff --git a/packages/storage/test/index.js b/packages/storage/test/index.js index 178a6fc19ed..33855e62cbc 100644 --- a/packages/storage/test/index.js +++ b/packages/storage/test/index.js @@ -36,7 +36,7 @@ function FakeService() { nodeutil.inherits(FakeService, Service); var extended = false; -var fakeStreamRouter = { +var fakePaginator = { extend: function(Class, methods) { if (Class.name !== 'Storage') { return; @@ -46,10 +46,23 @@ var fakeStreamRouter = { assert.equal(Class.name, 'Storage'); assert.deepEqual(methods, ['getBuckets']); extended = true; + }, + streamify: function(methodName) { + return methodName; } }; -var fakeUtil = extend({}, util); +var promisified = false; +var fakeUtil = extend({}, util, { + promisifyAll: function(Class, options) { + if (Class.name !== 'Storage') { + return; + } + + promisified = true; + assert.deepEqual(options.exclude, ['bucket', 'channel']); + } +}); describe('Storage', function() { var PROJECT_ID = 'project-id'; @@ -61,7 +74,7 @@ describe('Storage', function() { Storage = proxyquire('../', { '@google-cloud/common': { Service: FakeService, - streamRouter: fakeStreamRouter, + paginator: fakePaginator, util: fakeUtil }, './channel.js': FakeChannel, @@ -75,7 +88,15 @@ describe('Storage', function() { describe('instantiation', function() { it('should extend the correct methods', function() { - assert(extended); // See `fakeStreamRouter.extend` + assert(extended); // See `fakePaginator.extend` + }); + + it('should streamify the correct methods', function() { + assert.strictEqual(storage.getBucketsStream, 'getBuckets'); + }); + + it('should promisify all the things', function() { + assert(promisified); }); it('should normalize the arguments', function() {