--- /dev/null
+var CombinedStream = require('combined-stream');
+var util = require('util');
+var path = require('path');
+var http = require('http');
+var https = require('https');
+var parseUrl = require('url').parse;
+var fs = require('fs');
+var mime = require('mime-types');
+var async = require('async');
+
+module.exports = FormData;
+function FormData() {
+ this._overheadLength = 0;
+ this._valueLength = 0;
+ this._lengthRetrievers = [];
+
+ CombinedStream.call(this);
+}
+util.inherits(FormData, CombinedStream);
+
+FormData.LINE_BREAK = '\r\n';
+FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';
+
+FormData.prototype.append = function(field, value, options) {
+ options = (typeof options === 'string')
+ ? { filename: options }
+ : options || {};
+
+ var append = CombinedStream.prototype.append.bind(this);
+
+ // all that streamy business can't handle numbers
+ if (typeof value == 'number') value = ''+value;
+
+ // https://github.com/felixge/node-form-data/issues/38
+ if (util.isArray(value)) {
+ // Please convert your array into string
+ // the way web server expects it
+ this._error(new Error('Arrays are not supported.'));
+ return;
+ }
+
+ var header = this._multiPartHeader(field, value, options);
+ var footer = this._multiPartFooter(field, value, options);
+
+ append(header);
+ append(value);
+ append(footer);
+
+ // pass along options.knownLength
+ this._trackLength(header, value, options);
+};
+
+FormData.prototype._trackLength = function(header, value, options) {
+ var valueLength = 0;
+
+ // used w/ getLengthSync(), when length is known.
+ // e.g. for streaming directly from a remote server,
+ // w/ a known file a size, and not wanting to wait for
+ // incoming file to finish to get its size.
+ if (options.knownLength != null) {
+ valueLength += +options.knownLength;
+ } else if (Buffer.isBuffer(value)) {
+ valueLength = value.length;
+ } else if (typeof value === 'string') {
+ valueLength = Buffer.byteLength(value);
+ }
+
+ this._valueLength += valueLength;
+
+ // @check why add CRLF? does this account for custom/multiple CRLFs?
+ this._overheadLength +=
+ Buffer.byteLength(header) +
+ FormData.LINE_BREAK.length;
+
+ // empty or either doesn't have path or not an http response
+ if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) {
+ return;
+ }
+
+ // no need to bother with the length
+ if (!options.knownLength)
+ this._lengthRetrievers.push(function(next) {
+
+ if (value.hasOwnProperty('fd')) {
+
+ // take read range into a account
+ // `end` = Infinity –> read file till the end
+ //
+ // TODO: Looks like there is bug in Node fs.createReadStream
+ // it doesn't respect `end` options without `start` options
+ // Fix it when node fixes it.
+ // https://github.com/joyent/node/issues/7819
+ if (value.end != undefined && value.end != Infinity && value.start != undefined) {
+
+ // when end specified
+ // no need to calculate range
+ // inclusive, starts with 0
+ next(null, value.end+1 - (value.start ? value.start : 0));
+
+ // not that fast snoopy
+ } else {
+ // still need to fetch file size from fs
+ fs.stat(value.path, function(err, stat) {
+
+ var fileSize;
+
+ if (err) {
+ next(err);
+ return;
+ }
+
+ // update final size based on the range options
+ fileSize = stat.size - (value.start ? value.start : 0);
+ next(null, fileSize);
+ });
+ }
+
+ // or http response
+ } else if (value.hasOwnProperty('httpVersion')) {
+ next(null, +value.headers['content-length']);
+
+ // or request stream http://github.com/mikeal/request
+ } else if (value.hasOwnProperty('httpModule')) {
+ // wait till response come back
+ value.on('response', function(response) {
+ value.pause();
+ next(null, +response.headers['content-length']);
+ });
+ value.resume();
+
+ // something else
+ } else {
+ next('Unknown stream');
+ }
+ });
+};
+
+FormData.prototype._multiPartHeader = function(field, value, options) {
+ // custom header specified (as string)?
+ // it becomes responsible for boundary
+ // (e.g. to handle extra CRLFs on .NET servers)
+ if (options.header != null) {
+ return options.header;
+ }
+
+ var contents = '';
+ var headers = {
+ 'Content-Disposition': ['form-data', 'name="' + field + '"'],
+ 'Content-Type': []
+ };
+
+ // fs- and request- streams have path property
+ // or use custom filename and/or contentType
+ // TODO: Use request's response mime-type
+ if (options.filename || value.path) {
+ headers['Content-Disposition'].push(
+ 'filename="' + path.basename(options.filename || value.path) + '"'
+ );
+ headers['Content-Type'].push(
+ options.contentType ||
+ mime.lookup(options.filename || value.path) ||
+ FormData.DEFAULT_CONTENT_TYPE
+ );
+ // http response has not
+ } else if (value.readable && value.hasOwnProperty('httpVersion')) {
+ headers['Content-Disposition'].push(
+ 'filename="' + path.basename(value.client._httpMessage.path) + '"'
+ );
+ headers['Content-Type'].push(
+ options.contentType ||
+ value.headers['content-type'] ||
+ FormData.DEFAULT_CONTENT_TYPE
+ );
+ } else if (Buffer.isBuffer(value)) {
+ headers['Content-Type'].push(
+ options.contentType ||
+ FormData.DEFAULT_CONTENT_TYPE
+ );
+ } else if (options.contentType) {
+ headers['Content-Type'].push(options.contentType);
+ }
+
+ for (var prop in headers) {
+ if (headers[prop].length) {
+ contents += prop + ': ' + headers[prop].join('; ') + FormData.LINE_BREAK;
+ }
+ }
+
+ return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;
+};
+
+FormData.prototype._multiPartFooter = function(field, value, options) {
+ return function(next) {
+ var footer = FormData.LINE_BREAK;
+
+ var lastPart = (this._streams.length === 0);
+ if (lastPart) {
+ footer += this._lastBoundary();
+ }
+
+ next(footer);
+ }.bind(this);
+};
+
+FormData.prototype._lastBoundary = function() {
+ return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;
+};
+
+FormData.prototype.getHeaders = function(userHeaders) {
+ var formHeaders = {
+ 'content-type': 'multipart/form-data; boundary=' + this.getBoundary()
+ };
+
+ for (var header in userHeaders) {
+ formHeaders[header.toLowerCase()] = userHeaders[header];
+ }
+
+ return formHeaders;
+}
+
+FormData.prototype.getCustomHeaders = function(contentType) {
+ contentType = contentType ? contentType : 'multipart/form-data';
+
+ var formHeaders = {
+ 'content-type': contentType + '; boundary=' + this.getBoundary(),
+ 'content-length': this.getLengthSync()
+ };
+
+ return formHeaders;
+}
+
+FormData.prototype.getBoundary = function() {
+ if (!this._boundary) {
+ this._generateBoundary();
+ }
+
+ return this._boundary;
+};
+
+FormData.prototype._generateBoundary = function() {
+ // This generates a 50 character boundary similar to those used by Firefox.
+ // They are optimized for boyer-moore parsing.
+ var boundary = '--------------------------';
+ for (var i = 0; i < 24; i++) {
+ boundary += Math.floor(Math.random() * 10).toString(16);
+ }
+
+ this._boundary = boundary;
+};
+
+// Note: getLengthSync DOESN'T calculate streams length
+// As workaround one can calculate file size manually
+// and add it as knownLength option
+FormData.prototype.getLengthSync = function(debug) {
+ var knownLength = this._overheadLength + this._valueLength;
+
+ // Don't get confused, there are 3 "internal" streams for each keyval pair
+ // so it basically checks if there is any value added to the form
+ if (this._streams.length) {
+ knownLength += this._lastBoundary().length;
+ }
+
+ // https://github.com/felixge/node-form-data/issues/40
+ if (this._lengthRetrievers.length) {
+ // Some async length retrivers are present
+ // therefore synchronous length calculation is false.
+ // Please use getLength(callback) to get proper length
+ this._error(new Error('Cannot calculate proper length in synchronous way.'));
+ }
+
+ return knownLength;
+};
+
+FormData.prototype.getLength = function(cb) {
+ var knownLength = this._overheadLength + this._valueLength;
+
+ if (this._streams.length) {
+ knownLength += this._lastBoundary().length;
+ }
+
+ if (!this._lengthRetrievers.length) {
+ process.nextTick(cb.bind(this, null, knownLength));
+ return;
+ }
+
+ async.parallel(this._lengthRetrievers, function(err, values) {
+ if (err) {
+ cb(err);
+ return;
+ }
+
+ values.forEach(function(length) {
+ knownLength += length;
+ });
+
+ cb(null, knownLength);
+ });
+};
+
+FormData.prototype.submit = function(params, cb) {
+
+ var request
+ , options
+ , defaults = {
+ method : 'post'
+ };
+
+ // parse provided url if it's string
+ // or treat it as options object
+ if (typeof params == 'string') {
+ params = parseUrl(params);
+
+ options = populate({
+ port: params.port,
+ path: params.pathname,
+ host: params.hostname
+ }, defaults);
+ }
+ else // use custom params
+ {
+ options = populate(params, defaults);
+ // if no port provided use default one
+ if (!options.port) {
+ options.port = options.protocol == 'https:' ? 443 : 80;
+ }
+ }
+
+ // put that good code in getHeaders to some use
+ options.headers = this.getHeaders(params.headers);
+
+ // https if specified, fallback to http in any other case
+ if (options.protocol == 'https:') {
+ request = https.request(options);
+ } else {
+ request = http.request(options);
+ }
+
+ // get content length and fire away
+ this.getLength(function(err, length) {
+
+ // TODO: Add chunked encoding when no length (if err)
+
+ // add content length
+ request.setHeader('Content-Length', length);
+
+ this.pipe(request);
+ if (cb) {
+ request.on('error', cb);
+ request.on('response', cb.bind(this, null));
+ }
+ }.bind(this));
+
+ return request;
+};
+
+FormData.prototype._error = function(err) {
+ if (this.error) return;
+
+ this.error = err;
+ this.pause();
+ this.emit('error', err);
+};
+
+/*
+ * Santa's little helpers
+ */
+
+// populates missing values
+function populate(dst, src) {
+ for (var prop in src) {
+ if (!dst[prop]) dst[prop] = src[prop];
+ }
+ return dst;
+}