/** * Data format: * --FormStreamBoundary1349886663601\r\n Content-Disposition: form-data; name="foo"\r\n \r\n \r\n --FormStreamBoundary1349886663601\r\n Content-Disposition: form-data; name="file"; filename="formstream.test.js"\r\n Content-Type: application/javascript\r\n \r\n \r\n --FormStreamBoundary1349886663601\r\n Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n Content-Type: image/png\r\n \r\n \r\n --FormStreamBoundary1349886663601-- * */ 'use strict'; var Stream = require('stream'); var parseStream = require('pause-stream'); var util = require('util'); var mime = require('mime'); var path = require('path'); var fs = require('fs'); var destroy = require('destroy'); var PADDING = '--'; var NEW_LINE = '\r\n'; var NEW_LINE_BUFFER = Buffer.from(NEW_LINE); function FormStream() { if (!(this instanceof FormStream)) { return new FormStream(); } FormStream.super_.call(this); this._boundary = this._generateBoundary(); this._streams = []; this._buffers = []; this._endData = Buffer.from(PADDING + this._boundary + PADDING + NEW_LINE); this._contentLength = 0; this._isAllStreamSizeKnown = true; this._knownStreamSize = 0; } util.inherits(FormStream, Stream); module.exports = FormStream; FormStream.prototype._generateBoundary = function() { // https://github.com/felixge/node-form-data/blob/master/lib/form_data.js#L162 // 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); } return boundary; }; FormStream.prototype.setTotalStreamSize = function (size) { // this method should not make any sense if the length of each stream is known. if (this._isAllStreamSizeKnown) { return this; } size = size || 0; for (var i = 0; i < this._streams.length; i++) { size += this._streams[i][0].length; size += NEW_LINE_BUFFER.length; // stream field end pedding size } this._knownStreamSize = size; this._isAllStreamSizeKnown = true; return this; }; FormStream.prototype.headers = function (options) { var headers = { 'Content-Type': 'multipart/form-data; boundary=' + this._boundary }; // calculate total stream size this._contentLength += this._knownStreamSize; // calculate length of end padding this._contentLength += this._endData.length; if (this._isAllStreamSizeKnown) { headers['Content-Length'] = String(this._contentLength); } if (options) { for (var k in options) { headers[k] = options[k]; } } return headers; }; FormStream.prototype.file = function (name, filepath, filename, filesize) { var mimeType = mime.getType(filepath); if (typeof filename === 'number' && !filesize) { filesize = filename; filename = path.basename(filepath); } else if (!filename) { filename = path.basename(filepath); } var stream = fs.createReadStream(filepath); return this.stream(name, stream, filename, mimeType, filesize); }; /** * Add a form field * @param {String} name field name * @param {String|Buffer} value field value * @return {this} */ FormStream.prototype.field = function (name, value) { if (!Buffer.isBuffer(value)) { // field(String, Number) // https://github.com/qiniu/nodejs-sdk/issues/123 if (typeof value === 'number') { value = String(value); } value = Buffer.from(value); } return this.buffer(name, value); }; FormStream.prototype.stream = function (name, stream, filename, mimeType, size) { if (typeof mimeType === 'number' && !size) { size = mimeType; mimeType = mime.getType(filename); } else if (!mimeType) { mimeType = mime.getType(filename); } stream.once('error', this.emit.bind(this, 'error')); // if form stream destroy, also destroy the source stream this.once('destroy', function () { destroy(stream); }); var leading = this._leading({ name: name, filename: filename }, mimeType); var ps = parseStream().pause(); stream.pipe(ps); this._streams.push([leading, ps]); // if the size of this stream is known, plus the total content-length; // otherwise, content-length is unknown. if (typeof size === 'number') { this._knownStreamSize += leading.length; this._knownStreamSize += size; this._knownStreamSize += NEW_LINE_BUFFER.length; } else { this._isAllStreamSizeKnown = false; } process.nextTick(this.resume.bind(this)); return this; }; FormStream.prototype.buffer = function (name, buffer, filename, mimeType) { if (filename && !mimeType) { mimeType = mime.getType(filename); } var disposition = { name: name }; if (filename) { disposition.filename = filename; } var leading = this._leading(disposition, mimeType); this._buffers.push([leading, buffer]); // plus buffer length to total content-length this._contentLength += leading.length; this._contentLength += buffer.length; this._contentLength += NEW_LINE_BUFFER.length; process.nextTick(this.resume.bind(this)); return this; }; FormStream.prototype._leading = function (disposition, type) { var leading = [PADDING + this._boundary]; var disps = []; if (disposition) { for (var k in disposition) { disps.push(k + '="' + disposition[k] + '"'); } } leading.push('Content-Disposition: form-data; ' + disps.join('; ')); if (type) { leading.push('Content-Type: ' + type); } leading.push(''); leading.push(''); return Buffer.from(leading.join(NEW_LINE)); }; FormStream.prototype._emitBuffers = function () { if (!this._buffers.length) { return; } for (var i = 0; i < this._buffers.length; i++) { var item = this._buffers[i]; this.emit('data', item[0]); // part leading this.emit('data', item[1]); // part content this.emit('data', NEW_LINE_BUFFER); } this._buffers = []; }; FormStream.prototype._emitStream = function (item) { var self = this; // item: [ fieldData, stream ] self.emit('data', item[0]); var stream = item[1]; stream.on('data', function (data) { self.emit('data', data); }); stream.on('end', function () { self.emit('data', NEW_LINE_BUFFER); return process.nextTick(self.drain.bind(self)); }); stream.resume(); }; FormStream.prototype._emitEnd = function () { // ending format: // // --{boundary}--\r\n this.emit('data', this._endData); this.emit('end'); }; FormStream.prototype.drain = function () { this._emitBuffers(); var item = this._streams.shift(); if (item) { this._emitStream(item); } else { this._emitEnd(); } return this; }; FormStream.prototype.resume = function () { this.paused = false; if (!this._draining) { this._draining = true; this.drain(); } return this; }; FormStream.prototype.close = FormStream.prototype.destroy = function () { this.emit('destroy'); };