diff --git a/lib/storage/acl.js b/lib/storage/acl.js index 2267bbbca96..7d5b580c12f 100644 --- a/lib/storage/acl.js +++ b/lib/storage/acl.js @@ -20,6 +20,7 @@ 'use strict'; +var arrify = require('arrify'); var is = require('is'); var nodeutil = require('util'); @@ -66,8 +67,8 @@ var nodeutil = require('util'); function Acl(options) { AclRoleAccessorMethods.call(this); - this.makeReq = options.makeReq; this.pathPrefix = options.pathPrefix; + this.request_ = options.request; } /** @@ -232,28 +233,29 @@ nodeutil.inherits(Acl, AclRoleAccessorMethods); * }, function(err, aclObject, apiResponse) {}); */ Acl.prototype.add = function(options, callback) { - var that = this; + var self = this; - var body = { - entity: options.entity, - role: options.role.toUpperCase() - }; - - var query = null; + var query = {}; if (options.generation) { - query = { - generation: options.generation - }; + query.generation = options.generation; } - this.makeReq_('POST', '', query, body, function(err, resp) { + this.request({ + method: 'POST', + uri: '', + qs: query, + json: { + entity: options.entity, + role: options.role.toUpperCase() + } + }, function(err, resp) { if (err) { callback(err, null, resp); return; } - callback(null, that.makeAclObject_(resp), resp); + callback(null, self.makeAclObject_(resp), resp); }); }; @@ -287,16 +289,19 @@ Acl.prototype.add = function(options, callback) { * }, function(err, apiResponse) {}); */ Acl.prototype.delete = function(options, callback) { - var path = '/' + encodeURIComponent(options.entity); - var query = null; + var query = {}; if (options.generation) { - query = { - generation: options.generation - }; + query.generation = options.generation; } - this.makeReq_('DELETE', path, query, null, callback); + this.request({ + method: 'DELETE', + uri: '/' + encodeURIComponent(options.entity), + qs: query + }, function(err, resp) { + callback(err, resp); + }); }; /** @@ -346,9 +351,9 @@ Acl.prototype.delete = function(options, callback) { * }, function(err, aclObject, apiResponse) {}); */ Acl.prototype.get = function(options, callback) { - var that = this; + var self = this; var path = ''; - var query = null; + var query = {}; if (is.fn(options)) { callback = options; @@ -357,24 +362,25 @@ Acl.prototype.get = function(options, callback) { path = '/' + encodeURIComponent(options.entity); if (options.generation) { - query = { - generation: options.generation - }; + query.generation = options.generation; } } - this.makeReq_('GET', path, query, null, function(err, resp) { + this.request({ + uri: path, + qs: query, + }, function(err, resp) { if (err) { callback(err, null, resp); return; } - var results = resp; + var results; if (resp.items) { - results = (resp.items || []).map(that.makeAclObject_); + results = arrify(resp.items).map(self.makeAclObject_); } else { - results = that.makeAclObject_(results); + results = self.makeAclObject_(resp); } callback(null, results, resp); @@ -420,27 +426,28 @@ Acl.prototype.get = function(options, callback) { * }, function(err, aclObject, apiResponse) {}); */ Acl.prototype.update = function(options, callback) { - var that = this; - var path = '/' + encodeURIComponent(options.entity); - var query = null; + var self = this; + + var query = {}; if (options.generation) { - query = { - generation: options.generation - }; + query.generation = options.generation; } - var body = { - role: options.role.toUpperCase() - }; - - this.makeReq_('PUT', path, query, body, function(err, resp) { + this.request({ + method: 'PUT', + uri: '/' + encodeURIComponent(options.entity), + qs: query, + json: { + role: options.role.toUpperCase() + } + }, function(err, resp) { if (err) { callback(err, null, resp); return; } - callback(null, that.makeAclObject_(resp), resp); + callback(null, self.makeAclObject_(resp), resp); }); }; @@ -473,8 +480,9 @@ Acl.prototype.makeAclObject_ = function(accessControlObject) { * @param {*} body - Request body contents. * @param {function} callback - The callback function. */ -Acl.prototype.makeReq_ = function(method, path, query, body, callback) { - this.makeReq(method, this.pathPrefix + path, query, body, callback); +Acl.prototype.request = function(reqOpts, callback) { + reqOpts.uri = this.pathPrefix + reqOpts.uri; + this.request_(reqOpts, callback); }; module.exports = Acl; @@ -522,7 +530,7 @@ AclRoleAccessorMethods.roles = [ ]; AclRoleAccessorMethods.prototype._assignAccessMethods = function(role) { - var that = this; + var self = this; var accessMethods = AclRoleAccessorMethods.accessMethods; var entities = AclRoleAccessorMethods.entities; @@ -553,7 +561,7 @@ AclRoleAccessorMethods.prototype._assignAccessMethods = function(role) { callback = entityId; } - that[accessMethod]({ + self[accessMethod]({ entity: apiEntity, role: role }, callback); diff --git a/lib/storage/bucket.js b/lib/storage/bucket.js index c10ed8b9d54..ea7c1e32fca 100644 --- a/lib/storage/bucket.js +++ b/lib/storage/bucket.js @@ -20,12 +20,13 @@ 'use strict'; +var arrify = require('arrify'); var async = require('async'); var extend = require('extend'); -var format = require('string-format-obj'); var fs = require('fs'); var is = require('is'); var mime = require('mime-types'); +var nodeutil = require('util'); var path = require('path'); /** @@ -41,22 +42,22 @@ var Acl = require('./acl.js'); var File = require('./file.js'); /** - * @type {module:common/streamrouter} + * @type {module:common/serviceObject} * @private */ -var streamRouter = require('../common/stream-router.js'); +var ServiceObject = require('../common/service-object.js'); /** - * @type {module:common/util} + * @type {module:common/streamrouter} * @private */ -var util = require('../common/util.js'); +var streamRouter = require('../common/stream-router.js'); /** - * @const {string} + * @type {module:common/util} * @private */ -var STORAGE_BASE_URL = 'https://www.googleapis.com/storage/v1/b'; +var util = require('../common/util.js'); /** * The size of a file (in bytes) must be greater than this number to @@ -87,20 +88,139 @@ var RESUMABLE_THRESHOLD = 5000000; * var gcloud = require('gcloud'); * * var gcs = gcloud.storage({ + * keyFilename: '/path/to/keyfile.json', * projectId: 'grape-spaceship-123' * }); * - * var albums = gcs.bucket('albums'); + * var bucket = gcs.bucket('albums'); */ + function Bucket(storage, name) { - this.metadata = {}; + var methods = { + /** + * Create a bucket. + * + * @param {object=} config - See {module:storage#createBucket}. + * + * @example + * bucket.create(function(err, zone, apiResponse) { + * if (!err) { + * // The zone was created successfully. + * } + * }); + */ + create: true, + + /** + * Delete the bucket. + * + * @resource [Buckets: delete API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/buckets/delete} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * bucket.delete(function(err, apiResponse) {}); + */ + delete: true, + + /** + * Check if the bucket exists. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {boolean} callback.exists - Whether the bucket exists or not. + * + * @example + * bucket.exists(function(err, exists) {}); + */ + exists: true, + + /** + * Get a bucket if it exists. + * + * You may optionally use this to "get or create" an object by providing an + * object with `autoCreate` set to `true`. Any extra configuration that is + * normally required for the `create` method must be contained within this + * object as well. + * + * @param {options=} options - Configuration object. + * @param {boolean} options.autoCreate - Automatically create the object if + * it does not exist. Default: `false` + * + * @example + * bucket.get(function(err, bucket, apiResponse) { + * // `bucket.metadata` has been populated. + * }); + */ + get: true, + + /** + * Get the bucket's metadata. + * + * To set metadata, see {module:storage/bucket#setMetadata}. + * + * @resource [Buckets: get API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/buckets/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.metadata - Tbe bucket's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * bucket.getMetadata(function(err, metadata, apiResponse) {}); + */ + getMetadata: true, + + /** + * Set the bucket's metadata. + * + * @resource [Buckets: patch API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/buckets/patch} + * + * @param {object} metadata - The metadata you wish to set. + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * //- + * // Set website metadata field on the bucket. + * //- + * bucket.setMetadata({ + * website: { + * mainPageSuffix: 'http://example.com', + * notFoundPage: 'http://example.com/404.html' + * } + * }, function(err, apiResponse) {}); + * + * //- + * // Enable versioning for your bucket. + * //- + * bucket.setMetadata({ + * versioning: { + * enabled: true + * } + * }, function(err, apiResponse) {}); + */ + setMetadata: true + }; + + ServiceObject.call(this, { + parent: storage, + baseUrl: '/b', + id: name, + createMethod: storage.createBucket.bind(storage), + methods: methods + }); + this.name = name; this.storage = storage; - if (!this.name) { - throw new Error('A bucket name is needed to use Google Cloud Storage.'); - } - /** * Google Cloud Storage uses access control lists (ACLs) to manage object and * bucket access. ACLs are the mechanism you use to share objects with other @@ -138,12 +258,12 @@ function Bucket(storage, name) { * }, function(err, aclObject) {}); */ this.acl = new Acl({ - makeReq: this.makeReq_.bind(this), + request: this.request.bind(this), pathPrefix: '/acl' }); this.acl.default = new Acl({ - makeReq: this.makeReq_.bind(this), + request: this.request.bind(this), pathPrefix: '/defaultObjectAcl' }); @@ -209,6 +329,8 @@ function Bucket(storage, name) { /* jshint ignore:end */ } +nodeutil.inherits(Bucket, ServiceObject); + /** * Combine mutliple files into one new file. * @@ -269,13 +391,10 @@ Bucket.prototype.combine = function(sources, destination, callback) { } } - this.storage.makeAuthenticatedRequest_({ + // Make the request from the destination File object. + destination.request({ method: 'POST', - uri: format('{base}/{destBucket}/o/{destFile}/compose', { - base: STORAGE_BASE_URL, - destBucket: destination.bucket.name, - destFile: encodeURIComponent(destination.name) - }), + uri: '/compose', json: { destination: { contentType: destination.metadata.contentType @@ -310,24 +429,6 @@ Bucket.prototype.combine = function(sources, destination, callback) { } }; -/** - * Delete the bucket. - * - * @resource [Buckets: delete API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/buckets/delete} - * - * @param {function=} callback - The callback function. - * @param {?error} callback.err - An error returned while making this request - * @param {object} callback.apiResponse - The full API response. - * - * @example - * var bucket = gcs.bucket('delete-me'); - * bucket.delete(function(err, apiResponse) {}); - */ -Bucket.prototype.delete = function(callback) { - callback = callback || util.noop; - this.makeReq_('DELETE', '', null, true, callback); -}; - /** * Iterate over the bucket's files, calling `file.delete()` on each. * @@ -444,6 +545,10 @@ Bucket.prototype.deleteFiles = function(query, callback) { * var file = bucket.file('my-existing-file.png'); */ Bucket.prototype.file = function(name, options) { + if (!name) { + throw Error('A file name must be specified.'); + } + return new File(this, name, options); }; @@ -544,62 +649,39 @@ Bucket.prototype.getFiles = function(query, callback) { query = {}; } - this.makeReq_('GET', '/o', query, true, function(err, resp) { + this.request({ + uri: '/o', + qs: query + }, function(err, resp) { if (err) { callback(err, null, null, resp); return; } - var files = (resp.items || []).map(function(item) { + var files = arrify(resp.items).map(function(file) { var options = {}; if (query.versions) { - options.generation = item.generation; + options.generation = file.generation; } - var file = self.file(item.name, options); - file.metadata = item; + var fileInstance = self.file(file.name, options); + fileInstance.metadata = file; - return file; + return fileInstance; }); var nextQuery = null; - if (resp.nextPageToken) { - nextQuery = extend({}, query, { pageToken: resp.nextPageToken }); + nextQuery = extend({}, query, { + pageToken: resp.nextPageToken + }); } callback(null, files, nextQuery, resp); }); }; -/** - * Get the bucket's metadata. - * - * To set metadata, see {module:storage/bucket#setMetadata}. - * - * @resource [Buckets: get API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/buckets/get} - * - * @param {function=} callback - The callback function. - * @param {?error} callback.err - An error returned while making this request - * @param {object} callback.metadata - Tbe bucket's metadata. - * @param {object} callback.apiResponse - The full API response. - * - * @example - * bucket.getMetadata(function(err, metadata, apiResponse) {}); - */ -Bucket.prototype.getMetadata = function(callback) { - callback = callback || util.noop; - this.makeReq_('GET', '', null, true, function(err, resp) { - if (err) { - callback(err, null, resp); - return; - } - this.metadata = resp; - callback(null, this.metadata, resp); - }.bind(this)); -}; - /** * Make the bucket listing private. * @@ -685,9 +767,16 @@ Bucket.prototype.makePrivate = function(options, callback) { // You aren't allowed to set both predefinedAcl & acl properties on a bucket // so acl must explicitly be nullified. - var metadata = { acl: null }; + var metadata = { + acl: null + }; - self.makeReq_('PATCH', '', query, metadata, function(err, resp) { + self.request({ + method: 'PATCH', + uri: '', + qs: query, + json: metadata + }, function(err, resp) { if (err) { done(err); return; @@ -816,52 +905,6 @@ Bucket.prototype.makePublic = function(options, callback) { } }; -/** - * Set the bucket's metadata. - * - * @resource [Buckets: patch API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/buckets/patch} - * - * @param {object} metadata - The metadata you wish to set. - * @param {function=} callback - The callback function. - * @param {?error} callback.err - An error returned while making this request - * @param {object} callback.apiResponse - The full API response. - * - * @example - * //- - * // Set website metadata field on the bucket. - * //- - * bucket.setMetadata({ - * website: { - * mainPageSuffix: 'http://example.com', - * notFoundPage: 'http://example.com/404.html' - * } - * }, function(err, apiResponse) {}); - * - * //- - * // Enable versioning for your bucket. - * //- - * bucket.setMetadata({ - * versioning: { - * enabled: true - * } - * }, function(err, apiResponse) {}); - */ -Bucket.prototype.setMetadata = function(metadata, callback) { - var that = this; - callback = callback || util.noop; - - this.makeReq_('PATCH', '', null, metadata, function(err, resp) { - if (err) { - callback(err, resp); - return; - } - - that.metadata = resp; - - callback(null, resp); - }); -}; - /** * Upload a file to the bucket. This is a convenience method that wraps * {module:storage/file#createWriteStream}. @@ -1081,32 +1124,6 @@ Bucket.prototype.makeAllFilesPublicPrivate_ = function(options, callback) { }); }; -/** - * Make a new request object from the provided arguments and wrap the callback - * to intercept non-successful responses. - * - * @private - * - * @param {string} method - Action. - * @param {string} path - Request path. - * @param {*} query - Request query object. - * @param {*} body - Request body contents. - * @param {function} callback - The callback function. - */ -Bucket.prototype.makeReq_ = function(method, path, query, body, callback) { - var reqOpts = { - method: method, - qs: query, - uri: STORAGE_BASE_URL + '/' + this.name + path - }; - - if (body) { - reqOpts.json = body; - } - - this.storage.makeAuthenticatedRequest_(reqOpts, callback); -}; - /*! Developer Documentation * * This method can be used with either a callback or as a readable object diff --git a/lib/storage/file.js b/lib/storage/file.js index 1870da0d62d..d58c94ec2cb 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -28,11 +28,12 @@ var format = require('string-format-obj'); var fs = require('fs'); var hashStreamValidation = require('hash-stream-validation'); var is = require('is'); +var nodeutil = require('util'); var once = require('once'); var pumpify = require('pumpify'); +var resumableUpload = require('gcs-resumable-upload'); var streamEvents = require('stream-events'); var through = require('through2'); -var resumableUpload = require('gcs-resumable-upload'); var zlib = require('zlib'); /** @@ -41,6 +42,12 @@ var zlib = require('zlib'); */ var Acl = require('./acl.js'); +/** + * @type {module:common/serviceObject} + * @private + */ +var ServiceObject = require('../common/service-object.js'); + /** * @type {module:common/util} * @private @@ -57,6 +64,12 @@ var SigningError = createErrorClass('SigningError', function(message) { this.message = message; }); +/** + * @const {string} + * @private + */ +var STORAGE_DOWNLOAD_BASE_URL = 'https://storage.googleapis.com'; + /** * @const {string} * @private @@ -77,18 +90,57 @@ var STORAGE_UPLOAD_BASE_URL = 'https://www.googleapis.com/upload/storage/v1/b'; * * @alias module:storage/file * @constructor + * + * @example + * var gcloud = require('gcloud'); + * + * var gcs = gcloud.storage({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var myBucket = gcs.bucket('my-bucket'); + * + * var file = myBucket.file('my-file'); */ function File(bucket, name, options) { - if (!name) { - throw Error('A file name must be specified.'); - } + var methods = { + /** + * Check if the file exists. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {boolean} callback.exists - Whether the file exists or not. + * + * @example + * file.exists(function(err, exists) {}); + */ + exists: true, + + /** + * Get a file object and its metadata if it exists. + * + * @example + * file.get(function(err, file, apiResponse) { + * // file.metadata` has been populated. + * }); + */ + get: true + }; + + ServiceObject.call(this, { + parent: bucket, + baseUrl: '/o', + id: encodeURIComponent(name), + methods: methods + }); options = options || {}; this.bucket = bucket; this.generation = parseInt(options.generation, 10); - this.makeReq_ = bucket.makeReq_.bind(bucket); - this.metadata = {}; + this.storage = bucket.parent; Object.defineProperty(this, 'name', { enumerable: true, @@ -117,23 +169,19 @@ function File(bucket, name, options) { * //- * // Make a file publicly readable. * //- - * var gcs = gcloud.storage({ - * projectId: 'grape-spaceship-123' - * }); - * - * var myFile = gcs.bucket('my-bucket').file('my-file'); - * - * myFile.acl.add({ + * file.acl.add({ * entity: 'allUsers', * role: gcs.acl.READER_ROLE * }, function(err, aclObject) {}); */ this.acl = new Acl({ - makeReq: this.makeReq_, - pathPrefix: '/o/' + encodeURIComponent(this.name) + '/acl' + request: this.request.bind(this), + pathPrefix: '/acl' }); } +nodeutil.inherits(File, ServiceObject); + /** * Copy this file to another file. By default, this will copy the file to the * same bucket, but you can choose to copy it to another Bucket by providing @@ -233,124 +281,27 @@ File.prototype.copy = function(destination, callback) { throw noDestinationError; } - var path = format('/o/{srcName}/copyTo/b/{destBucket}/o/{destName}', { - srcName: encodeURIComponent(this.name), - destBucket: destBucket.name, - destName: encodeURIComponent(destName) - }); - var query = {}; - if (this.generation) { query.sourceGeneration = this.generation; } - this.makeReq_('POST', path, query, null, function(err, resp) { - if (err) { - callback(err, null, resp); - return; - } - - callback(null, newFile || destBucket.file(destName), resp); - }); -}; - -/** - * Move this file to another location. By default, this will move the file to - * the same bucket, but you can choose to move it to another Bucket by providing - * either a Bucket or File object. - * - * **Warning**: - * There is currently no atomic `move` method in the Google Cloud Storage API, - * so this method is a composition of {module:storage/file#copy} (to the new - * location) and {module:storage/file#delete} (from the old location). While - * unlikely, it is possible that an error returned to your callback could be - * triggered from either one of these API calls failing, which could leave a - * duplicate file lingering. - * - * @resource [Objects: copy API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/objects/copy} - * - * @throws {Error} If the destination file is not provided. - * - * @param {string|module:storage/bucket|module:storage/file} destination - - * Destination file. - * @param {function=} callback - The callback function. - * @param {?error} callback.err - An error returned while making this request - * @param {module:storage/file} callback.destinationFile - The destination File. - * @param {object} callback.apiResponse - The full API response. - * - * @example - * //- - * // You can pass in a variety of types for the destination. - * // - * // For all of the below examples, assume we are working with the following - * // Bucket and File objects. - * //- - * var bucket = gcs.bucket('my-bucket'); - * var file = bucket.file('my-image.png'); - * - * //- - * // If you pass in a string for the destination, the file is moved to its - * // current bucket, under the new name provided. - * //- - * file.move('my-image-new.png', function(err, destinationFile, apiResponse) { - * // `my-bucket` no longer contains: - * // - "my-image.png" - * // but contains instead: - * // - "my-image-new.png" - * - * // `destinationFile` is an instance of a File object that refers to your - * // new file. - * }); - * - * //- - * // If you pass in a Bucket object, the file will be moved to that bucket - * // using the same name. - * //- - * var anotherBucket = gcs.bucket('another-bucket'); - * - * file.move(anotherBucket, function(err, destinationFile, apiResponse) { - * // `my-bucket` no longer contains: - * // - "my-image.png" - * // - * // `another-bucket` now contains: - * // - "my-image.png" - * - * // `destinationFile` is an instance of a File object that refers to your - * // new file. - * }); - * - * //- - * // If you pass in a File object, you have complete control over the new - * // bucket and filename. - * //- - * var anotherFile = anotherBucket.file('my-awesome-image.png'); - * - * file.move(anotherFile, function(err, destinationFile, apiResponse) { - * // `my-bucket` no longer contains: - * // - "my-image.png" - * // - * // `another-bucket` now contains: - * // - "my-awesome-image.png" - * - * // Note: - * // The `destinationFile` parameter is equal to `anotherFile`. - * }); - */ -File.prototype.move = function(destination, callback) { - var self = this; - - callback = callback || util.noop; + newFile = newFile || destBucket.file(destName); - this.copy(destination, function(err, destinationFile, apiResponse) { + this.request({ + method: 'POST', + uri: format('/copyTo/b/{bucketName}/o/{fileName}', { + bucketName: destBucket.name, + fileName: encodeURIComponent(destName) + }), + qs: query + }, function(err, resp) { if (err) { - callback(err, null, apiResponse); + callback(err, null, resp); return; } - self.delete(function(err, apiResponse) { - callback(err, destinationFile, apiResponse); - }); + callback(null, newFile, resp); }); }; @@ -393,8 +344,7 @@ File.prototype.move = function(destination, callback) { * // backup of your remote data. * //- * var fs = require('fs'); - * var myBucket = gcs.bucket('my-bucket'); - * var remoteFile = myBucket.file('image.png'); + * var remoteFile = bucket.file('image.png'); * var localFilename = '/Users/stephen/Photos/image.png'; * * remoteFile.createReadStream() @@ -459,9 +409,10 @@ File.prototype.createReadStream = function(options) { // returned to the user. function makeRequest() { var reqOpts = { - uri: format('https://storage.googleapis.com/{b}/{o}', { - b: self.bucket.name, - o: encodeURIComponent(self.name) + uri: format('{downloadBaseUrl}/{bucketName}/{fileName}', { + downloadBaseUrl: STORAGE_DOWNLOAD_BASE_URL, + bucketName: self.bucket.name, + fileName: encodeURIComponent(self.name) }), gzip: true }; @@ -481,7 +432,7 @@ File.prototype.createReadStream = function(options) { }; } - var requestStream = self.bucket.storage.makeAuthenticatedRequest_(reqOpts); + var requestStream = self.storage.makeAuthenticatedRequest(reqOpts); var validateStream; // We listen to the response event from the request stream so that we can... @@ -603,9 +554,6 @@ File.prototype.createReadStream = function(options) { * @param {string} callback.uri - The resumable upload's unique session URI. * * @example - * var bucket = gcs.bucket('my-bucket'); - * var file = bucket.file('large-file.zip'); - * * file.createResumableUpload(function(err, uri) { * if (!err) { * // `uri` can be used to PUT data to. @@ -619,7 +567,7 @@ File.prototype.createResumableUpload = function(metadata, callback) { } resumableUpload.createURI({ - authClient: this.bucket.storage.makeAuthenticatedRequest_.authClient, + authClient: this.bucket.storage.authClient, bucket: this.bucket.name, file: this.name, generation: this.generation, @@ -657,6 +605,8 @@ File.prototype.createResumableUpload = function(metadata, callback) { * completely, however this is **not recommended**. * * @example + * var fs = require('fs'); + * * //- * //

Uploading a File

* // @@ -664,11 +614,8 @@ File.prototype.createResumableUpload = function(metadata, callback) { * // have the option of using {module:storage/bucket#upload}, but that is just * // a convenience method which will do the following. * //- - * var fs = require('fs'); - * var image = myBucket.file('image.png'); - * * fs.createReadStream('/Users/stephen/Photos/birthday-at-the-zoo/panda.jpg') - * .pipe(image.createWriteStream()) + * .pipe(file.createWriteStream()) * .on('error', function(err) {}) * .on('finish', function() { * // The file upload is complete. @@ -677,11 +624,8 @@ File.prototype.createResumableUpload = function(metadata, callback) { * //- * //

Uploading a File with gzip compression

* //- - * var fs = require('fs'); - * var htmlFile = myBucket.file('index.html'); - * * fs.createReadStream('/Users/stephen/site/index.html') - * .pipe(htmlFile.createWriteStream({ gzip: true })) + * .pipe(file.createWriteStream({ gzip: true })) * .on('error', function(err) {}) * .on('finish', function() { * // The file upload is complete. @@ -700,11 +644,8 @@ File.prototype.createResumableUpload = function(metadata, callback) { * // {module:storage/bucket#upload} to do this, which is just a wrapper around * // the following. * //- - * var fs = require('fs'); - * var image = myBucket.file('image.png'); - * * fs.createReadStream('/Users/stephen/Photos/birthday-at-the-zoo/panda.jpg') - * .pipe(image.createWriteStream({ + * .pipe(file.createWriteStream({ * metadata: { * contentType: 'image/jpeg', * metadata: { @@ -846,15 +787,17 @@ File.prototype.createWriteStream = function(options) { File.prototype.delete = function(callback) { callback = callback || util.noop; - var path = '/o/' + encodeURIComponent(this.name); - var query = {}; if (this.generation) { query.generation = this.generation; } - this.makeReq_('DELETE', path, query, null, function(err, resp) { + this.request({ + method: 'DELETE', + uri: '', + qs: query + }, function(err, resp) { if (err) { callback(err, resp); return; @@ -930,17 +873,18 @@ File.prototype.download = function(options, callback) { */ File.prototype.getMetadata = function(callback) { var self = this; - callback = callback || util.noop; - var path = '/o/' + encodeURIComponent(this.name); + callback = callback || util.noop; var query = {}; - if (this.generation) { query.generation = this.generation; } - this.makeReq_('GET', path, query, null, function(err, resp) { + this.request({ + uri: '', + qs: query + }, function(err, resp) { if (err) { callback(err, null, resp); return; @@ -1079,9 +1023,7 @@ File.prototype.getSignedPolicy = function(options, callback) { conditions: conditions }; - var makeAuthenticatedRequest_ = this.bucket.storage.makeAuthenticatedRequest_; - - makeAuthenticatedRequest_.getCredentials(function(err, credentials) { + this.storage.getCredentials(function(err, credentials) { if (err) { callback(new SigningError(err.message)); return; @@ -1208,9 +1150,7 @@ File.prototype.getSignedUrl = function(options, callback) { options.resource = '/' + this.bucket.name + '/' + name; - var makeAuthenticatedRequest_ = this.bucket.storage.makeAuthenticatedRequest_; - - makeAuthenticatedRequest_.getCredentials(function(err, credentials) { + this.storage.getCredentials(function(err, credentials) { if (err) { callback(new SigningError(err.message)); return; @@ -1266,67 +1206,6 @@ File.prototype.getSignedUrl = function(options, callback) { }); }; -/** - * Merge the given metadata with the current remote file's metadata. This will - * set metadata if it was previously unset or update previously set metadata. To - * unset previously set metadata, set its value to null. - * - * You can set custom key/value pairs in the metadata key of the given object, - * however the other properties outside of this object must adhere to the - * [official API documentation](https://goo.gl/BOnnCK). - * - * See the examples below for more information. - * - * @resource [Objects: patch API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/objects/patch} - * - * @param {object} metadata - The metadata you wish to update. - * @param {function=} callback - The callback function. - * @param {?error} callback.err - An error returned while making this request - * @param {object} callback.apiResponse - The full API response. - * - * @example - * file.setMetadata({ - * contentType: 'application/x-font-ttf', - * metadata: { - * my: 'custom', - * properties: 'go here' - * } - * }, function(err, apiResponse) {}); - * - * // Assuming current metadata = { hello: 'world', unsetMe: 'will do' } - * file.setMetadata({ - * metadata: { - * abc: '123', // will be set. - * unsetMe: null, // will be unset (deleted). - * hello: 'goodbye' // will be updated from 'hello' to 'goodbye'. - * } - * }, function(err, apiResponse) { - * // metadata should now be { abc: '123', hello: 'goodbye' } - * }); - */ -File.prototype.setMetadata = function(metadata, callback) { - callback = callback || util.noop; - - var that = this; - var path = '/o/' + encodeURIComponent(this.name); - var query = {}; - - if (this.generation) { - query.generation = this.generation; - } - - this.makeReq_('PATCH', path, query, metadata, function(err, resp) { - if (err) { - callback(err, resp); - return; - } - - that.metadata = resp; - - callback(null, resp); - }); -}; - /** * Make a file private to the project and remove all other permissions. * Set `options.strict` to true to make the file private to only the owner. @@ -1352,27 +1231,37 @@ File.prototype.setMetadata = function(metadata, callback) { * file.makePrivate({ strict: true }, function(err) {}); */ File.prototype.makePrivate = function(options, callback) { - var that = this; + var self = this; + if (is.fn(options)) { callback = options; options = {}; } - var path = '/o/' + encodeURIComponent(this.name); - var query = { predefinedAcl: options.strict ? 'private' : 'projectPrivate' }; + + var query = { + predefinedAcl: options.strict ? 'private' : 'projectPrivate' + }; // You aren't allowed to set both predefinedAcl & acl properties on a file, so // acl must explicitly be nullified, destroying all previous acls on the file. - var metadata = { acl: null }; + var metadata = { + acl: null + }; callback = callback || util.noop; - this.makeReq_('PATCH', path, query, metadata, function(err, resp) { + this.request({ + method: 'PATCH', + uri: '', + qs: query, + json: metadata + }, function(err, resp) { if (err) { callback(err, resp); return; } - that.metadata = resp; + self.metadata = resp; callback(null, resp); }); @@ -1401,6 +1290,170 @@ File.prototype.makePublic = function(callback) { }); }; +/** + * Move this file to another location. By default, this will move the file to + * the same bucket, but you can choose to move it to another Bucket by providing + * either a Bucket or File object. + * + * **Warning**: + * There is currently no atomic `move` method in the Google Cloud Storage API, + * so this method is a composition of {module:storage/file#copy} (to the new + * location) and {module:storage/file#delete} (from the old location). While + * unlikely, it is possible that an error returned to your callback could be + * triggered from either one of these API calls failing, which could leave a + * duplicate file lingering. + * + * @resource [Objects: copy API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/objects/copy} + * + * @throws {Error} If the destination file is not provided. + * + * @param {string|module:storage/bucket|module:storage/file} destination - + * Destination file. + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {module:storage/file} callback.destinationFile - The destination File. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * //- + * // You can pass in a variety of types for the destination. + * // + * // For all of the below examples, assume we are working with the following + * // Bucket and File objects. + * //- + * var bucket = gcs.bucket('my-bucket'); + * var file = bucket.file('my-image.png'); + * + * //- + * // If you pass in a string for the destination, the file is moved to its + * // current bucket, under the new name provided. + * //- + * file.move('my-image-new.png', function(err, destinationFile, apiResponse) { + * // `my-bucket` no longer contains: + * // - "my-image.png" + * // but contains instead: + * // - "my-image-new.png" + * + * // `destinationFile` is an instance of a File object that refers to your + * // new file. + * }); + * + * //- + * // If you pass in a Bucket object, the file will be moved to that bucket + * // using the same name. + * //- + * var anotherBucket = gcs.bucket('another-bucket'); + * + * file.move(anotherBucket, function(err, destinationFile, apiResponse) { + * // `my-bucket` no longer contains: + * // - "my-image.png" + * // + * // `another-bucket` now contains: + * // - "my-image.png" + * + * // `destinationFile` is an instance of a File object that refers to your + * // new file. + * }); + * + * //- + * // If you pass in a File object, you have complete control over the new + * // bucket and filename. + * //- + * var anotherFile = anotherBucket.file('my-awesome-image.png'); + * + * file.move(anotherFile, function(err, destinationFile, apiResponse) { + * // `my-bucket` no longer contains: + * // - "my-image.png" + * // + * // `another-bucket` now contains: + * // - "my-awesome-image.png" + * + * // Note: + * // The `destinationFile` parameter is equal to `anotherFile`. + * }); + */ +File.prototype.move = function(destination, callback) { + var self = this; + + callback = callback || util.noop; + + this.copy(destination, function(err, destinationFile, apiResponse) { + if (err) { + callback(err, null, apiResponse); + return; + } + + self.delete(function(err, apiResponse) { + callback(err, destinationFile, apiResponse); + }); + }); +}; + +/** + * Merge the given metadata with the current remote file's metadata. This will + * set metadata if it was previously unset or update previously set metadata. To + * unset previously set metadata, set its value to null. + * + * You can set custom key/value pairs in the metadata key of the given object, + * however the other properties outside of this object must adhere to the + * [official API documentation](https://goo.gl/BOnnCK). + * + * See the examples below for more information. + * + * @resource [Objects: patch API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/objects/patch} + * + * @param {object} metadata - The metadata you wish to update. + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.apiResponse - The full API response. + * + * @example + * file.setMetadata({ + * contentType: 'application/x-font-ttf', + * metadata: { + * my: 'custom', + * properties: 'go here' + * } + * }, function(err, apiResponse) {}); + * + * // Assuming current metadata = { hello: 'world', unsetMe: 'will do' } + * file.setMetadata({ + * metadata: { + * abc: '123', // will be set. + * unsetMe: null, // will be unset (deleted). + * hello: 'goodbye' // will be updated from 'hello' to 'goodbye'. + * } + * }, function(err, apiResponse) { + * // metadata should now be { abc: '123', hello: 'goodbye' } + * }); + */ +File.prototype.setMetadata = function(metadata, callback) { + var self = this; + + callback = callback || util.noop; + + var query = {}; + if (this.generation) { + query.generation = this.generation; + } + + this.request({ + method: 'PATCH', + uri: '', + qs: query, + json: metadata + }, function(err, resp) { + if (err) { + callback(err, resp); + return; + } + + self.metadata = resp; + + callback(null, resp); + }); +}; + /** * This creates a gcs-resumable-upload upload stream. * @@ -1415,7 +1468,7 @@ File.prototype.startResumableUpload_ = function(dup, metadata) { var self = this; var uploadStream = resumableUpload({ - authClient: this.bucket.storage.makeAuthenticatedRequest_.authClient, + authClient: this.storage.authClient, bucket: this.bucket.name, file: this.name, generation: this.generation, @@ -1452,8 +1505,8 @@ File.prototype.startSimpleUpload_ = function(dup, metadata) { qs: { name: self.name }, - uri: format('{base}/{bucket}/o', { - base: STORAGE_UPLOAD_BASE_URL, + uri: format('{uploadBaseUrl}/{bucket}/o', { + uploadBaseUrl: STORAGE_UPLOAD_BASE_URL, bucket: self.bucket.name }) }; @@ -1463,7 +1516,7 @@ File.prototype.startSimpleUpload_ = function(dup, metadata) { } util.makeWritableStream(dup, { - makeAuthenticatedRequest: self.bucket.storage.makeAuthenticatedRequest_, + makeAuthenticatedRequest: this.storage.makeAuthenticatedRequest, metadata: metadata, request: reqOpts }, function(data) { diff --git a/lib/storage/index.js b/lib/storage/index.js index 0cae08b5b93..62247da20b3 100644 --- a/lib/storage/index.js +++ b/lib/storage/index.js @@ -20,7 +20,9 @@ 'use strict'; +var arrify = require('arrify'); var extend = require('extend'); +var nodeutil = require('util'); /** * @type {module:storage/bucket} @@ -29,29 +31,22 @@ var extend = require('extend'); var Bucket = require('./bucket.js'); /** - * @type {module:common/streamrouter} + * @type {module:common/service} * @private */ -var streamRouter = require('../common/stream-router.js'); +var Service = require('../common/service.js'); /** - * @type {module:common/util} - * @private - */ -var util = require('../common/util.js'); - -/** - * Required scopes for Google Cloud Storage API. - * @const {array} + * @type {module:common/streamrouter} * @private */ -var SCOPES = ['https://www.googleapis.com/auth/devstorage.full_control']; +var streamRouter = require('../common/stream-router.js'); /** - * @const {string} + * @type {module:common/util} * @private */ -var STORAGE_BASE_URL = 'https://www.googleapis.com/storage/v1/b'; +var util = require('../common/util.js'); /*! Developer Documentation * @@ -88,16 +83,19 @@ function Storage(options) { return new Storage(options); } - this.makeAuthenticatedRequest_ = util.makeAuthenticatedRequestFactory({ - credentials: options.credentials, - keyFile: options.keyFilename, - scopes: SCOPES, - email: options.email - }); + var config = { + baseUrl: 'https://www.googleapis.com/storage/v1', + projectIdRequired: false, + scopes: [ + 'https://www.googleapis.com/auth/devstorage.full_control' + ] + }; - this.projectId = options.projectId; + Service.call(this, config, options); } +nodeutil.inherits(Storage, Service); + /** * Google Cloud Storage uses access control lists (ACLs) to manage object and * bucket access. ACLs are the mechanism you use to share objects with other @@ -157,7 +155,7 @@ Storage.prototype.acl = Storage.acl; /** * Get a reference to a Google Cloud Storage bucket. * - * @param {object|string} name - Name of the existing bucket. + * @param {object|string} name - Name of the bucket. * @return {module:storage/bucket} * * @example @@ -172,6 +170,10 @@ Storage.prototype.acl = Storage.acl; * var photos = gcs.bucket('photos'); */ Storage.prototype.bucket = function(name) { + if (!name) { + throw new Error('A bucket name is needed to use Google Cloud Storage.'); + } + return new Bucket(this, name); }; @@ -233,19 +235,20 @@ Storage.prototype.bucket = function(name) { */ Storage.prototype.createBucket = function(name, metadata, callback) { var self = this; + if (!name) { throw new Error('A name is required to create a bucket.'); } + if (!callback) { callback = metadata; metadata = {}; } - var query = { - project: this.projectId - }; + var body = extend(metadata, { name: name }); + var storageClasses = { dra: 'DURABLE_REDUCED_AVAILABILITY', nearline: 'NEARLINE' @@ -258,13 +261,22 @@ Storage.prototype.createBucket = function(name, metadata, callback) { } }); - this.makeReq_('POST', '', query, body, function(err, resp) { + this.request({ + method: 'POST', + uri: '/b', + qs: { + project: this.projectId + }, + json: body + }, function(err, resp) { if (err) { callback(err, null, resp); return; } + var bucket = self.bucket(name); bucket.metadata = resp; + callback(null, bucket, resp); }); }; @@ -340,56 +352,39 @@ Storage.prototype.createBucket = function(name, metadata, callback) { * }); */ Storage.prototype.getBuckets = function(query, callback) { - var that = this; + var self = this; + if (!callback) { callback = query; query = {}; } + query.project = query.project || this.projectId; - this.makeReq_('GET', '', query, null, function(err, resp) { + + this.request({ + uri: '/b', + qs: query + }, function(err, resp) { if (err) { callback(err, null, null, resp); return; } - var buckets = (resp.items || []).map(function(item) { - var bucket = that.bucket(item.id); - bucket.metadata = item; - return bucket; + + var buckets = arrify(resp.items).map(function(bucket) { + var bucketInstance = self.bucket(bucket.id); + bucketInstance.metadata = bucket; + return bucketInstance; }); + var nextQuery = null; if (resp.nextPageToken) { nextQuery = extend({}, query, { pageToken: resp.nextPageToken }); } + callback(null, buckets, nextQuery, resp); }); }; -/** - * Make a new request object from the provided arguments and wrap the callback - * to intercept non-successful responses. - * - * @private - * - * @param {string} method - Action. - * @param {string} path - Request path. - * @param {*} query - Request query object. - * @param {*} body - Request body contents. - * @param {function} callback - The callback function. - */ -Storage.prototype.makeReq_ = function(method, path, query, body, callback) { - var reqOpts = { - method: method, - qs: query, - uri: STORAGE_BASE_URL + path - }; - - if (body) { - reqOpts.json = body; - } - - this.makeAuthenticatedRequest_(reqOpts, callback); -}; - /*! Developer Documentation * * This method can be used with either a callback or as a readable object diff --git a/system-test/storage.js b/system-test/storage.js index e7960f99ffd..5c46a55f6ae 100644 --- a/system-test/storage.js +++ b/system-test/storage.js @@ -75,14 +75,10 @@ function setHash(obj, file, done) { } describe('storage', function() { - var bucket; + var bucket = storage.bucket(BUCKET_NAME); before(function(done) { - storage.createBucket(BUCKET_NAME, function(err, newBucket) { - assert.ifError(err); - bucket = newBucket; - done(); - }); + bucket.create(done); }); after(function(done) { @@ -384,7 +380,8 @@ describe('storage', function() { describe('getting buckets', function() { var bucketsToCreate = [ - generateBucketName(), generateBucketName() + generateBucketName(), + generateBucketName() ]; before(function(done) { @@ -685,10 +682,11 @@ describe('storage', function() { }); it('should copy an existing file', function(done) { - bucket.upload(files.logo.path, 'CloudLogo', function(err, file) { + var opts = { destination: 'CloudLogo' }; + bucket.upload(files.logo.path, opts, function(err, file) { assert.ifError(err); + file.copy('CloudLogoCopy', function(err, copiedFile) { - assert.ifError(err); async.parallel([ file.delete.bind(file), copiedFile.delete.bind(copiedFile) @@ -801,20 +799,20 @@ describe('storage', function() { describe('file generations', function() { var VERSIONED_BUCKET_NAME = generateBucketName(); - var versionedBucket; + var versionedBucket = storage.bucket(VERSIONED_BUCKET_NAME); before(function(done) { - var opts = { versioning: { enabled: true } }; - - storage.createBucket(VERSIONED_BUCKET_NAME, opts, function(err, bucket) { - assert.ifError(err); - versionedBucket = bucket; - done(); - }); + versionedBucket.create({ + versioning: { + enabled: true + } + }, done); }); after(function(done) { - versionedBucket.deleteFiles({ versions: true }, function(err) { + versionedBucket.deleteFiles({ + versions: true + }, function(err) { if (err) { done(err); return; diff --git a/test/storage/acl.js b/test/storage/acl.js index 43a62c1cfe2..a5ea659152c 100644 --- a/test/storage/acl.js +++ b/test/storage/acl.js @@ -31,23 +31,22 @@ describe('storage/acl', function() { var ENTITY = 'user-user@example.com'; beforeEach(function() { - acl = new Acl({ makeReq: MAKE_REQ, pathPrefix: PATH_PREFIX }); + acl = new Acl({ request: MAKE_REQ, pathPrefix: PATH_PREFIX }); }); describe('initialization', function() { it('should assign makeReq and pathPrefix', function() { - assert.equal(acl.makeReq, MAKE_REQ); - assert.equal(acl.pathPrefix, PATH_PREFIX); + assert.strictEqual(acl.pathPrefix, PATH_PREFIX); + assert.strictEqual(acl.request_, MAKE_REQ); }); }); describe('add', function() { it('makes the correct api request', function(done) { - acl.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'POST'); - assert.equal(path, ''); - assert.strictEqual(query, null); - assert.deepEqual(body, { entity: ENTITY, role: ROLE }); + acl.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, ''); + assert.deepEqual(reqOpts.json, { entity: ENTITY, role: ROLE }); done(); }; @@ -63,7 +62,7 @@ describe('storage/acl', function() { return expectedAclObject; }; - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(null, apiResponse); }; @@ -75,7 +74,7 @@ describe('storage/acl', function() { }); it('executes the callback with an error', function(done) { - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(ERROR); }; @@ -87,7 +86,8 @@ describe('storage/acl', function() { it('executes the callback with apiResponse', function(done) { var resp = { success: true }; - acl.makeReq_ = function(method, path, query, body, callback) { + + acl.request = function(reqOpts, callback) { callback(null, resp); }; @@ -100,11 +100,9 @@ describe('storage/acl', function() { describe('delete', function() { it('makes the correct api request', function(done) { - acl.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'DELETE'); - assert.equal(path, '/' + encodeURIComponent(ENTITY)); - assert.strictEqual(query, null); - assert.strictEqual(body, null); + acl.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'DELETE'); + assert.strictEqual(reqOpts.uri, '/' + encodeURIComponent(ENTITY)); done(); }; @@ -113,7 +111,7 @@ describe('storage/acl', function() { }); it('should execute the callback with an error', function(done) { - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(ERROR); }; @@ -125,7 +123,8 @@ describe('storage/acl', function() { it('should execute the callback with apiResponse', function(done) { var resp = { success: true }; - acl.makeReq_ = function(method, path, query, body, callback) { + + acl.request = function(reqOpts, callback) { callback(null, resp); }; @@ -139,11 +138,8 @@ describe('storage/acl', function() { describe('get', function() { describe('all ACL objects', function() { it('should make the correct API request', function(done) { - acl.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'GET'); - assert.equal(path, ''); - assert.strictEqual(query, null); - assert.strictEqual(body, null); + acl.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, ''); done(); }; @@ -154,8 +150,8 @@ describe('storage/acl', function() { it('should accept a configuration object', function(done) { var generation = 1; - acl.makeReq_ = function(method, path, query) { - assert.equal(query.generation, generation); + acl.request = function(reqOpts) { + assert.strictEqual(reqOpts.qs.generation, generation); done(); }; @@ -182,7 +178,7 @@ describe('storage/acl', function() { return expectedAclObjects[index]; }; - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(null, apiResponse); }; @@ -196,11 +192,8 @@ describe('storage/acl', function() { describe('ACL object for an entity', function() { it('should get a specific ACL object', function(done) { - acl.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'GET'); - assert.equal(path, '/' + encodeURIComponent(ENTITY)); - assert.strictEqual(query, null); - assert.strictEqual(body, null); + acl.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/' + encodeURIComponent(ENTITY)); done(); }; @@ -211,8 +204,8 @@ describe('storage/acl', function() { it('should accept a configuration object', function(done) { var generation = 1; - acl.makeReq_ = function(method, path, query) { - assert.equal(query.generation, generation); + acl.request = function(reqOpts) { + assert.strictEqual(reqOpts.qs.generation, generation); done(); }; @@ -228,7 +221,7 @@ describe('storage/acl', function() { return expectedAclObject; }; - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(null, apiResponse); }; @@ -241,7 +234,7 @@ describe('storage/acl', function() { }); it('should execute the callback with an error', function(done) { - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(ERROR); }; @@ -253,7 +246,8 @@ describe('storage/acl', function() { it('should execute the callback with apiResponse', function(done) { var resp = { success: true }; - acl.makeReq_ = function(method, path, query, body, callback) { + + acl.request = function(reqOpts, callback) { callback(null, resp); }; @@ -266,11 +260,10 @@ describe('storage/acl', function() { describe('update', function() { it('should make the correct API request', function(done) { - acl.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'PUT'); - assert.equal(path, '/' + encodeURIComponent(ENTITY)); - assert.strictEqual(query, null); - assert.deepEqual(body, { role: ROLE }); + acl.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'PUT'); + assert.strictEqual(reqOpts.uri, '/' + encodeURIComponent(ENTITY)); + assert.deepEqual(reqOpts.json, { role: ROLE }); done(); }; @@ -286,7 +279,7 @@ describe('storage/acl', function() { return expectedAclObject; }; - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(null, apiResponse); }; @@ -298,7 +291,7 @@ describe('storage/acl', function() { }); it('should execute the callback with an error', function(done) { - acl.makeReq_ = function(method, path, query, body, callback) { + acl.request = function(reqOpts, callback) { callback(ERROR); }; @@ -310,7 +303,8 @@ describe('storage/acl', function() { it('should execute the callback with apiResponse', function(done) { var resp = { success: true }; - acl.makeReq_ = function(method, path, query, body, callback) { + + acl.request = function(reqOpts, callback) { callback(null, resp); }; @@ -344,29 +338,6 @@ describe('storage/acl', function() { }); }); }); - - describe('makeReq_', function() { - it('patches requests through to the makeReq function', function(done) { - var method = 'POST'; - var path = '/path'; - var query = { a: 'b', c: 'd' }; - var body = { a: 'b', c: 'd' }; - var callback = util.noop; - - // This is overriding the method we passed on instantiation. - acl.makeReq = function(m, p, q, b, c) { - assert.equal(m, method); - assert.equal(p, PATH_PREFIX + path); - assert.deepEqual(q, query); - assert.deepEqual(b, body); - assert.equal(c, callback); - - done(); - }; - - acl.makeReq_(method, path, query, body, callback); - }); - }); }); describe('storage/AclRoleAccessorMethods', function() { diff --git a/test/storage/bucket.js b/test/storage/bucket.js index bf72eb23b74..0f3132b2161 100644 --- a/test/storage/bucket.js +++ b/test/storage/bucket.js @@ -20,12 +20,14 @@ var arrify = require('arrify'); var assert = require('assert'); var async = require('async'); var extend = require('extend'); -var format = require('string-format-obj'); var mime = require('mime-types'); var mockery = require('mockery'); +var nodeutil = require('util'); var propAssign = require('prop-assign'); var request = require('request'); var stream = require('stream'); + +var ServiceObject = require('../../lib/common/service-object.js'); var util = require('../../lib/common/util.js'); function FakeFile(bucket, name) { @@ -80,25 +82,39 @@ var fakeStreamRouter = { } }; +function FakeAcl() { + this.calledWith_ = [].slice.call(arguments); +} + +function FakeServiceObject() { + this.calledWith_ = arguments; + ServiceObject.apply(this, arguments); +} + +nodeutil.inherits(FakeServiceObject, ServiceObject); + describe('Bucket', function() { var Bucket; - var BUCKET_NAME = 'test-bucket'; var bucket; - var options = { - makeAuthenticatedRequest_: function(req, callback) { - callback(null, req); - } + + var STORAGE = { + createBucket: util.noop }; + var BUCKET_NAME = 'test-bucket'; before(function() { - mockery.registerMock('./file.js', FakeFile); - mockery.registerMock('../common/stream-router.js', fakeStreamRouter); mockery.registerMock('async', fakeAsync); mockery.registerMock('request', fakeRequest); + mockery.registerMock('../common/service-object.js', FakeServiceObject); + mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + mockery.registerMock('./acl.js', FakeAcl); + mockery.registerMock('./file.js', FakeFile); + mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); + Bucket = require('../../lib/storage/bucket.js'); }); @@ -110,7 +126,7 @@ describe('Bucket', function() { beforeEach(function() { requestOverride = null; eachLimitOverride = null; - bucket = new Bucket(options, BUCKET_NAME); + bucket = new Bucket(STORAGE, BUCKET_NAME); }); describe('instantiation', function() { @@ -118,18 +134,68 @@ describe('Bucket', function() { assert(extended); // See `fakeStreamRouter.extend` }); - it('should re-use provided connection', function() { - assert.deepEqual(bucket.authenticateReq_, options.authenticateReq_); + it('should localize the name', function() { + assert.strictEqual(bucket.name, BUCKET_NAME); }); - it('should default metadata to an empty object', function() { - assert.deepEqual(bucket.metadata, {}); + it('should localize the storage instance', function() { + assert.strictEqual(bucket.storage, STORAGE); }); - it('should throw if no name was provided', function() { - assert.throws(function() { - new Bucket(); - }, /A bucket name is needed/); + it('should create an ACL object', function() { + FakeServiceObject.prototype.request = { + bind: function(context) { + return context; + } + }; + + var bucket = new Bucket(STORAGE, BUCKET_NAME); + assert.deepEqual(bucket.acl.calledWith_[0], { + request: bucket, + pathPrefix: '/acl' + }); + }); + + it('should create a default ACL object', function() { + FakeServiceObject.prototype.request = { + bind: function(context) { + return context; + } + }; + + var bucket = new Bucket(STORAGE, BUCKET_NAME); + assert.deepEqual(bucket.acl.default.calledWith_[0], { + request: bucket, + pathPrefix: '/defaultObjectAcl' + }); + }); + + it('should inherit from ServiceObject', function(done) { + var storageInstance = extend({}, STORAGE, { + createBucket: { + bind: function(context) { + assert.strictEqual(context, storageInstance); + done(); + } + } + }); + + var bucket = new Bucket(storageInstance, BUCKET_NAME); + assert(bucket instanceof ServiceObject); + + var calledWith = bucket.calledWith_[0]; + + assert.strictEqual(calledWith.parent, storageInstance); + assert.strictEqual(calledWith.baseUrl, '/b'); + assert.strictEqual(calledWith.id, BUCKET_NAME); + assert.deepEqual(calledWith.methods, { + create: true, + delete: true, + exists: true, + get: true, + getMetadata: true, + setMetadata: true + }); }); }); @@ -157,40 +223,42 @@ describe('Bucket', function() { it('should accept string or file input for sources', function(done) { var file1 = bucket.file('1.txt'); var file2 = '2.txt'; + var destinationFileName = 'destination.txt'; - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { - assert.equal(reqOpts.json.sourceObjects[0].name, file1.name); - assert.equal(reqOpts.json.sourceObjects[1].name, file2); - done(); - }; + var originalFileMethod = bucket.file; + bucket.file = function(name) { + var file = originalFileMethod(name); - bucket.combine([file1, file2], 'destination.txt'); - }); + if (name === '2.txt') { + return file; + } + + assert.strictEqual(name, destinationFileName); - it('should accept string or file input for destination', function(done) { - var destinations = [ - 'destination.txt', - bucket.file('destination.txt') - ]; + file.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/compose'); + assert.strictEqual(reqOpts.json.sourceObjects[0].name, file1.name); + assert.strictEqual(reqOpts.json.sourceObjects[1].name, file2); - async.each(destinations, function(destination, next) { - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { - assert(reqOpts.uri.indexOf(bucket.name + '/o/destination.txt') > -1); - next(); + done(); }; - bucket.combine(['1', '2'], destination); - }, done); + return file; + }; + + bucket.combine([file1, file2], destinationFileName); }); it('should use content type from the destination metadata', function(done) { - var destination = 'destination.txt'; + var destination = bucket.file('destination.txt'); - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { - assert.equal( + destination.request = function(reqOpts) { + assert.strictEqual( reqOpts.json.destination.contentType, - mime.contentType(destination) + mime.contentType(destination.name) ); + done(); }; @@ -201,11 +269,12 @@ describe('Bucket', function() { var destination = bucket.file('destination.txt'); destination.metadata = { contentType: 'content-type' }; - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { - assert.equal( + destination.request = function(reqOpts) { + assert.strictEqual( reqOpts.json.destination.contentType, destination.metadata.contentType ); + done(); }; @@ -213,13 +282,14 @@ describe('Bucket', function() { }); it('should detect dest content type if not in metadata', function(done) { - var destination = 'destination.txt'; + var destination = bucket.file('destination.txt'); - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { - assert.equal( + destination.request = function(reqOpts) { + assert.strictEqual( reqOpts.json.destination.contentType, - mime.contentType(destination) + mime.contentType(destination.name) ); + done(); }; @@ -227,30 +297,22 @@ describe('Bucket', function() { }); it('should throw if content type cannot be determined', function() { - var error = - 'A content type could not be detected for the destination file.'; - assert.throws(function() { bucket.combine(['1', '2'], 'destination'); - }, new RegExp(error)); + }, /A content type could not be detected/); }); it('should make correct API request', function(done) { var sources = [bucket.file('1.txt'), bucket.file('2.txt')]; var destination = bucket.file('destination.txt'); - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { - var expectedUri = format('{base}/{bucket}/o/{file}/compose', { - base: 'https://www.googleapis.com/storage/v1/b', - bucket: destination.bucket.name, - file: encodeURIComponent(destination.name) - }); - - assert.equal(reqOpts.uri, expectedUri); + destination.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/compose'); assert.deepEqual(reqOpts.json, { destination: { contentType: mime.contentType(destination.name) }, sourceObjects: [{ name: sources[0].name }, { name: sources[1].name }] }); + done(); }; @@ -259,10 +321,10 @@ describe('Bucket', function() { it('should encode the destination file name', function(done) { var sources = [bucket.file('1.txt'), bucket.file('2.txt')]; - var destination = 'needs encoding.jpg'; + var destination = bucket.file('needs encoding.jpg'); - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { - assert.equal(reqOpts.uri.indexOf(destination), -1); + destination.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri.indexOf(destination), -1); done(); }; @@ -276,7 +338,7 @@ describe('Bucket', function() { var destination = bucket.file('destination.txt'); - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts) { + destination.request = function(reqOpts) { assert.deepEqual(reqOpts.json.sourceObjects, [ { name: sources[0].name, generation: sources[0].metadata.generation }, { name: sources[1].name, generation: sources[1].metadata.generation } @@ -290,9 +352,9 @@ describe('Bucket', function() { it('should execute the callback', function(done) { var sources = [bucket.file('1.txt'), bucket.file('2.txt')]; - var destination = 'destination.txt'; + var destination = bucket.file('destination.txt'); - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts, callback) { + destination.request = function(reqOpts, callback) { callback(); }; @@ -301,62 +363,31 @@ describe('Bucket', function() { it('should execute the callback with an error', function(done) { var sources = [bucket.file('1.txt'), bucket.file('2.txt')]; - var destination = 'destination.txt'; + var destination = bucket.file('destination.txt'); var error = new Error('Error.'); - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts, callback) { + destination.request = function(reqOpts, callback) { callback(error); }; bucket.combine(sources, destination, function(err) { - assert.equal(err, error); + assert.strictEqual(err, error); done(); }); }); it('should execute the callback with apiResponse', function(done) { var sources = [bucket.file('1.txt'), bucket.file('2.txt')]; - var destination = 'destination.txt'; + var destination = bucket.file('destination.txt'); var resp = { success: true }; - bucket.storage.makeAuthenticatedRequest_ = function(reqOpts, callback) { + destination.request = function(reqOpts, callback) { callback(null, resp); }; bucket.combine(sources, destination, function(err, obj, apiResponse) { - assert.equal(resp, apiResponse); - done(); - }); - }); - }); - - describe('delete', function() { - it('should delete the bucket', function(done) { - bucket.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'DELETE'); - assert.equal(path, ''); - assert.strictEqual(query, null); - assert.strictEqual(body, true); - done(); - }; - bucket.delete(); - }); - - it('should execute callback', function(done) { - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(); - }; - bucket.delete(done); - }); - - it('should execute callback with apiResponse', function(done) { - var resp = { success: true }; - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(null, resp); - }; - bucket.delete(function(err, apiResponse) { - assert.deepEqual(resp, apiResponse); + assert.strictEqual(resp, apiResponse); done(); }); }); @@ -473,6 +504,12 @@ describe('Bucket', function() { file = bucket.file(FILE_NAME, options); }); + it('should throw if no name is provided', function() { + assert.throws(function() { + bucket.file(); + }, /A file name must be specified/); + }); + it('should return a File object', function() { assert(file instanceof FakeFile); }); @@ -492,20 +529,19 @@ describe('Bucket', function() { describe('getFiles', function() { it('should get files without a query', function(done) { - bucket.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'GET'); - assert.equal(path, '/o'); - assert.deepEqual(query, {}); - assert.strictEqual(body, true); + bucket.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/o'); + assert.deepEqual(reqOpts.qs, {}); done(); }; + bucket.getFiles(util.noop); }); it('should get files with a query', function(done) { var token = 'next-page-token'; - bucket.makeReq_ = function(method, path, query) { - assert.deepEqual(query, { maxResults: 5, pageToken: token }); + bucket.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, { maxResults: 5, pageToken: token }); done(); }; bucket.getFiles({ maxResults: 5, pageToken: token }, util.noop); @@ -513,7 +549,7 @@ describe('Bucket', function() { it('should return nextQuery if more results exist', function() { var token = 'next-page-token'; - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(null, { nextPageToken: token, items: [] }); }; bucket.getFiles({ maxResults: 5 }, function(err, results, nextQuery) { @@ -523,7 +559,7 @@ describe('Bucket', function() { }); it('should return null nextQuery if there are no more results', function() { - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(null, { items: [] }); }; bucket.getFiles({ maxResults: 5 }, function(err, results, nextQuery) { @@ -532,7 +568,7 @@ describe('Bucket', function() { }); it('should return File objects', function(done) { - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(null, { items: [{ name: 'fake-file-name', generation: 1 }] }); @@ -546,7 +582,7 @@ describe('Bucket', function() { }); it('should return versioned Files if queried for versions', function(done) { - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(null, { items: [{ name: 'fake-file-name', generation: 1 }] }); @@ -562,7 +598,7 @@ describe('Bucket', function() { it('should return apiResponse in callback', function(done) { var resp = { items: [{ name: 'fake-file-name' }] }; - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(null, resp); }; bucket.getFiles(function(err, files, nextQuery, apiResponse) { @@ -575,7 +611,7 @@ describe('Bucket', function() { var error = new Error('Error.'); var apiResponse = {}; - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(error, apiResponse); }; @@ -597,7 +633,7 @@ describe('Bucket', function() { my: 'custom metadata' } }; - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(null, { items: [fileMetadata] }); }; bucket.getFiles(function(err, files) { @@ -608,72 +644,58 @@ describe('Bucket', function() { }); }); - describe('getMetadata', function() { - var metadata = { a: 'b', c: 'd' }; + describe('makePrivate', function() { + it('should set predefinedAcl & privatize files', function(done) { + var didSetPredefinedAcl = false; + var didMakeFilesPrivate = false; - it('should get the metadata of a bucket', function(done) { - bucket.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'GET'); - assert.equal(path, ''); - assert.strictEqual(query, null); - assert.strictEqual(body, true); - done(); - }; - bucket.getMetadata(); - }); + bucket.request = function(reqOpts, callback) { + // Correct request. + assert.equal(reqOpts.method, 'PATCH'); + assert.equal(reqOpts.uri, ''); + assert.deepEqual(reqOpts.qs, { predefinedAcl: 'projectPrivate' }); + assert.deepEqual(reqOpts.json, { acl: null }); - it('should execute callback', function(done) { - bucket.makeReq_ = function(method, path, query, body, callback) { + didSetPredefinedAcl = true; callback(); }; - bucket.getMetadata(done); - }); - it('should update metadata property on object', function() { - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(null, metadata); + bucket.makeAllFilesPublicPrivate_ = function(opts, callback) { + assert.strictEqual(opts.private, true); + assert.strictEqual(opts.force, true); + didMakeFilesPrivate = true; + callback(); }; - assert.deepEqual(bucket.metadata, {}); - bucket.getMetadata(function(err, newMetadata) { - assert.deepEqual(newMetadata, metadata); - }); - assert.deepEqual(bucket.metadata, metadata); - }); - it('should pass metadata to callback', function(done) { - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(null, metadata); - }; - bucket.getMetadata(function(err, fileMetadata) { - assert.deepEqual(fileMetadata, metadata); + bucket.makePrivate({ includeFiles: true, force: true }, function(err) { + assert.ifError(err); + assert(didSetPredefinedAcl); + assert(didMakeFilesPrivate); done(); }); }); - it('should pass apiResponse to callback', function(done) { - var resp = metadata; - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(null, resp); + it('should not make files private by default', function(done) { + bucket.request = function(reqOpts, callback) { + callback(); }; - bucket.getMetadata(function(err, fileMetadata, apiResponse) { - assert.deepEqual(resp, apiResponse); - done(); - }); + + bucket.makeAllFilesPublicPrivate_ = function() { + throw new Error('Please, no. I do not want to be called.'); + }; + + bucket.makePrivate(done); }); - it('should execute callback with error & API response', function(done) { + it('should execute callback with error', function(done) { var error = new Error('Error.'); - var apiResponse = {}; - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(error, apiResponse); + bucket.request = function(reqOpts, callback) { + callback(error); }; - bucket.getMetadata(function(err, metadata, apiResponse_) { - assert.strictEqual(err, error); - assert.strictEqual(metadata, null); - assert.strictEqual(apiResponse_, apiResponse); - + bucket.makePrivate(function(err) { + assert.equal(err, error); done(); }); }); @@ -681,7 +703,7 @@ describe('Bucket', function() { describe('makePublic', function() { beforeEach(function() { - bucket.makeReq_ = function(method, path, query, body, callback) { + bucket.request = function(reqOpts, callback) { callback(); }; }); @@ -754,120 +776,6 @@ describe('Bucket', function() { }); }); - describe('makePrivate', function() { - it('should set predefinedAcl & privatize files', function(done) { - var didSetPredefinedAcl = false; - var didMakeFilesPrivate = false; - - bucket.makeReq_ = function(method, path, query, body, callback) { - // Correct request. - assert.equal(method, 'PATCH'); - assert.equal(path, ''); - assert.deepEqual(query, { predefinedAcl: 'projectPrivate' }); - assert.deepEqual(body, { acl: null }); - - didSetPredefinedAcl = true; - callback(); - }; - - bucket.makeAllFilesPublicPrivate_ = function(opts, callback) { - assert.strictEqual(opts.private, true); - assert.strictEqual(opts.force, true); - didMakeFilesPrivate = true; - callback(); - }; - - bucket.makePrivate({ includeFiles: true, force: true }, function(err) { - assert.ifError(err); - assert(didSetPredefinedAcl); - assert(didMakeFilesPrivate); - done(); - }); - }); - - it('should not make files private by default', function(done) { - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(); - }; - - bucket.makeAllFilesPublicPrivate_ = function() { - throw new Error('Please, no. I do not want to be called.'); - }; - - bucket.makePrivate(done); - }); - - it('should execute callback with error', function(done) { - var error = new Error('Error.'); - - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(error); - }; - - bucket.makePrivate(function(err) { - assert.equal(err, error); - done(); - }); - }); - }); - - describe('setMetadata', function() { - var metadata = { fake: 'metadata' }; - - it('should set metadata', function(done) { - bucket.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'PATCH'); - assert.equal(path, ''); - assert.deepEqual(body, metadata); - done(); - }; - bucket.setMetadata(metadata); - }); - - it('should execute callback', function(done) { - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(); - }; - bucket.setMetadata(metadata, done); - }); - - it('should execute callback with apiResponse', function(done) { - var resp = { success: true }; - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(null, resp); - }; - bucket.setMetadata(metadata, function(err, apiResponse) { - assert.deepEqual(resp, apiResponse); - done(); - }); - }); - - it('should execute callback with error & API response', function(done) { - var error = new Error('Error.'); - var apiResponse = {}; - - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(error, apiResponse); - }; - - bucket.setMetadata(metadata, function(err, apiResponse_) { - assert.strictEqual(err, error); - assert.strictEqual(apiResponse_, apiResponse); - - done(); - }); - }); - - it('should update internal metadata property', function() { - bucket.makeReq_ = function(method, path, query, body, callback) { - callback(null, metadata); - }; - bucket.setMetadata(metadata, function() { - assert.deepEqual(bucket.metadata, metadata); - }); - }); - }); - describe('upload', function() { var basename = 'proto_query.json'; var filepath = 'test/testdata/' + basename; @@ -1199,30 +1107,4 @@ describe('Bucket', function() { }); }); }); - - describe('makeReq_', function() { - var method = 'POST'; - var path = '/path'; - var query = { a: 'b', c: { d: 'e' } }; - var body = { hi: 'there' }; - - it('should make correct request', function(done) { - bucket.storage.makeAuthenticatedRequest_ = function(request) { - var basePath = 'https://www.googleapis.com/storage/v1/b'; - assert.equal(request.method, method); - assert.equal(request.uri, basePath + '/' + bucket.name + path); - assert.deepEqual(request.qs, query); - assert.deepEqual(request.json, body); - done(); - }; - bucket.makeReq_(method, path, query, body, util.noop); - }); - - it('should execute callback', function(done) { - bucket.storage.makeAuthenticatedRequest_ = function(request, callback) { - callback(); - }; - bucket.makeReq_(method, path, query, body, done); - }); - }); }); diff --git a/test/storage/file.js b/test/storage/file.js index 0f0d7b0e126..2765e5fde19 100644 --- a/test/storage/file.js +++ b/test/storage/file.js @@ -17,7 +17,6 @@ 'use strict'; var assert = require('assert'); -var Bucket = require('../../lib/storage/bucket.js'); var duplexify = require('duplexify'); var extend = require('extend'); var format = require('string-format-obj'); @@ -29,7 +28,10 @@ var stream = require('stream'); var through = require('through2'); var tmp = require('tmp'); var url = require('url'); -var util = require('../../lib/common/util'); + +var Bucket = require('../../lib/storage/bucket.js'); +var ServiceObject = require('../../lib/common/service-object.js'); +var util = require('../../lib/common/util.js'); var makeWritableStreamOverride; var handleRespOverride; @@ -72,21 +74,34 @@ fakeResumableUpload.createURI = function() { return createURI.apply(null, arguments); }; +function FakeServiceObject() { + this.calledWith_ = arguments; + ServiceObject.apply(this, arguments); +} + +nodeutil.inherits(FakeServiceObject, ServiceObject); + describe('File', function() { var File; - var FILE_NAME = 'file-name.png'; var file; + + var FILE_NAME = 'file-name.png'; var directoryFile; - var bucket; + + var STORAGE; + var BUCKET; before(function() { - mockery.registerMock('request', fakeRequest); mockery.registerMock('gcs-resumable-upload', fakeResumableUpload); + mockery.registerMock('request', fakeRequest); + mockery.registerMock('../common/service-object.js', FakeServiceObject); mockery.registerMock('../common/util.js', fakeUtil); + mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); + File = require('../../lib/storage/file.js'); }); @@ -96,8 +111,10 @@ describe('File', function() { }); beforeEach(function() { - var options = { - makeAuthenticatedRequest_: function(req, callback) { + STORAGE = { + createBucket: util.noop, + request: util.noop, + makeAuthenticatedRequest: function(req, callback) { if (callback) { (callback.onAuthenticated || callback)(null, req); } else { @@ -105,13 +122,14 @@ describe('File', function() { } } }; - bucket = new Bucket(options, 'bucket-name'); - file = new File(bucket, FILE_NAME); - file.makeReq_ = util.noop; + BUCKET = new Bucket(STORAGE, 'bucket-name'); - directoryFile = new File(bucket, 'directory/file.jpg'); - directoryFile.makeReq_ = util.noop; + file = new File(BUCKET, FILE_NAME); + file.request = util.noop; + + directoryFile = new File(BUCKET, 'directory/file.jpg'); + directoryFile.request = util.noop; handleRespOverride = null; makeWritableStreamOverride = null; @@ -120,20 +138,36 @@ describe('File', function() { }); describe('initialization', function() { - it('should throw if no name is provided', function() { - assert.throws(function() { - new File(bucket); - }, /A file name must be specified/); - }); - it('should assign file name', function() { assert.equal(file.name, FILE_NAME); }); + it('should assign the bucket instance', function() { + assert.strictEqual(file.bucket, BUCKET); + }); + + it('should assign the storage instance', function() { + assert.strictEqual(file.storage, BUCKET.storage); + }); + it('should accept specifying a generation', function() { - var file = new File(bucket, 'name', { generation: 2 }); + var file = new File(BUCKET, 'name', { generation: 2 }); assert.equal(file.generation, 2); }); + + it('should inherit from ServiceObject', function() { + assert(file instanceof ServiceObject); + + var calledWith = file.calledWith_[0]; + + assert.strictEqual(calledWith.parent, BUCKET); + assert.strictEqual(calledWith.baseUrl, '/o'); + assert.strictEqual(calledWith.id, encodeURIComponent(FILE_NAME)); + assert.deepEqual(calledWith.methods, { + exists: true, + get: true + }); + }); }); describe('copy', function() { @@ -144,17 +178,15 @@ describe('File', function() { }); it('should URI encode file names', function(done) { - var newFile = new File(bucket, 'nested/file.jpg'); + var newFile = new File(BUCKET, 'nested/file.jpg'); - var expectedPath = - format('/o/{srcName}/copyTo/b/{destBucket}/o/{destName}', { - srcName: encodeURIComponent(directoryFile.name), - destBucket: file.bucket.name, - destName: encodeURIComponent(newFile.name) - }); + var expectedPath = format('/copyTo/b/{destBucket}/o/{destName}', { + destBucket: file.bucket.name, + destName: encodeURIComponent(newFile.name) + }); - directoryFile.makeReq_ = function(method, path) { - assert.equal(path, expectedPath); + directoryFile.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, expectedPath); done(); }; @@ -165,9 +197,9 @@ describe('File', function() { var error = new Error('Error.'); var apiResponse = {}; - var newFile = new File(bucket, 'new-file'); + var newFile = new File(BUCKET, 'new-file'); - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(error, apiResponse); }; @@ -181,11 +213,11 @@ describe('File', function() { }); it('should send query.sourceGeneration if File has one', function(done) { - var versionedFile = new File(bucket, 'name', { generation: 1 }); - var newFile = new File(bucket, 'new-file'); + var versionedFile = new File(BUCKET, 'name', { generation: 1 }); + var newFile = new File(BUCKET, 'new-file'); - versionedFile.makeReq_ = function(method, path, query) { - assert.equal(query.sourceGeneration, 1); + versionedFile.request = function(reqOpts) { + assert.strictEqual(reqOpts.qs.sourceGeneration, 1); done(); }; @@ -194,45 +226,37 @@ describe('File', function() { describe('destination types', function() { function assertPathEquals(file, expectedPath, callback) { - file.makeReq_ = function(method, path) { - assert.equal(path, expectedPath); + file.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, expectedPath); callback(); }; } it('should allow a string', function(done) { var newFileName = 'new-file-name.png'; - var expectedPath = - format('/o/{srcName}/copyTo/b/{destBucket}/o/{destName}', { - srcName: file.name, - destBucket: file.bucket.name, - destName: newFileName - }); + var expectedPath = format('/copyTo/b/{destBucket}/o/{destName}', { + destBucket: file.bucket.name, + destName: newFileName + }); assertPathEquals(file, expectedPath, done); file.copy(newFileName); }); it('should allow a Bucket', function(done) { - var newBucket = new Bucket({}, 'new-bucket'); - var expectedPath = - format('/o/{srcName}/copyTo/b/{destBucket}/o/{destName}', { - srcName: file.name, - destBucket: newBucket.name, - destName: file.name - }); + var expectedPath = format('/copyTo/b/{destBucket}/o/{destName}', { + destBucket: BUCKET.name, + destName: file.name + }); assertPathEquals(file, expectedPath, done); - file.copy(newBucket); + file.copy(BUCKET); }); it('should allow a File', function(done) { - var newBucket = new Bucket({}, 'new-bucket'); - var newFile = new File(newBucket, 'new-file'); - var expectedPath = - format('/o/{srcName}/copyTo/b/{destBucket}/o/{destName}', { - srcName: file.name, - destBucket: newBucket.name, - destName: newFile.name - }); + var newFile = new File(BUCKET, 'new-file'); + var expectedPath = format('/copyTo/b/{destBucket}/o/{destName}', { + destBucket: BUCKET.name, + destName: newFile.name + }); assertPathEquals(file, expectedPath, done); file.copy(newFile); }); @@ -247,14 +271,13 @@ describe('File', function() { describe('returned File object', function() { beforeEach(function() { var resp = { success: true }; - file.makeReq_ = function(method, path, qs, body, callback) { + file.request = function(reqOpts, callback) { callback(null, resp); }; }); it('should re-use file object if one is provided', function(done) { - var newBucket = new Bucket({}, 'new-bucket'); - var newFile = new File(newBucket, 'new-file'); + var newFile = new File(BUCKET, 'new-file'); file.copy(newFile, function(err, copiedFile) { assert.ifError(err); assert.deepEqual(copiedFile, newFile); @@ -266,25 +289,24 @@ describe('File', function() { var newFilename = 'new-filename'; file.copy(newFilename, function(err, copiedFile) { assert.ifError(err); - assert.equal(copiedFile.bucket.name, bucket.name); + assert.equal(copiedFile.bucket.name, BUCKET.name); assert.equal(copiedFile.name, newFilename); done(); }); }); it('should create new file on the destination bucket', function(done) { - var newBucket = new Bucket({}, 'new-bucket'); - file.copy(newBucket, function(err, copiedFile) { + file.copy(BUCKET, function(err, copiedFile) { assert.ifError(err); - assert.equal(copiedFile.bucket.name, newBucket.name); + assert.equal(copiedFile.bucket.name, BUCKET.name); assert.equal(copiedFile.name, file.name); done(); }); }); it('should pass apiResponse into callback', function(done) { - var newBucket = new Bucket({}, 'new-bucket'); - file.copy(newBucket, function(err, copiedFile, apiResponse) { + file.copy(BUCKET, function(err, copiedFile, apiResponse) { + assert.ifError(err); assert.deepEqual({ success: true }, apiResponse); done(); }); @@ -292,94 +314,6 @@ describe('File', function() { }); }); - describe('move', function() { - it('should throw if no destination is provided', function() { - assert.throws(function() { - file.move(); - }, /should have a name/); - }); - - describe('copy to destination', function() { - function assertCopyFile(file, expectedDestination, callback) { - file.copy = function(destination) { - assert.equal(destination, expectedDestination); - callback(); - }; - } - - it('should call copy with string', function(done) { - var newFileName = 'new-file-name.png'; - assertCopyFile(file, newFileName, done); - file.move(newFileName); - }); - - it('should call copy with Bucket', function(done) { - var newBucket = new Bucket({}, 'new-bucket'); - assertCopyFile(file, newBucket, done); - file.move(newBucket); - }); - - it('should call copy with File', function(done) { - var newBucket = new Bucket({}, 'new-bucket'); - var newFile = new File(newBucket, 'new-file'); - assertCopyFile(file, newFile, done); - file.move(newFile); - }); - - it('should fail if copy fails', function(done) { - var error = new Error('Error.'); - file.copy = function(destination, callback) { - callback(error); - }; - file.move('new-filename', function(err) { - assert.equal(err, error); - done(); - }); - }); - }); - - describe('delete original file', function() { - it('should delete if copy is successful', function(done) { - file.copy = function(destination, callback) { - callback(null); - }; - file.delete = function() { - assert.equal(this, file); - done(); - }; - file.move('new-filename'); - }); - - it('should not delete if copy fails', function(done) { - var deleteCalled = false; - file.copy = function(destination, callback) { - callback(new Error('Error.')); - }; - file.delete = function() { - deleteCalled = true; - }; - file.move('new-filename', function() { - assert.equal(deleteCalled, false); - done(); - }); - }); - - it('should fail if delete fails', function(done) { - var error = new Error('Error.'); - file.copy = function(destination, callback) { - callback(); - }; - file.delete = function(callback) { - callback(error); - }; - file.move('new-filename', function(err) { - assert.equal(err, error); - done(); - }); - }); - }); - }); - describe('createReadStream', function() { function getFakeRequest(data) { var aborted = false; @@ -499,13 +433,11 @@ describe('File', function() { }); it('should send query.generation if File has one', function(done) { - var versionedFile = new File(bucket, 'file.txt', { generation: 1 }); + var versionedFile = new File(BUCKET, 'file.txt', { generation: 1 }); - versionedFile.bucket.storage.makeAuthenticatedRequest_ = function(rOpts) { + versionedFile.bucket.storage.makeAuthenticatedRequest = function(rOpts) { assert.equal(rOpts.qs.generation, 1); - setImmediate(function() { - done(); - }); + setImmediate(done); return duplexify(); }; @@ -531,7 +463,7 @@ describe('File', function() { it('should confirm the abort method exists', function(done) { var reqStream = through(); - file.bucket.storage.makeAuthenticatedRequest_ = function() { + file.bucket.storage.makeAuthenticatedRequest = function() { return reqStream; }; @@ -554,7 +486,7 @@ describe('File', function() { o: encodeURIComponent(file.name) }); - file.bucket.storage.makeAuthenticatedRequest_ = function(opts) { + file.bucket.storage.makeAuthenticatedRequest = function(opts) { assert.equal(opts.uri, expectedPath); setImmediate(function() { done(); @@ -566,7 +498,7 @@ describe('File', function() { }); it('should accept gzip encoding', function(done) { - file.bucket.storage.makeAuthenticatedRequest_ = function(opts) { + file.bucket.storage.makeAuthenticatedRequest = function(opts) { assert.strictEqual(opts.gzip, true); setImmediate(function() { done(); @@ -581,7 +513,7 @@ describe('File', function() { var ERROR = new Error('Error.'); beforeEach(function() { - file.bucket.storage.makeAuthenticatedRequest_ = function(opts) { + file.bucket.storage.makeAuthenticatedRequest = function(opts) { var stream = (requestOverride || request)(opts); setImmediate(function() { @@ -609,7 +541,7 @@ describe('File', function() { requestOverride = getFakeRequest(); - file.bucket.storage.makeAuthenticatedRequest_ = function() { + file.bucket.storage.makeAuthenticatedRequest = function() { setImmediate(function() { assert.deepEqual(requestOverride.getRequestOptions(), fakeRequest); done(); @@ -634,7 +566,7 @@ describe('File', function() { it('should unpipe stream from an error on the response', function(done) { var requestStream = through(); - file.bucket.storage.makeAuthenticatedRequest_ = function() { + file.bucket.storage.makeAuthenticatedRequest = function() { setImmediate(function() { // Must be a stream. Doesn't matter for the tests, though. requestStream.emit('response', through()); @@ -670,7 +602,7 @@ describe('File', function() { done(); }; - file.bucket.storage.makeAuthenticatedRequest_ = function() { + file.bucket.storage.makeAuthenticatedRequest = function() { var stream = through(); setImmediate(function() { stream.emit('complete', response); @@ -714,7 +646,7 @@ describe('File', function() { beforeEach(function() { file.metadata.mediaLink = 'http://uri'; - file.bucket.storage.makeAuthenticatedRequest_ = function(opts, cb) { + file.bucket.storage.makeAuthenticatedRequest = function(opts, cb) { if (cb) { (cb.onAuthenticated || cb)(null, {}); } else { @@ -914,7 +846,7 @@ describe('File', function() { createURI: function(opts, callback) { var bucket = file.bucket; var storage = bucket.storage; - var authClient = storage.makeAuthenticatedRequest_.authClient; + var authClient = storage.makeAuthenticatedRequest.authClient; assert.strictEqual(opts.authClient, authClient); assert.strictEqual(opts.bucket, bucket.name); @@ -1218,13 +1150,14 @@ describe('File', function() { describe('delete', function() { it('should delete the file', function(done) { - file.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'DELETE'); - assert.equal(path, '/o/' + FILE_NAME); - assert.deepEqual(query, {}); - assert.strictEqual(body, null); + file.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'DELETE'); + assert.equal(reqOpts.uri, ''); + assert.deepEqual(reqOpts.qs, {}); + done(); }; + file.delete(); }); @@ -1232,7 +1165,7 @@ describe('File', function() { var error = new Error('Error.'); var apiResponse = {}; - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(error, apiResponse); }; @@ -1244,20 +1177,12 @@ describe('File', function() { }); }); - it('should URI encode file names', function(done) { - directoryFile.makeReq_ = function(method, path) { - assert.equal(path, '/o/' + encodeURIComponent(directoryFile.name)); - done(); - }; - - directoryFile.delete(); - }); - it('should send query.generation if File has one', function(done) { - var versionedFile = new File(bucket, 'new-file.txt', { generation: 1 }); + var versionedFile = new File(BUCKET, 'new-file.txt', { generation: 1 }); + + versionedFile.request = function(reqOpts) { + assert.strictEqual(reqOpts.qs.generation, 1); - versionedFile.makeReq_ = function(method, path, query) { - assert.equal(query.generation, 1); done(); }; @@ -1265,17 +1190,20 @@ describe('File', function() { }); it('should execute callback', function(done) { - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(); }; + file.delete(done); }); it('should execute callback with apiResponse', function(done) { var resp = { success: true }; - file.makeReq_ = function(method, path, query, body, callback) { + + file.request = function(reqOpts, callback) { callback(null, resp); }; + file.delete(function(err, apiResponse) { assert.deepEqual(resp, apiResponse); done(); @@ -1419,30 +1347,21 @@ describe('File', function() { var metadata = { a: 'b', c: 'd' }; it('should get the metadata of a file', function(done) { - file.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'GET'); - assert.equal(path, '/o/' + FILE_NAME); - assert.deepEqual(query, {}); - assert.strictEqual(body, null); - done(); - }; - file.getMetadata(); - }); + file.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, ''); + assert.deepEqual(reqOpts.qs, {}); - it('should URI encode file names', function(done) { - directoryFile.makeReq_ = function(method, path) { - assert.equal(path, '/o/' + encodeURIComponent(directoryFile.name)); done(); }; - directoryFile.getMetadata(); + file.getMetadata(); }); it('should send query.generation if File has one', function(done) { - var versionedFile = new File(bucket, 'new-file.txt', { generation: 1 }); + var versionedFile = new File(BUCKET, 'new-file.txt', { generation: 1 }); - versionedFile.makeReq_ = function(method, path, query) { - assert.equal(query.generation, 1); + versionedFile.request = function(reqOpts) { + assert.strictEqual(reqOpts.qs.generation, 1); done(); }; @@ -1450,15 +1369,16 @@ describe('File', function() { }); it('should execute callback', function(done) { - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(); }; + file.getMetadata(done); }); it('should execute callback with apiResponse', function(done) { var resp = { success: true }; - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(null, resp); }; file.getMetadata(function(err, metadata, apiResponse) { @@ -1468,7 +1388,7 @@ describe('File', function() { }); it('should update metadata property on object', function() { - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(null, metadata); }; assert.deepEqual(file.metadata, {}); @@ -1479,7 +1399,7 @@ describe('File', function() { }); it('should pass metadata to callback', function(done) { - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(null, metadata); }; file.getMetadata(function(err, fileMetadata) { @@ -1493,8 +1413,8 @@ describe('File', function() { var credentials = require('../testdata/privateKeyFile.json'); beforeEach(function() { - var storage = bucket.storage; - storage.makeAuthenticatedRequest_.getCredentials = function(callback) { + var storage = BUCKET.storage; + storage.getCredentials = function(callback) { callback(null, credentials); }; }); @@ -1514,8 +1434,8 @@ describe('File', function() { it('should return an error if getCredentials errors', function(done) { var error = new Error('Error.'); - var storage = bucket.storage; - storage.makeAuthenticatedRequest_.getCredentials = function(callback) { + var storage = BUCKET.storage; + storage.getCredentials = function(callback) { callback(error); }; @@ -1529,8 +1449,8 @@ describe('File', function() { }); it('should return an error if credentials are not present', function(done) { - var storage = bucket.storage; - storage.makeAuthenticatedRequest_.getCredentials = function(callback) { + var storage = BUCKET.storage; + storage.getCredentials = function(callback) { callback(null, {}); }; @@ -1783,8 +1703,8 @@ describe('File', function() { var credentials = require('../testdata/privateKeyFile.json'); beforeEach(function() { - var storage = bucket.storage; - storage.makeAuthenticatedRequest_.getCredentials = function(callback) { + var storage = BUCKET.storage; + storage.getCredentials = function(callback) { callback(null, credentials); }; }); @@ -1803,8 +1723,8 @@ describe('File', function() { it('should return an error if getCredentials errors', function(done) { var error = new Error('Error.'); - var storage = bucket.storage; - storage.makeAuthenticatedRequest_.getCredentials = function(callback) { + var storage = BUCKET.storage; + storage.getCredentials = function(callback) { callback(error); }; @@ -1819,8 +1739,8 @@ describe('File', function() { }); it('should return an error if credentials are not present', function(done) { - var storage = bucket.storage; - storage.makeAuthenticatedRequest_.getCredentials = function(callback) { + var storage = BUCKET.storage; + storage.getCredentials = function(callback) { callback(null, {}); }; @@ -1963,79 +1883,57 @@ describe('File', function() { }); }); - describe('setMetadata', function() { - var metadata = { fake: 'metadata' }; - - it('should set metadata', function(done) { - file.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'PATCH'); - assert.equal(path, '/o/' + file.name); - assert.deepEqual(body, metadata); - done(); - }; - file.setMetadata(metadata); - }); + describe('makePrivate', function() { + it('should execute callback with API response', function(done) { + var apiResponse = {}; - it('should URI encode file names', function(done) { - directoryFile.makeReq_ = function(method, path) { - assert.equal(path, '/o/' + encodeURIComponent(directoryFile.name)); - done(); + file.request = function(reqOpts, callback) { + callback(null, apiResponse); }; - directoryFile.setMetadata(metadata); - }); - - it('should send query.generation if File has one', function(done) { - var versionedFile = new File(bucket, 'new-file.txt', { generation: 1 }); + file.makePrivate(function(err, apiResponse_) { + assert.ifError(err); + assert.strictEqual(apiResponse_, apiResponse); - versionedFile.makeReq_ = function(method, path, query) { - assert.equal(query.generation, 1); done(); - }; - - versionedFile.setMetadata(); - }); - - it('should execute callback', function(done) { - file.makeReq_ = function(method, path, query, body, callback) { - callback(); - }; - file.setMetadata(metadata, done); + }); }); it('should execute callback with error & API response', function(done) { var error = new Error('Error.'); var apiResponse = {}; - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(error, apiResponse); }; - file.setMetadata(metadata, function(err, apiResponse_) { + file.makePrivate(function(err, apiResponse_) { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); + done(); }); }); - it('should execute callback with apiResponse', function(done) { - var resp = { success: true }; - file.makeReq_ = function(method, path, query, body, callback) { - callback(null, resp); - }; - file.setMetadata(metadata, function(err, apiResponse) { - assert.deepEqual(resp, apiResponse); + it('should make the file private to project by default', function(done) { + file.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'PATCH'); + assert.strictEqual(reqOpts.uri, ''); + assert.deepEqual(reqOpts.qs, { predefinedAcl: 'projectPrivate' }); + assert.deepEqual(reqOpts.json, { acl: null }); done(); - }); + }; + + file.makePrivate(util.noop); }); - it('should update internal metadata property', function() { - file.makeReq_ = function(method, path, query, body, callback) { - callback(null, metadata); + it('should make the file private to user if strict = true', function(done) { + file.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, { predefinedAcl: 'private' }); + done(); }; - file.setMetadata(metadata, function() { - assert.deepEqual(file.metadata, metadata); - }); + + file.makePrivate({ strict: true }, util.noop); }); }); @@ -2058,60 +1956,156 @@ describe('File', function() { }); }); - describe('makePrivate', function() { - it('should execute callback with API response', function(done) { - var apiResponse = {}; + describe('move', function() { + it('should throw if no destination is provided', function() { + assert.throws(function() { + file.move(); + }, /should have a name/); + }); - file.makeReq_ = function(method, path, query, body, callback) { - callback(null, apiResponse); + describe('copy to destination', function() { + function assertCopyFile(file, expectedDestination, callback) { + file.copy = function(destination) { + assert.strictEqual(destination, expectedDestination); + callback(); + }; + } + + it('should call copy with string', function(done) { + var newFileName = 'new-file-name.png'; + assertCopyFile(file, newFileName, done); + file.move(newFileName); + }); + + it('should call copy with Bucket', function(done) { + assertCopyFile(file, BUCKET, done); + file.move(BUCKET); + }); + + it('should call copy with File', function(done) { + var newFile = new File(BUCKET, 'new-file'); + assertCopyFile(file, newFile, done); + file.move(newFile); + }); + + it('should fail if copy fails', function(done) { + var error = new Error('Error.'); + file.copy = function(destination, callback) { + callback(error); + }; + file.move('new-filename', function(err) { + assert.equal(err, error); + done(); + }); + }); + }); + + describe('delete original file', function() { + it('should delete if copy is successful', function(done) { + file.copy = function(destination, callback) { + callback(null); + }; + file.delete = function() { + assert.equal(this, file); + done(); + }; + file.move('new-filename'); + }); + + it('should not delete if copy fails', function(done) { + var deleteCalled = false; + file.copy = function(destination, callback) { + callback(new Error('Error.')); + }; + file.delete = function() { + deleteCalled = true; + }; + file.move('new-filename', function() { + assert.equal(deleteCalled, false); + done(); + }); + }); + + it('should fail if delete fails', function(done) { + var error = new Error('Error.'); + file.copy = function(destination, callback) { + callback(); + }; + file.delete = function(callback) { + callback(error); + }; + file.move('new-filename', function(err) { + assert.equal(err, error); + done(); + }); + }); + }); + }); + + describe('setMetadata', function() { + var metadata = { fake: 'metadata' }; + + it('should set metadata', function(done) { + file.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'PATCH'); + assert.strictEqual(reqOpts.uri, ''); + assert.deepEqual(reqOpts.json, metadata); + done(); }; + file.setMetadata(metadata); + }); - file.makePrivate(function(err, apiResponse_) { - assert.ifError(err); - assert.strictEqual(apiResponse_, apiResponse); + it('should send query.generation if File has one', function(done) { + var versionedFile = new File(BUCKET, 'new-file.txt', { generation: 1 }); + versionedFile.request = function(reqOpts) { + assert.strictEqual(reqOpts.qs.generation, 1); done(); - }); + }; + + versionedFile.setMetadata(); + }); + + it('should execute callback', function(done) { + file.request = function(reqOpts, callback) { + callback(); + }; + file.setMetadata(metadata, done); }); it('should execute callback with error & API response', function(done) { var error = new Error('Error.'); var apiResponse = {}; - file.makeReq_ = function(method, path, query, body, callback) { + file.request = function(reqOpts, callback) { callback(error, apiResponse); }; - file.makePrivate(function(err, apiResponse_) { + file.setMetadata(metadata, function(err, apiResponse_) { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); - done(); }); }); - it('should make the file private to project by default', function(done) { - file.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'PATCH'); - assert.equal(path, '/o/' + encodeURIComponent(file.name)); - assert.deepEqual(query, { predefinedAcl: 'projectPrivate' }); - assert.deepEqual(body, { acl: null }); - done(); + it('should execute callback with apiResponse', function(done) { + var resp = { success: true }; + file.request = function(reqOpts, callback) { + callback(null, resp); }; - - file.makePrivate(util.noop); + file.setMetadata(metadata, function(err, apiResponse) { + assert.deepEqual(resp, apiResponse); + done(); + }); }); - it('should make the file private to user if strict = true', function(done) { - file.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'PATCH'); - assert.equal(path, '/o/' + encodeURIComponent(file.name)); - assert.deepEqual(query, { predefinedAcl: 'private' }); - assert.deepEqual(body, { acl: null }); - done(); + it('should update internal metadata property', function() { + file.request = function(reqOpts, callback) { + callback(null, metadata); }; - - file.makePrivate({ strict: true }, util.noop); + file.setMetadata(metadata, function() { + assert.deepEqual(file.metadata, metadata); + }); }); }); @@ -2127,7 +2121,7 @@ describe('File', function() { resumableUploadOverride = function(opts) { var bucket = file.bucket; var storage = bucket.storage; - var authClient = storage.makeAuthenticatedRequest_.authClient; + var authClient = storage.makeAuthenticatedRequest.authClient; assert.strictEqual(opts.authClient, authClient); assert.strictEqual(opts.bucket, bucket.name); @@ -2224,7 +2218,7 @@ describe('File', function() { }); it('should send query.ifGenerationMatch if File has one', function(done) { - var versionedFile = new File(bucket, 'new-file.txt', { generation: 1 }); + var versionedFile = new File(BUCKET, 'new-file.txt', { generation: 1 }); makeWritableStreamOverride = function(stream, options) { assert.equal(options.request.qs.ifGenerationMatch, 1); diff --git a/test/storage/index.js b/test/storage/index.js index 315472ad0e2..53aa5665649 100644 --- a/test/storage/index.js +++ b/test/storage/index.js @@ -20,7 +20,9 @@ var arrify = require('arrify'); var assert = require('assert'); var extend = require('extend'); var mockery = require('mockery'); +var nodeutil = require('util'); +var Service = require('../../lib/common/service.js'); var util = require('../../lib/common/util.js'); var fakeUtil = extend({}, util); @@ -39,6 +41,13 @@ var fakeStreamRouter = { } }; +function FakeService() { + this.calledWith_ = arguments; + Service.apply(this, arguments); +} + +nodeutil.inherits(FakeService, Service); + describe('Storage', function() { var PROJECT_ID = 'project-id'; var Storage; @@ -46,8 +55,10 @@ describe('Storage', function() { var Bucket; before(function() { + mockery.registerMock('../common/service.js', FakeService); mockery.registerMock('../common/util.js', fakeUtil); mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + mockery.enable({ useCleanCache: true, warnOnUnregistered: false @@ -90,12 +101,27 @@ describe('Storage', function() { fakeUtil.normalizeArguments = normalizeArguments; }); - it('should set the project id', function() { - assert.equal(storage.projectId, 'project-id'); + it('should inherit from Service', function() { + assert(storage instanceof Service); + + var calledWith = storage.calledWith_[0]; + + var baseUrl = 'https://www.googleapis.com/storage/v1'; + assert.strictEqual(calledWith.baseUrl, baseUrl); + assert.strictEqual(calledWith.projectIdRequired, false); + assert.deepEqual(calledWith.scopes, [ + 'https://www.googleapis.com/auth/devstorage.full_control' + ]); }); }); describe('bucket', function() { + it('should throw if no name was provided', function() { + assert.throws(function() { + storage.bucket(); + }, /A bucket name is needed/); + }); + it('should accept a string for a name', function() { var newBucketName = 'new-bucket-name'; var bucket = storage.bucket(newBucketName); @@ -110,11 +136,12 @@ describe('Storage', function() { var BUCKET = { name: BUCKET_NAME }; it('should make correct API request', function(done) { - storage.makeReq_ = function(method, path, query, body, callback) { - assert.equal(method, 'POST'); - assert.equal(path, ''); - assert.equal(query.project, storage.projectId); - assert.equal(body.name, BUCKET_NAME); + storage.request = function(reqOpts, callback) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/b'); + assert.strictEqual(reqOpts.qs.project, storage.projectId); + assert.strictEqual(reqOpts.json.name, BUCKET_NAME); + callback(); }; @@ -122,8 +149,8 @@ describe('Storage', function() { }); it('should accept a name, metadata, and callback', function(done) { - storage.makeReq_ = function(method, path, query, body, callback) { - assert.deepEqual(body, extend(METADATA, { name: BUCKET_NAME })); + storage.request = function(reqOpts, callback) { + assert.deepEqual(reqOpts.json, extend(METADATA, { name: BUCKET_NAME })); callback(null, METADATA); }; storage.bucket = function(name) { @@ -137,7 +164,7 @@ describe('Storage', function() { }); it('should accept a name and callback only', function(done) { - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(); }; storage.createBucket(BUCKET_NAME, done); @@ -153,7 +180,7 @@ describe('Storage', function() { storage.bucket = function() { return BUCKET; }; - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(null, METADATA); }; storage.createBucket(BUCKET_NAME, function(err, bucket) { @@ -166,7 +193,7 @@ describe('Storage', function() { it('should execute callback on error', function(done) { var error = new Error('Error.'); - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(error); }; storage.createBucket(BUCKET_NAME, function(err) { @@ -177,7 +204,7 @@ describe('Storage', function() { it('should execute callback with apiResponse', function(done) { var resp = { success: true }; - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(null, resp); }; storage.createBucket(BUCKET_NAME, function(err, bucket, apiResponse) { @@ -187,30 +214,28 @@ describe('Storage', function() { }); it('should expand the Nearline option', function(done) { - storage.makeReq_ = function(method, path, query, body) { - assert.strictEqual(body.storageClass, 'NEARLINE'); + storage.request = function(reqOpts) { + assert.strictEqual(reqOpts.json.storageClass, 'NEARLINE'); done(); }; storage.createBucket(BUCKET_NAME, { nearline: true }, function() {}); }); it('should expand the Durable Reduced Availability option', function(done) { - storage.makeReq_ = function(method, path, query, body) { + storage.request = function(reqOpts) { + var body = reqOpts.json; assert.strictEqual(body.storageClass, 'DURABLE_REDUCED_AVAILABILITY'); done(); }; storage.createBucket(BUCKET_NAME, { dra: true }, function() {}); }); - }); describe('getBuckets', function() { it('should get buckets without a query', function(done) { - storage.makeReq_ = function(method, path, query, body) { - assert.equal(method, 'GET'); - assert.equal(path, ''); - assert.deepEqual(query, { project: storage.projectId }); - assert.strictEqual(body, null); + storage.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/b'); + assert.deepEqual(reqOpts.qs, { project: storage.projectId }); done(); }; storage.getBuckets(util.noop); @@ -218,8 +243,8 @@ describe('Storage', function() { it('should get buckets with a query', function(done) { var token = 'next-page-token'; - storage.makeReq_ = function(method, path, query) { - assert.deepEqual(query, { + storage.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, { project: storage.projectId, maxResults: 5, pageToken: token @@ -231,7 +256,7 @@ describe('Storage', function() { it('should return nextQuery if more results exist', function() { var token = 'next-page-token'; - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(null, { nextPageToken: token, items: [] }); }; storage.getBuckets({ maxResults: 5 }, function(err, results, nextQuery) { @@ -241,7 +266,7 @@ describe('Storage', function() { }); it('should return null nextQuery if there are no more results', function() { - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(null, { items: [] }); }; storage.getBuckets({ maxResults: 5 }, function(err, results, nextQuery) { @@ -250,7 +275,7 @@ describe('Storage', function() { }); it('should return Bucket objects', function(done) { - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(null, { items: [{ id: 'fake-bucket-name' }] }); }; storage.getBuckets(function(err, buckets) { @@ -262,7 +287,7 @@ describe('Storage', function() { it('should return apiResponse', function(done) { var resp = { items: [{ id: 'fake-bucket-name' }] }; - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(null, resp); }; storage.getBuckets(function(err, buckets, nextQuery, apiResponse) { @@ -279,7 +304,7 @@ describe('Storage', function() { my: 'custom metadata' } }; - storage.makeReq_ = function(method, path, query, body, callback) { + storage.request = function(reqOpts, callback) { callback(null, { items: [bucketMetadata] }); }; storage.getBuckets(function(err, buckets) {