Skip to content

Commit 2a81d2d

Browse files
storage: support file.compose - fixes #401
1 parent 6938342 commit 2a81d2d

File tree

5 files changed

+381
-4
lines changed

5 files changed

+381
-4
lines changed

lib/common/util.js

+46
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,52 @@ function getType(value) {
208208
return Object.prototype.toString.call(value).match(/\s(\w+)\]/)[1];
209209
}
210210

211+
/**
212+
* Used in an Array iterator usually, this will return the value of a property
213+
* in an object by its name.
214+
*
215+
* @param {string} name - Name of the property to return.
216+
* @return {function}
217+
*
218+
* @example
219+
* var people = [
220+
* {
221+
* name: 'Stephen',
222+
* origin: 'USA',
223+
* beenToNYC: false
224+
* },
225+
* {
226+
* name: 'Ryan',
227+
* origin: 'Canada',
228+
* beenToNYC: true
229+
* }
230+
* ];
231+
*
232+
* var names = people.map(prop('name'));
233+
* // [
234+
* // 'Stephen',
235+
* // 'Ryan'
236+
* // ]
237+
*
238+
* var peopleInUSA = people.filter(prop('beenToNYC'));
239+
* // [
240+
* // {
241+
* // name: 'Ryan',
242+
* // origin: 'Canada',
243+
* // beenToNYC: true
244+
* // }
245+
* // ]
246+
*/
247+
function prop(name) {
248+
return function(item) {
249+
if (name in item) {
250+
return item[name];
251+
}
252+
};
253+
}
254+
255+
module.exports.prop = prop;
256+
211257
/**
212258
* Assign a value to a property in an Array iterator.
213259
*

lib/storage/bucket.js

+94
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,100 @@ function Bucket(storage, name) {
202202
/* jshint ignore:end */
203203
}
204204

