123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- /**
- * Data format:
- *
- --FormStreamBoundary1349886663601\r\n
- Content-Disposition: form-data; name="foo"\r\n
- \r\n
- <FIELD-CONTENT>\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
- <FILE-CONTENT>\r\n
- --FormStreamBoundary1349886663601\r\n
- Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n
- Content-Type: image/png\r\n
- \r\n
- <IMAGE-CONTENT>\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 = new Buffer(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 = new Buffer(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.lookup(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 = new Buffer(value);
- }
- return this.buffer(name, value);
- };
- FormStream.prototype.stream = function (name, stream, filename, mimeType, size) {
- if (typeof mimeType === 'number' && !size) {
- size = mimeType;
- mimeType = mime.lookup(filename);
- } else if (!mimeType) {
- mimeType = mime.lookup(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.lookup(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 new Buffer(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');
- };
|