|
|
if (global.GENTLY) require = GENTLY.hijack(require);
var fs = require('fs'); var util = require('util'), path = require('path'), File = require('./file'), MultipartParser = require('./multipart_parser').MultipartParser, QuerystringParser = require('./querystring_parser').QuerystringParser, OctetParser = require('./octet_parser').OctetParser, JSONParser = require('./json_parser').JSONParser, StringDecoder = require('string_decoder').StringDecoder, EventEmitter = require('events').EventEmitter, Stream = require('stream').Stream, os = require('os');
function IncomingForm(opts) { if (!(this instanceof IncomingForm)) return new IncomingForm(opts); EventEmitter.call(this);
opts=opts||{};
this.error = null; this.ended = false;
this.maxFields = opts.maxFields || 1000; this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024; this.keepExtensions = opts.keepExtensions || false; this.uploadDir = opts.uploadDir || os.tmpDir(); this.encoding = opts.encoding || 'utf-8'; this.headers = null; this.type = null; this.hash = false;
this.bytesReceived = null; this.bytesExpected = null;
this._parser = null; this._flushing = 0; this._fieldsSize = 0; this.openedFiles = [];
return this; }; util.inherits(IncomingForm, EventEmitter); exports.IncomingForm = IncomingForm;
IncomingForm.prototype.parse = function(req, cb) { this.pause = function() { try { req.pause(); } catch (err) { // the stream was destroyed
if (!this.ended) { // before it was completed, crash & burn
this._error(err); } return false; } return true; };
this.resume = function() { try { req.resume(); } catch (err) { // the stream was destroyed
if (!this.ended) { // before it was completed, crash & burn
this._error(err); } return false; }
return true; };
// Setup callback first, so we don't miss anything from data events emitted
// immediately.
if (cb) { var fields = {}, files = {}; this .on('field', function(name, value) { fields[name] = value; }) .on('file', function(name, file) { files[name] = file; }) .on('error', function(err) { cb(err, fields, files); }) .on('end', function() { cb(null, fields, files); }); }
// Parse headers and setup the parser, ready to start listening for data.
this.writeHeaders(req.headers);
// Start listening for data.
var self = this; req .on('error', function(err) { self._error(err); }) .on('aborted', function() { self.emit('aborted'); self._error(new Error('Request aborted')); }) .on('data', function(buffer) { self.write(buffer); }) .on('end', function() { if (self.error) { return; }
var err = self._parser.end(); if (err) { self._error(err); } });
return this; };
IncomingForm.prototype.writeHeaders = function(headers) { this.headers = headers; this._parseContentLength(); this._parseContentType(); };
IncomingForm.prototype.write = function(buffer) { if (!this._parser) { this._error(new Error('unintialized parser')); return; }
this.bytesReceived += buffer.length; this.emit('progress', this.bytesReceived, this.bytesExpected);
var bytesParsed = this._parser.write(buffer); if (bytesParsed !== buffer.length) { this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); }
return bytesParsed; };
IncomingForm.prototype.pause = function() { // this does nothing, unless overwritten in IncomingForm.parse
return false; };
IncomingForm.prototype.resume = function() { // this does nothing, unless overwritten in IncomingForm.parse
return false; };
IncomingForm.prototype.onPart = function(part) { // this method can be overwritten by the user
this.handlePart(part); };
IncomingForm.prototype.handlePart = function(part) { var self = this;
if (part.filename === undefined) { var value = '' , decoder = new StringDecoder(this.encoding);
part.on('data', function(buffer) { self._fieldsSize += buffer.length; if (self._fieldsSize > self.maxFieldsSize) { self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); return; } value += decoder.write(buffer); });
part.on('end', function() { self.emit('field', part.name, value); }); return; }
this._flushing++;
var file = new File({ path: this._uploadPath(part.filename), name: part.filename, type: part.mime, hash: self.hash });
this.emit('fileBegin', part.name, file);
file.open(); this.openedFiles.push(file);
part.on('data', function(buffer) { self.pause(); file.write(buffer, function() { self.resume(); }); });
part.on('end', function() { file.end(function() { self._flushing--; self.emit('file', part.name, file); self._maybeEnd(); }); }); };
function dummyParser(self) { return { end: function () { self.ended = true; self._maybeEnd(); return null; } }; }
IncomingForm.prototype._parseContentType = function() { if (this.bytesExpected === 0) { this._parser = dummyParser(this); return; }
if (!this.headers['content-type']) { this._error(new Error('bad content-type header, no content-type')); return; }
if (this.headers['content-type'].match(/octet-stream/i)) { this._initOctetStream(); return; }
if (this.headers['content-type'].match(/urlencoded/i)) { this._initUrlencoded(); return; }
if (this.headers['content-type'].match(/multipart/i)) { var m; if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { this._initMultipart(m[1] || m[2]); } else { this._error(new Error('bad content-type header, no multipart boundary')); } return; }
if (this.headers['content-type'].match(/json/i)) { this._initJSONencoded(); return; }
this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); };
IncomingForm.prototype._error = function(err) { if (this.error || this.ended) { return; }
this.error = err; this.pause(); this.emit('error', err);
if (Array.isArray(this.openedFiles)) { this.openedFiles.forEach(function(file) { file._writeStream.destroy(); setTimeout(fs.unlink, 0, file.path); }); } };
IncomingForm.prototype._parseContentLength = function() { this.bytesReceived = 0; if (this.headers['content-length']) { this.bytesExpected = parseInt(this.headers['content-length'], 10); } else if (this.headers['transfer-encoding'] === undefined) { this.bytesExpected = 0; }
if (this.bytesExpected !== null) { this.emit('progress', this.bytesReceived, this.bytesExpected); } };
IncomingForm.prototype._newParser = function() { return new MultipartParser(); };
IncomingForm.prototype._initMultipart = function(boundary) { this.type = 'multipart';
var parser = new MultipartParser(), self = this, headerField, headerValue, part;
parser.initWithBoundary(boundary);
parser.onPartBegin = function() { part = new Stream(); part.readable = true; part.headers = {}; part.name = null; part.filename = null; part.mime = null;
part.transferEncoding = 'binary'; part.transferBuffer = '';
headerField = ''; headerValue = ''; };
parser.onHeaderField = function(b, start, end) { headerField += b.toString(self.encoding, start, end); };
parser.onHeaderValue = function(b, start, end) { headerValue += b.toString(self.encoding, start, end); };
parser.onHeaderEnd = function() { headerField = headerField.toLowerCase(); part.headers[headerField] = headerValue;
var m; if (headerField == 'content-disposition') { if (m = headerValue.match(/\bname="([^"]+)"/i)) { part.name = m[1]; }
part.filename = self._fileName(headerValue); } else if (headerField == 'content-type') { part.mime = headerValue; } else if (headerField == 'content-transfer-encoding') { part.transferEncoding = headerValue.toLowerCase(); }
headerField = ''; headerValue = ''; };
parser.onHeadersEnd = function() { switch(part.transferEncoding){ case 'binary': case '7bit': case '8bit': parser.onPartData = function(b, start, end) { part.emit('data', b.slice(start, end)); };
parser.onPartEnd = function() { part.emit('end'); }; break;
case 'base64': parser.onPartData = function(b, start, end) { part.transferBuffer += b.slice(start, end).toString('ascii');
/* four bytes (chars) in base64 converts to three bytes in binary encoding. So we should always work with a number of bytes that can be divided by 4, it will result in a number of buytes that can be divided vy 3. */ var offset = parseInt(part.transferBuffer.length / 4) * 4; part.emit('data', new Buffer(part.transferBuffer.substring(0, offset), 'base64')) part.transferBuffer = part.transferBuffer.substring(offset); };
parser.onPartEnd = function() { part.emit('data', new Buffer(part.transferBuffer, 'base64')) part.emit('end'); }; break;
default: return self._error(new Error('unknown transfer-encoding')); }
self.onPart(part); };
parser.onEnd = function() { self.ended = true; self._maybeEnd(); };
this._parser = parser; };
IncomingForm.prototype._fileName = function(headerValue) { var m = headerValue.match(/\bfilename="(.*?)"($|; )/i); if (!m) return;
var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); filename = filename.replace(/%22/g, '"'); filename = filename.replace(/&#([\d]{4});/g, function(m, code) { return String.fromCharCode(code); }); return filename; };
IncomingForm.prototype._initUrlencoded = function() { this.type = 'urlencoded';
var parser = new QuerystringParser(this.maxFields) , self = this;
parser.onField = function(key, val) { self.emit('field', key, val); };
parser.onEnd = function() { self.ended = true; self._maybeEnd(); };
this._parser = parser; };
IncomingForm.prototype._initOctetStream = function() { this.type = 'octet-stream'; var filename = this.headers['x-file-name']; var mime = this.headers['content-type'];
var file = new File({ path: this._uploadPath(filename), name: filename, type: mime });
file.open();
this.emit('fileBegin', filename, file);
this._flushing++;
var self = this;
self._parser = new OctetParser();
//Keep track of writes that haven't finished so we don't emit the file before it's done being written
var outstandingWrites = 0;
self._parser.on('data', function(buffer){ self.pause(); outstandingWrites++;
file.write(buffer, function() { outstandingWrites--; self.resume();
if(self.ended){ self._parser.emit('doneWritingFile'); } }); });
self._parser.on('end', function(){ self._flushing--; self.ended = true;
var done = function(){ self.emit('file', 'file', file); self._maybeEnd(); };
if(outstandingWrites === 0){ done(); } else { self._parser.once('doneWritingFile', done); } }); };
IncomingForm.prototype._initJSONencoded = function() { this.type = 'json';
var parser = new JSONParser() , self = this;
if (this.bytesExpected) { parser.initWithLength(this.bytesExpected); }
parser.onField = function(key, val) { self.emit('field', key, val); }
parser.onEnd = function() { self.ended = true; self._maybeEnd(); };
this._parser = parser; };
IncomingForm.prototype._uploadPath = function(filename) { var name = ''; for (var i = 0; i < 32; i++) { name += Math.floor(Math.random() * 16).toString(16); }
if (this.keepExtensions) { var ext = path.extname(filename); ext = ext.replace(/(\.[a-z0-9]+).*/, '$1');
name += ext; }
return path.join(this.uploadDir, name); };
IncomingForm.prototype._maybeEnd = function() { if (!this.ended || this._flushing || this.error) { return; }
this.emit('end'); };
|