Skip to content

Commit

Permalink
Build multipart only when resource and media body provided
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanseys committed Jul 29, 2014
1 parent ac91269 commit 10303fd
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 42 deletions.
62 changes: 38 additions & 24 deletions lib/apirequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ var Multipart = require('multipart-stream');
var utils = require('./utils.js');
var DefaultTransporter = require('./transporters.js');
var transporter = new DefaultTransporter();
var stream = require('stream');

function isReadableStream(obj) {
return obj instanceof stream.Stream && typeof obj._read == 'function' &&
typeof obj._readableState == 'object';
}

function logErrorOnly(err) {
if (err) {
Expand Down Expand Up @@ -50,7 +55,7 @@ function isValidParams(params, keys, callback) {
* @return {Request} Returns Request object or null
*/
function createAPIRequest(parameters, callback) {
var req;
var req, body;
var mediaUrl = parameters.mediaUrl;
var context = parameters.context;
var params = parameters.params;
Expand Down Expand Up @@ -79,6 +84,7 @@ function createAPIRequest(parameters, callback) {
var media = params.media || {};
var resource = params.resource;
var authClient = params.auth || context._options.auth || context.google._options.auth;
var defaultMime = typeof media.body === 'string' ? 'text/plain' : 'application/octet-stream';
delete params.media;
delete params.resource;
delete params.auth;
Expand All @@ -96,37 +102,45 @@ function createAPIRequest(parameters, callback) {

if (mediaUrl && media && media.body) {
options.url = mediaUrl;
// Create a boundary identifier and multipart read stream
var boundary = Math.random().toString(36).slice(2);
var mp = new Multipart(boundary);
if (resource) {
// Create a boundary identifier and multipart read stream
var boundary = Math.random().toString(36).slice(2);
body = new Multipart(boundary);

// Always a multipart upload
params.uploadType = 'multipart';
// Use multipart upload
params.uploadType = 'multipart';

options.headers = {
'Content-Type': 'multipart/related; boundary="' + boundary + '"'
};
options.headers = {
'Content-Type': 'multipart/related; boundary="' + boundary + '"'
};

// Add parts to multipart request
if (resource) {
mp.addPart({
// Add parts to multipart request
body.addPart({
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(resource)
});
}

var defaultMime = typeof media.body === 'string' ? 'text/plain' : 'application/octet-stream';

mp.addPart({
headers: {
'Content-Type': media.mimeType || resource && resource.mimeType || defaultMime
},
body: media.body // can be a readable stream or raw string!
});
}
else {
body.addPart({
headers: {
'Content-Type': media.mimeType || resource && resource.mimeType || defaultMime
},
body: media.body // can be a readable stream or raw string!
});
} else {
params.uploadType = 'media';
options.headers = {
'Content-Type': media.mimeType || defaultMime
};

if (isReadableStream(media.body)) {
body = media.body;
} else {
options.body = media.body;
}
}
} else {
options.json = resource || ((method === 'GET' || method === 'DELETE') ? true : {});
}

Expand All @@ -141,7 +155,7 @@ function createAPIRequest(parameters, callback) {
req = transporter.request(options, callback);
}

if (mp) mp.pipe(req);
if (body) body.pipe(req);
return req;
}

Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/media-response.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Content-Type: application/json

$resource
--$boundary
Content-Type: text/plain
Content-Type: $mimeType

$media
--$boundary--
1 change: 1 addition & 0 deletions test/fixtures/mediabody.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello world abc123
92 changes: 75 additions & 17 deletions test/test.media.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,38 +33,39 @@ describe('Media', function() {
drive = google.drive('v2');
});

it('should post with uploadType=multipart', function(done) {
it('should post with uploadType=multipart if resource and media set', function(done) {
var scope = nock('https://www.googleapis.com')
.post('/upload/drive/v2/files?uploadType=multipart')
.reply(200, { fileId: 'abc123' });
var req = drive.files.insert({ resource: {}, media: { body: 'hello' }}, function(err, body) {
assert.equal(JSON.stringify(body), JSON.stringify({ fileId: 'abc123' }));
scope.done();
done();
});
});

it('should post with uploadType=media media set but not resource', function(done) {
var scope = nock('https://www.googleapis.com')
.post('/upload/drive/v2/files?uploadType=media')
.reply(200, { fileId: 'abc123' });
var req = drive.files.insert({ media: { body: 'hello' }}, function(err, body) {
assert.equal(JSON.stringify(body), JSON.stringify({ fileId: 'abc123' }));
scope.done();
done();
});
});

it('should generate a valid upload if media is set, metadata is not set', function(done) {
it('should generate a valid media upload if media is set, metadata is not set', function(done) {
var scope = nock('https://www.googleapis.com')
.post('/upload/drive/v2/files?uploadType=multipart')
.post('/upload/drive/v2/files?uploadType=media')
.reply(201, function(uri, reqBody) {
return reqBody; // return request body as response for testing purposes
});
var resource = { mimeType: 'text/plain' };
var media = { body: 'hey' };
var expectedResp = fs.readFileSync(__dirname + '/fixtures/media-response.txt', { encoding: 'utf8' });
var req = drive.files.insert({ resource: resource, media: media }, function(err, body) {
var req = drive.files.insert({ media: media }, function(err, body) {
assert.equal(req.method, 'POST');
assert.equal(req.uri.href, 'https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart');
assert.equal(req.headers['Content-Type'].indexOf('multipart/related;'), 0);
var boundary = req.src.boundary;
expectedResp = expectedResp
.replace(/\n/g, '\r\n')
.replace(/\$boundary/g, boundary)
.replace('$media', media.body)
.replace('$resource', JSON.stringify(resource))
.trim();
assert.strictEqual(expectedResp, body);
assert.equal(req.uri.href, 'https://www.googleapis.com/upload/drive/v2/files?uploadType=media');
assert.strictEqual(media.body, body);
scope.done();
done();
});
Expand All @@ -89,6 +90,7 @@ describe('Media', function() {
.replace(/\$boundary/g, boundary)
.replace('$media', media.body)
.replace('$resource', JSON.stringify(resource))
.replace('$mimeType', 'text/plain')
.trim();
assert.strictEqual(expectedResp, body);
scope.done();
Expand All @@ -98,7 +100,7 @@ describe('Media', function() {

it('should not require parameters for insertion requests', function() {
var req = drive.files.insert({ someAttr: 'someValue', media: { body: 'wat' } }, noop);
assert.equal(req.uri.query, 'someAttr=someValue&uploadType=multipart');
assert.equal(req.uri.query, 'someAttr=someValue&uploadType=media');
});

it('should not multipart upload if no media body given', function() {
Expand All @@ -125,6 +127,7 @@ describe('Media', function() {
.replace(/\$boundary/g, boundary)
.replace('$media', media.body)
.replace('$resource', JSON.stringify(resource))
.replace('$mimeType', 'text/plain')
.trim();
assert.strictEqual(expectedResp, body);
scope.done();
Expand All @@ -146,4 +149,59 @@ describe('Media', function() {
done();
});
});

it('should accept readable stream as media body without metadata', function(done) {
var scope = nock('https://www.googleapis.com')
.post('/upload/gmail/v1/users/me/drafts?uploadType=media')
.reply(201, function(uri, reqBody) {
return reqBody; // return request body as response for testing purposes
});

var gmail = google.gmail('v1');
var body = fs.createReadStream(__dirname + '/fixtures/mediabody.txt');
var expectedBody = fs.readFileSync(__dirname + '/fixtures/mediabody.txt');
var req = gmail.users.drafts.create({
userId: 'me',
media: {
mimeType: 'message/rfc822',
body: body
}
}, function(err, resp) {
assert.equal(resp, expectedBody);
scope.done();
done();
});
});

it('should accept readable stream as media body with metadata', function(done) {
var scope = nock('https://www.googleapis.com')
.post('/upload/gmail/v1/users/me/drafts?uploadType=multipart')
.reply(201, function(uri, reqBody) {
return reqBody; // return request body as response for testing purposes
});

var gmail = google.gmail('v1');
var resource = { message: { raw: (new Buffer('hello', 'binary')).toString('base64') } };
var body = fs.createReadStream(__dirname + '/fixtures/mediabody.txt');
var bodyString = fs.readFileSync(__dirname + '/fixtures/mediabody.txt', { encoding: 'utf8' });
var media = { mimeType: 'message/rfc822', body: body };
var expectedBody = fs.readFileSync(__dirname + '/fixtures/media-response.txt', { encoding: 'utf8' });
var req = gmail.users.drafts.create({
userId: 'me',
resource: resource,
media: media
}, function(err, resp) {
var boundary = req.src.boundary;
expectedBody = expectedBody
.replace(/\n/g, '\r\n')
.replace(/\$boundary/g, boundary)
.replace('$media', bodyString)
.replace('$resource', JSON.stringify(resource))
.replace('$mimeType', 'message/rfc822')
.trim();
assert.strictEqual(expectedBody, resp);
scope.done();
done();
});
});
});

0 comments on commit 10303fd

Please sign in to comment.