Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bucket#makePublic and bucket#makePrivate #418

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions lib/storage/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var extend = require('extend');
var fs = require('fs');
var mime = require('mime-types');
var path = require('path');
var async = require('async');

/**
* @type {module:storage/acl}
Expand Down Expand Up @@ -286,6 +287,7 @@ Bucket.prototype.getFiles = function(query, callback) {
return file;
});
var nextQuery = null;

if (resp.nextPageToken) {
nextQuery = extend({}, query, { pageToken: resp.nextPageToken });
}
Expand Down Expand Up @@ -348,6 +350,142 @@ Bucket.prototype.setMetadata = function(metadata, callback) {
});
};

/**
* Make the bucket listing and optionally its contents publicly readable to
* anybody.
* NOTE: This function may be long-running and use a high number of requests as
* it recursively applies makePublic to every file within the bucket.
*
* @param {object=} options - The configuration object.
* @param {boolean} options.recursive - Whether to recursively apply makePublic
* to every file in the bucket as well. Default is false.
* @param {boolean} options.force - By default, makePublic will return at the
* first error found, but set this to true to continue processing if this
* occurs.
* @param {Function} callback - The callback function.
*
* @example
* bucket.makePublic({ recursive: true }, function(err, files) {
* // err is the first error to occur, otherwise null.
* // files is an array of files successfully made public in the bucket.
* });
*/
Bucket.prototype.makePublic = function(options, callback) {
if (util.is(options, 'function')) {
callback = options;
options = {};
}

options.public = true;
this._makePublicPrivate(options, callback);
};

Bucket.prototype.makePrivate = function(options, callback) {
if (util.is(options, 'function')) {
callback = options;
options = {};
}

options.private = true;
this._makePublicPrivate(options, callback);
};

Bucket.prototype._makePublicPrivate = function(options, callback) {
var self = this;
var errors = [];
var filesProcessed = [];
var nextQuery = {};

// Allow public to read bucket contents
// while preserving original permissions
if (options.public) {
this.acl.add({
entity: 'allUsers',
role: 'READER'
}, processAllFiles);
} else if (options.private) {
var query = {
predefinedAcl: options.strict ? 'private' : 'projectPrivate'
};

// You aren't allowed to set both predefinedAcl & acl properties on a bucket
// so acl must explicitly be nullified.
var metadata = { acl: null };

this.makeReq_('PATCH', '', query, metadata, function(err, resp) {
if (err) {
processAllFiles(err);
return;
}
self.metadata = resp;
processAllFiles(null);
});
}

function processAllFiles(err) {
if (err) {
callback(err);
return;
}

if (options.recursive) {
async.whilst(function() {

This comment was marked as spam.

// while there are still more files to make public
return !!nextQuery;
}, function(cb) {
processMoreFiles(nextQuery, cb);
}, function(err) {
if (err || errors.length) {
callback(err || errors, filesProcessed);
} else {
callback(null, filesProcessed);
}
});
} else {
callback(null, filesProcessed);

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

}
}

function processMoreFiles(query, callback) {
self.getFiles(query, function(err, files, nextPageQuery) {
if (err) {
callback(err);
return;
}

// Iterate through each file and make it public.
async.eachLimit(files, 10, function(file, cb) {

This comment was marked as spam.

if (options.public) {
file.makePublic(processedCallback);
} else if (options.private) {
file.makePrivate({ strict: options.strict }, processedCallback);
}

function processedCallback(err) {

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

if (err) {
if (!options.force) {
cb(err);
} else {
errors.push(err);
cb();

This comment was marked as spam.

}
} else {
filesProcessed.push(file);
cb();
}
}
}, function(err) {
if (err) {
callback(err);
} else {
nextQuery = nextPageQuery;
callback();
}
});
});
}
};

/**
* Upload a file to the bucket. This is a convenience method that wraps the
* functionality provided by a File object, {module:storage/file}.
Expand Down
67 changes: 67 additions & 0 deletions lib/storage/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,73 @@ File.prototype.setMetadata = function(metadata, callback) {
});
};

/**
* 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.
*
* @param {object=} options - The configuration object.
* @param {boolean=} options.strict - If true, set the file to be private to
* only the owner user. Otherwise, it will be private to the project.
* @param {function=} callback - The callback function.
*
* @example
*
* //-
* // Set the file private so only project maintainers can see and modify it.
* //-
* file.makePrivate(function(err) {});
*
* //-
* // Set the file private so only the owner can see and modify it.
* //-
* file.makePrivate({ strict: true }, function(err) {});
*/
File.prototype.makePrivate = function(options, callback) {
var that = this;
if (util.is(options, 'function')) {
callback = options;
options = {};
}
var path = '/o/' + encodeURIComponent(this.name);
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 };

callback = callback || util.noop;

this.makeReq_('PATCH', path, query, metadata, function(err, resp) {
if (err) {
callback(err);
return;
}

that.metadata = resp;

callback(null);
});
};

/**
* Set a file to be publicly readable and maintain all previous permissions.
*
* @param {function=} callback - The callback function.
*
* @example
* file.makePublic(function(err) {});
*/
File.prototype.makePublic = function(callback) {
callback = callback || util.noop;

this.acl.add({
entity: 'allUsers',
role: 'READER'
}, function(err) {
callback(err);
});
};

/**
* `startResumableUpload_` uses the Resumable Upload API: http://goo.gl/jb0e9D.
*
Expand Down
26 changes: 26 additions & 0 deletions regression/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,32 @@ describe('storage', function() {
});
});
});

it('should be made public', function(done) {
file.makePublic(function(err) {
assert.ifError(err);
file.acl.get({ entity: 'allUsers' }, function(err, aclObject) {
assert.ifError(err);
assert.deepEqual(aclObject, { entity: 'allUsers', role: 'READER' });
file.acl.delete({ entity: 'allUsers' }, done);
});
});
});

it('should be made private', function(done) {
file.makePublic(function(err) {
assert.ifError(err);
file.makePrivate(function(err) {
assert.ifError(err);
file.acl.get({ entity: 'allUsers' }, function(err, aclObject) {
assert.equal(err.code, 404);
assert.equal(err.message, 'Not Found');
assert.equal(aclObject, null);
done();
});
});
});
});
});
});

Expand Down
53 changes: 53 additions & 0 deletions test/storage/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,59 @@ describe('File', function() {
});
});

describe('makePublic', function() {
it('should execute callback', function(done) {
file.acl.add = function(options, callback) {
callback();
};

file.makePublic(done);
});

it('should make the file public', function(done) {
file.acl.add = function(options) {
assert.deepEqual(options, { entity: 'allUsers', role: 'READER' });
done();
};

file.makePublic(util.noop);
});
});

describe('makePrivate', function() {
it('should execute callback', function(done) {
file.makeReq_ = function(method, path, query, body, callback) {
callback();
};

file.makePrivate(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();
};

file.makePrivate(util.noop);
});

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();
};

file.makePrivate({ strict: true }, util.noop);
});
});

describe('startResumableUpload_', function() {
var RESUMABLE_URI = 'http://resume';

Expand Down