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;
}

No comments:

Post a Comment