Saturday, April 27, 2013
NodeJS - simulating a multi-file upload request
Story:
I was building a NodeJS client application that connects to a backend Rest API. One of the functionalities requires uploading multiple files to the server.
Concept:
We will need to upload the files via a POST request to the server. The request will look like the following:
POST /cgi-bin/qtest HTTP/1.1
Host: aram
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://aram/~martind/banner.htm
Content-Type: multipart/form-data; boundary=---------------------------287032381131322
Content-Length: 582
-----------------------------287032381131322
Content-Disposition: form-data; name="datafile1"; filename="r.gif"
Content-Type: image/gif
GIF87a.............,...........D..;
-----------------------------287032381131322
Content-Disposition: form-data; name="datafile2"; filename="g.gif"
Content-Type: image/gif
GIF87a.............,...........D..;
-----------------------------287032381131322
Content-Disposition: form-data; name="datafile3"; filename="b.gif"
Content-Type: image/gif
GIF87a.............,...........D..;
-----------------------------287032381131322--
I took this above example from What should a Multipart HTTP request with multiple files look like?
The number 287032381131322 is a random number.
-----------------------------287032381131322 is a boundary that separates individual data fields.
Notice that there's always a "\r\n" appending the -----------------------------287032381131322.
The last -----------------------------287032381131322 will contain an extra "--" at the end.
Implementation:
We will create a function to generate the above request. The files that will be uploaded are called thumbnail, bigThumbnail, and photo.
I will be using the NodeJS library "Sequence" to make the code to follow a sequential order. Don't worry about it if you don't understand it.
Here's the code:
FileProvider.prototype.save = function(req, callback) {
var files = req.files;
var title = req.body.title;
if(files.length == 0) {
console.log("nothing uploaded");
return callback(500, null);
}
var thumbnail = files.thumbnail;
var bigThumbnail = files.bigThumbnail;
var photo = files.photo;
var boundary = Math.random();
var post_data = [];
post_data.push(new Buffer(httpRequest.encodeFieldPart(boundary, 'title', title), 'ascii'));
var sequence = Futures.sequence();
sequence.then(function(next) {
post_data.push(new Buffer(httpRequest.encodeFilePart(boundary, thumbnail.type, 'thumbnail', thumbnail.name), 'ascii'));
var file_reader = fs.createReadStream(thumbnail.path, {encoding: 'binary'});
var file_contents = '';
file_reader.on('data', function(data){
file_contents += data;
});
file_reader.on('end', function(){
post_data.push(new Buffer(file_contents, 'binary'));
post_data.push(new Buffer("--" + boundary + "\r\n"), 'ascii');
next();
});
})
.then(function(next) {
post_data.push(new Buffer(httpRequest.encodeFilePart(boundary, photo.type, 'digitalImage', photo.name), 'ascii'));
var file_reader = fs.createReadStream(photo.path, {encoding: 'binary'});
var file_contents = '';
file_reader.on('data', function(data){
file_contents += data;
});
file_reader.on('end', function(){
post_data.push(new Buffer(file_contents, 'binary'));
post_data.push(new Buffer("--" + boundary + "--\r\n"), 'ascii');
next();
});
})
.then(function(next) {
var dataLength = 0;
for(var i = 0; i < post_data.length; i++) {
dataLength += post_data[i].length;
}
var options = {
host: constants.SERVER_HOST,
port: constants.SERVER_PORT,
path: constants.SERVER_UPLOAD_PATH,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': dataLength,
'Cookie': req.session.user.authCookie
}
};
httpRequest.post(req, options, post_data, function(res, data) {
var response = null;
if(res.statusCode == 200) {
try {
response = JSON.parse(data);
} catch (err) {
console.log("FileProvider.save error: " + err);
}
callback(res.statusCode, response);
} else {
callback(res.statusCode, null);
}
});
});
};
Helper Library:
HttpRequest.prototype.post = function(request, options, postData, callback) {
// disable socket pooling
options.agent = false;
var req = http.request(options, function(res) {
console.log('RESPONSE STATUS: ' + res.statusCode);
console.log('RESPONSE HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
var responseData = '';
res.on('data', function (data) {
responseData += data;
});
res.on('end', function () {
callback(res, responseData);
});
});
req.setTimeout(100000, function() {
console.log('problem with request: timeout');
callback(408, null);
});
req.on('error', function(e) {
console.log('problem with request: ' + e.message);
callback(599, null);
});
if(postData instanceof Array) {
for (var i = 0; i < postData.length; i++) {
req.write(postData[i]);
}
} else {
req.write(postData);
}
req.end();
};
HttpRequest.prototype.encodeFieldPart = function(boundary,name,value) {
var return_part = "--" + boundary + "\r\n";
return_part += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n";
return_part += value + "\r\n";
return return_part;
}
HttpRequest.prototype.encodeFilePart = function(boundary,type,name,filename) {
var return_part = "--" + boundary + "\r\n";
return_part += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n";
return_part += "Content-Type: " + type + "\r\n\r\n";
return return_part;
}
Labels:
nodejs
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment