I was working on a little project which does not utilize express. Part of the program needed to handle file uploads. In PHP this is easy with the $_FILES superglobal, and with express this is also just as easy with req.files. In pure node.js the developer doesn’t have this luxury because they’re just dealing with raw http requests and responses (I’m surprised node.js even parses the headers, it is a miracle!). Before I begin, this whole process should be optimally streamed to a file so large file uploads do not consume more RAM than their response chunk size, but I didn’t do this, shame on me.
First, capture the response body. Here I am assuming you have a HTTP server that is listening and responding to requests:
1 2 3 4 5 6 7 8 9 10 11 12 |
function requestHandler(req,res) { var body, dataHandler = function(chunk) { body += chunk; }; req.on('data',dataHandler); req.once('end',function(){ req.removeListener('data',dataHandler); // now you have the entire request body in var body :) console.log(body); }); } |
Here, I’ll give you the entire source to my MultipartParser Node.js module. It requires nodeproxy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
var MultipartParser ,proxy = require('nodeproxy') ,NL = '\r\n' // RFC2046 S4.1.1 ,BOUNDARY_PREFIX = NL+'--' // RFC2046 S5.1.1 ,HEADER_PAIR_DELIM = ':' ,HEADER_SUB_DELIM = '=' ; module.exports = MultipartParser = function(contentType,data) { this.parts = {}; this.fields = []; this.isMultipart = false; if ( typeof contentType == 'string' ) { contentType = contentType.trim(); this.isMultipart = contentType.indexOf('multipart/form-data') === 0; parse.call(this,contentType,data); } }; function parse(contentType,data) { if ( !this.isMultipart ) { return; } if ( data.substr(0,NL.length) != NL ) { data = NL+data; } var params = parseHeaderValue(contentType); if ( params.hasOwnProperty('boundary') ) { var parts = data.split(BOUNDARY_PREFIX+params.boundary); parts.forEach(proxy(function(chunk,i,arr) { // split the headers and body for this chunk var pieces = splitHeaderBody(chunk); if ( pieces.header && pieces.body ) { // build headers object var headers = parseHeader(pieces.header); // if nested multipart form-data, recurse if ( headers.hasOwnProperty('content-type') && headers['content-type'].indexOf('multipart/form-data') === 0 ) { parse.call(this,headers['content-type'],pieces.body); } else if ( headers.hasOwnProperty('content-disposition') ) { var disposition = parseHeaderValue(headers['content-disposition']); if ( disposition.hasOwnProperty('name') ) { this.fields.push(disposition.name); this.parts[disposition.name] = { headers: headers, disposition: disposition, mime: headers['content-type']||'', body: pieces.body }; } } } },this)); } } function splitHeaderBody(data) { var sections = data.split(NL+NL,2); return { header: sections[0]||'', body: sections[1]||'' }; } function parseHeader(header) { var headers = {}, headersArr = header.split(NL) .map(function(v){return v.trim();}) .filter(function(v){return !!v;}) headersArr.forEach(function(v){ var o = {}, t = v.split(HEADER_PAIR_DELIM,2); if ( typeof t[1] == 'string' ) { t[1] = t[1].trim(); } headers[t[0].toLowerCase().trim()] = t[1]; }); return headers; } function parseHeaderValue(value) { var params = {}, paramsArr = value.split(';') .map(function(v){return v.trim();}) .filter(function(v){return !!(v||v.indexOf('='));}); paramsArr.forEach(function(v){ var o = {}, t = v.split(HEADER_SUB_DELIM,2); if ( typeof t[1] == 'string' ) { t[1] = t[1].replace(/^[\s'"]+|[\s'"]+$/g,''); } params[t[0].toLowerCase().trim()] = t[1]; }); return params; } |
And the implementation is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var LittleDiddy ,http = require('http') ,MultipartParser = require('./MultipartParser.js'); LittleDiddy = function(){ this.server = http.createServer(requestHandler); this.server.listen(1337,'127.0.0.1'); }; function requestHandler(req,res) { var body, dataHandler = function(chunk) { body += chunk; }; req.on('data',dataHandler); req.once('end',function(){ req.removeListener('data',dataHandler); if ( req.headers.hasOwnProperty('content-type') ) { var multipartData = new MultipartParser(req.headers['content-type'],body); console.log(multipartData); } }); } new LittleDiddy(); |
LittleDiddy does not have any path routing (e.g. it won’t actually show a form that uses enctype="multipart/form-data"), it simply accepts and parses a POST with “file” inputs. This “little diddy” (implementation example) is untested by the way, so good luck.
This article’s source is released under the MIT license; I don’t care what you do with it.