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

allow making a bucket public/private #429

Merged
Merged
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
292 changes: 292 additions & 0 deletions lib/storage/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

'use strict';

var async = require('async');
var extend = require('extend');
var fs = require('fs');
var mime = require('mime-types');
Expand Down Expand Up @@ -409,6 +410,214 @@ Bucket.prototype.getMetadata = function(callback) {
}.bind(this));
};

/**
* Make the bucket listing private.
*
* You may also choose to make the contents of the bucket private by specifying
* `includeFiles: true`. This will automatically run
* {module:storage/file#makePrivate} for every file in the bucket.
*
* When specifying `includeFiles: true`, use `force: true` to delay execution of
* your callback until all files have been processed. By default, the callback
* is executed after the first error. Use `force` to queue such errors until all
* files have been procssed, after which they will be returned as an array as
* the first argument to your callback.
*
* NOTE: This may cause the process to be long-running and use a high number of
* requests. Use with caution.
*
* @param {object=} options - The configuration object.
* @param {boolean} options.includeFiles - Make each file in the bucket private.
* Default: `false`.
* @param {boolean} options.force - Queue errors occurred while making files
* private until all files have been processed.
* @param {function} callback - The callback function.
*
* @example
* //-
* // Make the bucket private.
* //-
* bucket.makePrivate(function(err) {});
*
* //-
* // Make the bucket and its contents private.
* //-
* var opts = {
* includeFiles: true
* };
*
* bucket.makePrivate(opts, function(err, files) {
* // `err`:
* // The first error to occur, otherwise null.
* //
* // `files`:
* // Array of files successfully made private in the bucket.
* });
*
* //-
* // Make the bucket and its contents private, using force to suppress errors
* // until all files have been processed.
* //-
* var opts = {
* includeFiles: true,
* force: true
* };
*
* bucket.makePrivate(opts, function(errors, files) {
* // `errors`:
* // Array of errors if any occurred, otherwise null.
* //
* // `files`:
* // Array of files successfully made private in the bucket.
* });
*/
Bucket.prototype.makePrivate = function(options, callback) {
var self = this;

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

options = options || {};
options.private = true;

async.parallel([setPredefinedAcl, makeFilesPrivate], callback);

function setPredefinedAcl(done) {
var query = {
predefinedAcl: 'projectPrivate'
};

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

self.makeReq_('PATCH', '', query, metadata, function(err, resp) {
if (err) {
done(err);
return;
}

self.metadata = resp;

done();
});
}

function makeFilesPrivate(done) {
if (!options.includeFiles) {
done();
return;
}

self.makeAllFilesPublicPrivate_(options, done);
}
};

/**
* Make the bucket publicly readable.
*
* You may also choose to make the contents of the bucket publicly readable by
* specifying `includeFiles: true`. This will automatically run
* {module:storage/file#makePublic} for every file in the bucket.
*
* When specifying `includeFiles: true`, use `force: true` to delay execution of
* your callback until all files have been processed. By default, the callback
* is executed after the first error. Use `force` to queue such errors until all
* files have been procssed, after which they will be returned as an array as
* the first argument to your callback.
*
* NOTE: This may cause the process to be long-running and use a high number of
* requests. Use with caution.
*
* @param {object=} options - The configuration object.
* @param {boolean} options.includeFiles - Make each file in the bucket publicly
* readable. Default: `false`.
* @param {boolean} options.force - Queue errors occurred while making files
* public until all files have been processed.
* @param {function} callback - The callback function.
*
* @example
* //-
* // Make the bucket publicly readable.
* //-
* bucket.makePublic(function(err) {});
*
* //-
* // Make the bucket and its contents publicly readable.
* //-
* var opts = {
* includeFiles: true
* };
*
* bucket.makePublic(opts, function(err, files) {
* // `err`:
* // The first error to occur, otherwise null.
* //
* // `files`:
* // Array of files successfully made public in the bucket.
* });
*
* //-
* // Make the bucket and its contents publicly readable, using force to
* // suppress errors until all files have been processed.
* //-
* var opts = {
* includeFiles: true,
* force: true
* };
*
* bucket.makePublic(opts, function(errors, files) {
* // `errors`:
* // Array of errors if any occurred, otherwise null.
* //
* // `files`:
* // Array of files successfully made public in the bucket.
* });
*/
Bucket.prototype.makePublic = function(options, callback) {
var self = this;

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

options = options || {};
options.public = true;

async.parallel([
addAclPermissions,
addDefaultAclPermissions,
makeFilesPublic
], callback);

function addAclPermissions(done) {
// Allow reading bucket contents while preserving original permissions.
self.acl.add({
entity: 'allUsers',
role: 'READER'
}, done);
}

function addDefaultAclPermissions(done) {
self.acl.default.add({

This comment was marked as spam.

entity: 'allUsers',
role: 'READER'
}, done);
}

function makeFilesPublic(done) {
if (!options.includeFiles) {
done();
return;
}

self.makeAllFilesPublicPrivate_(options, done);
}
};

/**
* Set the bucket's metadata.
*
Expand Down Expand Up @@ -576,6 +785,89 @@ Bucket.prototype.upload = function(localPath, options, callback) {
}
};

/**
* Iterate over all of a bucket's files, calling `file.makePublic()` (public)
* or `file.makePrivate()` (private) on each.
*
* Operations are performed in parallel, up to 10 at once. The first error
* breaks the loop, and will execute the provided callback with it. Specify
* `{ force: true }` to suppress the errors.
*
* @private
*
* @param {object} options - Configuration object.
* @param {boolean} options.force - Supress errors until all files have been
* processed.
* @param {boolean} options.private - Make files private.
* @param {boolean} options.public - Make files public.
* @param {function} callback - The callback function.
*/
Bucket.prototype.makeAllFilesPublicPrivate_ = function(options, callback) {
var self = this;

var MAX_PARALLEL_LIMIT = 10;
var errors = [];
var updatedFiles = [];

// Start processing files, iteratively fetching more as necessary.
processFiles({}, function (err) {
if (err || errors.length > 0) {
callback(err || errors, updatedFiles);
return;
}

callback(null, updatedFiles);
});

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

// Iterate through each file and make it public or private.
async.eachLimit(files, MAX_PARALLEL_LIMIT, processFile, function(err) {
if (err) {
callback(err);
return;
}

if (nextQuery) {
processFiles(nextQuery, callback);
return;
}

callback();
});
});
}

function processFile(file, callback) {
if (options.public) {
file.makePublic(processedCallback);
} else if (options.private) {
file.makePrivate(processedCallback);
}

function processedCallback(err) {
if (err) {
if (options.force) {
errors.push(err);
callback();
return;
}

callback(err);
return;
}

updatedFiles.push(file);
callback();
}
}
};

/**
* Make a new request object from the provided arguments and wrap the callback
* to intercept non-successful responses.
Expand Down
Loading