Skip to content

Commit 85886f4

Browse files
storage: add deleteFiles()
1 parent ee56b17 commit 85886f4

3 files changed

Lines changed: 242 additions & 27 deletions

File tree

lib/storage/bucket.js

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,124 @@ Bucket.prototype.delete = function(callback) {
310310
this.makeReq_('DELETE', '', null, true, callback);
311311
};
312312

313+
/**
314+
* Iterate over the bucket's files, calling `file.delete()` on each.
315+
*
316+
* <strong>This is not an atomic request.</strong> A delete attempt will be made
317+
* for each file individually. Any one can fail, in which case only a portion of
318+
* the files you intended to be deleted would have.
319+
*
320+
* Operations are performed in parallel, up to 10 at once. The first error
321+
* breaks the loop and will execute the provided callback with it. Specify
322+
* `{ force: true }` to suppress the errors until all files have had a chance to
323+
* be processed.
324+
*
325+
* The `query` object passed as the first argument will also be passed to
326+
* {module:storage/bucket#getFiles}.
327+
*
328+
* @param {object=} query - Query object. See {module:storage/bucket#getFiles}
329+
* for all of the supported properties.
330+
* @param {boolean} query.force - Supress errors until all files have been
331+
* processed.
332+
* @param {function} callback - The callback function.
333+
* @param {?error|?error[]} callback.err - An API error or array of errors from
334+
* files that were not able to be deleted.
335+
*
336+
* @example
337+
* //-
338+
* // Delete all of the files in the bucket.
339+
* //-
340+
* bucket.deleteFiles(function(err) {});
341+
*
342+
* //-
343+
* // By default, if a file cannot be deleted, this method will stop deleting
344+
* // files from your bucket. You can override this setting with `force: true`.
345+
* //-
346+
* bucket.deleteFiles({
347+
* force: true
348+
* }, function(errors) {
349+
* // `errors`:
350+
* // Array of errors if any occurred, otherwise null.
351+
* });
352+
*
353+
* //-
354+
* // The first argument to this method acts as a query to
355+
* // {module:storage/bucket#getFiles}. As an example, you can delete files
356+
* // which match a prefix.
357+
* //-
358+
* bucket.deleteFiles({
359+
* prefix: 'images/'
360+
* }, function(err) {
361+
* if (!err) {
362+
* // All files in the `images` directory have been deleted.
363+
* }
364+
* });
365+
*/
366+
Bucket.prototype.deleteFiles = function(query, callback) {
367+
if (util.is(query, 'function')) {
368+
callback = query;
369+
query = {};
370+
}
371+
372+
query = query || {};
373+
374+
var self = this;
375+
376+
var MAX_PARALLEL_LIMIT = 10;
377+
var errors = [];
378+
379+
// Start deleting files, iteratively fetching more as necessary.
380+
deleteFiles(query, function(err) {
381+
if (err || errors.length > 0) {
382+
callback(err || errors);
383+
return;
384+
}
385+
386+
callback(null);
387+
});
388+
389+
function deleteFiles(query, callback) {
390+
self.getFiles(query, function(err, files, nextQuery) {
391+
if (err) {
392+
callback(err);
393+
return;
394+
}
395+
396+
// Iterate through each file and make it public or private.
397+
async.eachLimit(files, MAX_PARALLEL_LIMIT, deleteFile, function(err) {
398+
if (err) {
399+
callback(err);
400+
return;
401+
}
402+
403+
if (nextQuery) {
404+
deleteFiles(nextQuery, callback);
405+
return;
406+
}
407+
408+
callback();
409+
});
410+
});
411+
}
412+
413+
function deleteFile(file, callback) {
414+
file.delete(function(err) {
415+
if (err) {
416+
if (query.force) {
417+
errors.push(err);
418+
callback();
419+
return;
420+
}
421+
422+
callback(err);
423+
return;
424+
}
425+
426+
callback();
427+
});
428+
}
429+
};
430+
313431
/**
314432
* Create a File object. See {module:storage/file} to see how to handle
315433
* the different use cases you may have.
@@ -849,7 +967,7 @@ Bucket.prototype.makeAllFilesPublicPrivate_ = function(options, callback) {
849967
var updatedFiles = [];
850968

851969
// Start processing files, iteratively fetching more as necessary.
852-
processFiles({}, function (err) {
970+
processFiles({}, function(err) {
853971
if (err || errors.length > 0) {
854972
callback(err || errors, updatedFiles);
855973
return;

system-test/storage.js

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,6 @@ var files = {
4242
}
4343
};
4444

45-
function deleteVersionedFiles(bucket, callback) {
46-
bucket.getFiles({ versions: true }, function(err, files) {
47-
if (err) {
48-
callback(err);
49-
return;
50-
}
51-
52-
async.each(files, deleteFile, callback);
53-
});
54-
}
55-
56-
function deleteFiles(bucket, callback) {
57-
bucket.getFiles(function(err, files) {
58-
if (err) {
59-
callback(err);
60-
return;
61-
}
62-
async.map(files, deleteFile, callback);
63-
});
64-
}
65-
6645
function deleteFile(file, callback) {
6746
file.delete(callback);
6847
}
@@ -100,7 +79,7 @@ describe('storage', function() {
10079
});
10180

10281
after(function(done) {
103-
deleteFiles(bucket, function(err) {
82+
bucket.deleteFiles(function(err) {
10483
assert.ifError(err);
10584
bucket.delete(done);
10685
});
@@ -221,7 +200,7 @@ describe('storage', function() {
221200
bucket.acl.default.delete({ entity: 'allUsers' }, next);
222201
},
223202
function(next) {
224-
deleteFiles(bucket, next);
203+
bucket.deleteFiles(next);
225204
}
226205
], done);
227206
});
@@ -278,7 +257,7 @@ describe('storage', function() {
278257
async.each(files, isFilePrivate, function(err) {
279258
assert.ifError(err);
280259

281-
deleteFiles(bucket, done);
260+
bucket.deleteFiles(done);
282261
});
283262
});
284263
});
@@ -686,7 +665,7 @@ describe('storage', function() {
686665
var filenames = ['CloudLogo1', 'CloudLogo2', 'CloudLogo3'];
687666

688667
before(function(done) {
689-
deleteFiles(bucket, function(err) {
668+
bucket.deleteFiles(function(err) {
690669
assert.ifError(err);
691670

692671
var file = bucket.file(filenames[0]);
@@ -750,7 +729,7 @@ describe('storage', function() {
750729
});
751730

752731
afterEach(function(done) {
753-
deleteVersionedFiles(versionedBucket, done);
732+
versionedBucket.deleteFiles({ versions: true }, done);
754733
});
755734

756735
after(function(done) {

test/storage/bucket.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,124 @@ describe('Bucket', function() {
340340
});
341341
});
342342

343+
describe('deleteFiles', function() {
344+
it('should get files from the bucket', function(done) {
345+
var query = { a: 'b', c: 'd' };
346+
347+
bucket.getFiles = function(query_) {
348+
assert.deepEqual(query_, query);
349+
done();
350+
};
351+
352+
bucket.deleteFiles(query, assert.ifError);
353+
});
354+
355+
it('should process 10 files at a time', function(done) {
356+
eachLimit_Override = function(arr, limit) {
357+
assert.equal(limit, 10);
358+
done();
359+
};
360+
361+
bucket.getFiles = function(query, callback) {
362+
callback(null, []);
363+
};
364+
365+
bucket.deleteFiles({}, assert.ifError);
366+
});
367+
368+
it('should delete the files', function(done) {
369+
var timesCalled = 0;
370+
371+
var files = [
372+
bucket.file('1'),
373+
bucket.file('2')
374+
].map(util.propAssign('delete', function(callback) {
375+
timesCalled++;
376+
callback();
377+
}));
378+
379+
bucket.getFiles = function(query, callback) {
380+
callback(null, files);
381+
};
382+
383+
bucket.deleteFiles({}, function(err) {
384+
assert.ifError(err);
385+
assert.equal(timesCalled, files.length);
386+
done();
387+
});
388+
});
389+
390+
it('should get more files if more exist', function(done) {
391+
var fakeNextQuery = { a: 'b', c: 'd' };
392+
393+
bucket.getFiles = function(query, callback) {
394+
if (Object.keys(query).length === 0) {
395+
// First time through, return a `nextQuery` value.
396+
callback(null, [], fakeNextQuery);
397+
} else {
398+
// Second time through.
399+
assert.deepEqual(query, fakeNextQuery);
400+
done();
401+
}
402+
};
403+
404+
bucket.deleteFiles({}, assert.ifError);
405+
});
406+
407+
it('should execute callback with error from getting files', function(done) {
408+
var error = new Error('Error.');
409+
410+
bucket.getFiles = function(query, callback) {
411+
callback(error);
412+
};
413+
414+
bucket.deleteFiles({}, function(err) {
415+
assert.equal(err, error);
416+
done();
417+
});
418+
});
419+
420+
it('should execute callback with error from deleting file', function(done) {
421+
var error = new Error('Error.');
422+
423+
var files = [
424+
bucket.file('1'),
425+
bucket.file('2')
426+
].map(util.propAssign('delete', function(callback) {
427+
callback(error);
428+
}));
429+
430+
bucket.getFiles = function(query, callback) {
431+
callback(null, files);
432+
};
433+
434+
bucket.deleteFiles({}, function(err) {
435+
assert.equal(err, error);
436+
done();
437+
});
438+
});
439+
440+
it('should execute callback with queued errors', function(done) {
441+
var error = new Error('Error.');
442+
443+
var files = [
444+
bucket.file('1'),
445+
bucket.file('2')
446+
].map(util.propAssign('delete', function(callback) {
447+
callback(error);
448+
}));
449+
450+
bucket.getFiles = function(query, callback) {
451+
callback(null, files);
452+
};
453+
454+
bucket.deleteFiles({ force: true }, function(errs) {
455+
assert.deepEqual(errs, [error, error]);
456+
done();
457+
});
458+
});
459+
});
460+
343461
describe('file', function() {
344462
var FILE_NAME = 'remote-file-name.jpg';
345463
var file;

0 commit comments

Comments
 (0)