205+
/**
206+
* Combine mutliple files into one new file.
207+
*
208+
* @throws if a non-array is provided as sources argument.
209+
* @throws if less than two sources are provided.
210+
* @throws if no destination is provided.
211+
* @throws if a content type cannot be determined for the destination file.
212+
*
213+
* @param {string[]|module:storage/file[]} sources - The source files that will
214+
* be combined.
215+
* @param {string|module:storage/file} destination - The file you would like the
216+
* source files combined into.
217+
* @param {function=} callback - The callback function.
218+
*
219+
* @example
220+
* var 2013logs = bucket.file('2013-logs.txt');
221+
* var 2014logs = bucket.file('2014-logs.txt');
222+
*
223+
* var allLogs = bucket.file('all-logs.txt');
224+
*
225+
* bucket.combine([
226+
* 2013logs,
227+
* 2014logs
228+
* ], allLogs, function(err, newFile) {
229+
* // newFile === allLogs
230+
* });
231+
*/
232+
Bucket.prototype.combine = function(sources, destination, callback) {
233+
if (!util.is(sources, 'array') || sources.length < 2) {
234+
throw new Error('You must provide at least two source files.');
235+
}
236+
237+
if (!destination) {
238+
throw new Error('A destination file must be specified.');
239+
}
240+
241+
var self = this;
242+
243+
sources = sources.map(convertToFile);
244+
destination = convertToFile(destination);
245+
callback = callback || util.noop;
246+
247+
if (!destination.metadata.contentType) {
248+
var destinationContentType = mime.contentType(destination.name);
249+
250+
if (destinationContentType) {
251+
destination.metadata.contentType = destinationContentType;
252+
} else {
253+
throw new Error(
254+
'A content type could not be detected for the destination file.');
255+
}
256+
}
257+
258+
this.storage.makeAuthorizedRequest_({
259+
method: 'POST',
260+
uri: util.format('{base}/{destBucket}/o/{destFile}/compose', {
261+
base: STORAGE_BASE_URL,
262+
destBucket: destination.bucket.name,
263+
destFile: encodeURIComponent(destination.name)
264+
}),
265+
json: {
266+
destination: {
267+
contentType: destination.metadata.contentType
268+
},
269+
sourceObjects: sources.map(function (source) {
270+
var sourceObject = {
271+
name: source.name
272+
};
273+
274+
if (source.metadata && source.metadata.generation) {
275+
sourceObject.generation = source.metadata.generation;
276+
}
277+
278+
return sourceObject;
279+
})
280+
}
281+
}, function(err) {
282+
if (err) {
283+
callback(err);
284+
return;
285+
}
286+
287+
callback(null, destination);
288+
});
289+
290+
function convertToFile(file) {
291+
if (file instanceof File) {
292+
return file;
293+
} else {
294+
return self.file(file);
295+
}
296+
}
297+
};
298+
205299
/**
206300
* Delete the bucket.
207301
*

regression/storage.js

+43-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ var fs = require('fs');
2525
var request = require('request');
2626
var through = require('through2');
2727
var tmp = require('tmp');
28+
var util = require('../lib/common/util');
2829
var uuid = require('node-uuid');
2930

31+
var prop = util.prop;
32+
3033
var env = require('./env.js');
3134
var storage = require('../lib/storage')(env);
3235

@@ -47,12 +50,14 @@ function deleteFiles(bucket, callback) {
4750
callback(err);
4851
return;
4952
}
50-
async.map(files, function(file, next) {
51-
file.delete(next);
52-
}, callback);
53+
async.map(files, deleteFile, callback);
5354
});
5455
}
5556

57+
function deleteFile(file, callback) {
58+
file.delete(callback);
59+
}
60+
5661
function generateBucketName() {
5762
return 'gcloud-test-bucket-temp-' + uuid.v1();
5863
}
@@ -493,6 +498,41 @@ describe('storage', function() {
493498
});
494499
});
495500

501+
describe('combine files', function() {
502+
it('should combine multiple files into one', function(done) {
503+
var files = [
504+
{ file: bucket.file('file-one.txt'), contents: '123' },
505+
{ file: bucket.file('file-two.txt'), contents: '456' }
506+
];
507+
508+
async.each(files, createFile, function(err) {
509+
assert.ifError(err);
510+
511+
var sourceFiles = files.map(prop('file'));
512+
var destinationFile = bucket.file('file-one-and-two.txt');
513+
514+
bucket.combine(sourceFiles, destinationFile, function(err) {
515+
assert.ifError(err);
516+
517+
destinationFile.download(function(err, contents) {
518+
assert.ifError(err);
519+
520+
assert.equal(contents, files.map(prop('contents')).join(''));
521+
522+
async.each(sourceFiles.concat([destinationFile]), deleteFile, done);
523+
});
524+
});
525+
});
526+
527+
function createFile(fileObject, cb) {
528+
var ws = fileObject.file.createWriteStream();
529+
ws.on('error', cb);
530+
ws.on('complete', cb.bind(null, null));
531+
ws.end(fileObject.contents);
532+
}
533+
});
534+
});
535+
496536
describe('list files', function() {
497537
var filenames = ['CloudLogo1', 'CloudLogo2', 'CloudLogo3'];
498538

test/common/util.js

+12
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,18 @@ describe('common/util', function() {
449449
});
450450
});
451451

452+
describe('prop', function() {
453+
it('should return objects that match the property name', function() {
454+
var people = [
455+
{ name: 'Stephen', origin: 'USA', beenToNYC: false },
456+
{ name: 'Ryan', origin: 'Canada', beenToNYC: true }
457+
];
458+
459+
assert.deepEqual(people.map(util.prop('name')), ['Stephen', 'Ryan']);
460+
assert.deepEqual(people.filter(util.prop('beenToNYC')), [people[1]]);
461+
});
462+
});
463+
452464
describe('propAssign', function() {
453465
it('should assign a property and value to an object', function() {
454466
var obj = {};

0 commit comments

Comments
 (0)