| /** | |
|  * Form Data format: | |
|  * | |
|  | |
| ```txt | |
| --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="data"\r\n | |
| Content-Type: application/json\r\n | |
| \r\n | |
| <JSON-FORMAT-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-CHUNK-1> | |
| ... | |
| <FILE-CONTENT-CHUNK-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 | |
| <IMAGE-CONTENT>\r\n | |
| --FormStreamBoundary1349886663601-- | |
| ``` | |
|  | |
|  * | |
|  */ | |
| 
 | |
| 'use strict'; | |
| 
 | |
| var debug = require('util').debuglog('formstream'); | |
| 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 hex = require('node-hex'); | |
| 
 | |
| var PADDING = '--'; | |
| var NEW_LINE = '\r\n'; | |
| var NEW_LINE_BUFFER =  Buffer.from(NEW_LINE); | |
| 
 | |
| function FormStream(options) { | |
|   if (!(this instanceof FormStream)) { | |
|     return new FormStream(options); | |
|   } | |
| 
 | |
|   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; | |
|   this._minChunkSize = options && options.minChunkSize || 0; | |
| 
 | |
|   this.isFormStream = true; | |
|   debug('start boundary\n%s', this._boundary); | |
| } | |
| 
 | |
| 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 padding size | |
|   } | |
| 
 | |
|   this._knownStreamSize = size; | |
|   this._isAllStreamSizeKnown = true; | |
|   debug('set total size: %s', size); | |
|   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]; | |
|     } | |
|   } | |
| 
 | |
|   debug('headers: %j', headers); | |
|   return headers; | |
| }; | |
| 
 | |
| FormStream.prototype.file = function (name, filepath, filename, filesize) { | |
|   if (typeof filename === 'number' && !filesize) { | |
|     filesize = filename; | |
|     filename = path.basename(filepath); | |
|   } | |
|   if (!filename) { | |
|     filename = path.basename(filepath); | |
|   } | |
| 
 | |
|   var mimeType = mime.getType(filename); | |
|   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 | |
|  * @param  {String} [mimeType] field mimeType | |
|  * @return {this} | |
|  */ | |
| FormStream.prototype.field = function (name, value, mimeType) { | |
|   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, null, mimeType); | |
| }; | |
| 
 | |
| 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); | |
| 
 | |
|   // plus buffer length to total content-length | |
|   var bufferSize = leading.length + buffer.length + NEW_LINE_BUFFER.length; | |
|   this._buffers.push(Buffer.concat([leading, buffer, NEW_LINE_BUFFER], bufferSize));   | |
|   this._contentLength += bufferSize; | |
| 
 | |
|   process.nextTick(this.resume.bind(this)); | |
|   if (debug.enabled) { | |
|     if (buffer.length > 512) { | |
|       debug('new buffer field, content size: %d\n%s%s', | |
|         buffer.length, leading.toString(), hex(buffer.slice(0, 512))); | |
|     } else { | |
|       debug('new buffer field, content size: %d\n%s%s', | |
|         buffer.length, leading.toString(), hex(buffer)); | |
|     } | |
|   } | |
|   return this; | |
| }; | |
| 
 | |
| FormStream.prototype._leading = function (disposition, type) { | |
|   var leading = [PADDING + this._boundary]; | |
| 
 | |
|   var dispositions = []; | |
| 
 | |
|   if (disposition) { | |
|     for (var k in disposition) { | |
|       dispositions.push(k + '="' + disposition[k] + '"'); | |
|     } | |
|   } | |
| 
 | |
|   leading.push('Content-Disposition: form-data; ' + dispositions.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++) { | |
|     this.emit('data', this._buffers[i]); | |
|   } | |
|   this._buffers = []; | |
| }; | |
| 
 | |
| FormStream.prototype._emitStream = function (item) { | |
|   var self = this; | |
|   // item: [ leading, stream ] | |
|   var streamSize = 0; | |
|   var chunkCount = 0; | |
|   const leading = item[0]; | |
|   self.emit('data', leading); | |
|   chunkCount++; | |
|   if (debug.enabled) { | |
|     debug('new stream, chunk index %d\n%s', chunkCount, leading.toString()); | |
|   } | |
| 
 | |
|   var stream = item[1]; | |
|   stream.on('data', function (data) { | |
|     self.emit('data', data); | |
|     streamSize += leading.length; | |
|     chunkCount++; | |
|     if (debug.enabled) { | |
|       if (data.length > 512) { | |
|         debug('stream chunk, size %d, chunk index %d, stream size %d\n%s......   only show 512 bytes   ......', | |
|           data.length, chunkCount, streamSize, hex(data.slice(0, 512))); | |
|       } else { | |
|         debug('stream chunk, size %d, chunk index %d, stream size %d\n%s', | |
|           data.length, chunkCount, streamSize, hex(data)); | |
|       } | |
|     } | |
|   }); | |
|   stream.on('end', function () { | |
|     self.emit('data', NEW_LINE_BUFFER); | |
|     chunkCount++; | |
|     debug('stream end, chunk index %d, stream size %d', chunkCount, streamSize); | |
|     return process.nextTick(self.drain.bind(self)); | |
|   }); | |
|   stream.resume(); | |
| }; | |
| 
 | |
| FormStream.prototype._emitStreamWithChunkSize = function (item, minChunkSize) { | |
|   var self = this; | |
|   // item: [ leading, stream ] | |
|   var streamSize = 0; | |
|   var chunkCount = 0; | |
|   var bufferSize = 0; | |
|   var buffers = []; | |
|   const leading = item[0]; | |
|   buffers.push(leading); | |
|   bufferSize += leading.length; | |
|   if (debug.enabled) { | |
|     debug('new stream, with min chunk size: %d\n%s', minChunkSize, leading.toString()); | |
|   } | |
| 
 | |
|   var stream = item[1]; | |
|   stream.on('data', function (data) { | |
|     if (typeof data === 'string') { | |
|       data = Buffer.from(data, 'utf-8'); | |
|     } | |
|     buffers.push(data); | |
|     bufferSize += data.length; | |
|     streamSize += data.length; | |
|     debug('got stream data size %d, buffer size %d, stream size %d', | |
|       data.length, bufferSize, streamSize); | |
|     if (bufferSize >= minChunkSize) { | |
|       const chunk = Buffer.concat(buffers, bufferSize); | |
|       buffers = []; | |
|       bufferSize = 0; | |
|       self.emit('data', chunk); | |
|       chunkCount++; | |
|       if (debug.enabled) { | |
|         if (chunk.length > 512) { | |
|           debug('stream chunk, size %d, chunk index %d, stream size %d\n%s......   only show 512 bytes   ......', | |
|             chunk.length, chunkCount, streamSize, hex(chunk.slice(0, 512))); | |
|         } else { | |
|           debug('stream chunk, size %d, chunk index %d, stream size %d\n%s', | |
|             chunk.length, chunkCount, streamSize, hex(chunk)); | |
|         } | |
|       } | |
|     } | |
|   }); | |
|   stream.on('end', function () { | |
|     buffers.push(NEW_LINE_BUFFER); | |
|     bufferSize += NEW_LINE_BUFFER.length; | |
|     const chunk = Buffer.concat(buffers, bufferSize); | |
|     self.emit('data', chunk); | |
|     chunkCount++; | |
|     if (chunk.length > 512) { | |
|       debug('stream end, size %d, chunk index %d, stream size %d\n%s......   only show 512 bytes   ......', | |
|         chunk.length, chunkCount, streamSize, hex(chunk.slice(0, 512))); | |
|     } else { | |
|       debug('stream end, size %d, chunk index %d, stream size %d\n%s', | |
|         chunk.length, chunkCount, streamSize, hex(chunk)); | |
|     } | |
|     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'); | |
|   if (debug.enabled) { | |
|     debug('end boundary\n%s', this._endData.toString()); | |
|   } | |
| }; | |
| 
 | |
| FormStream.prototype.drain = function () { | |
|   // debug('drain'); | |
|   this._emitBuffers(); | |
| 
 | |
|   var item = this._streams.shift(); | |
|   if (item) { | |
|     if (this._minChunkSize && this._minChunkSize > 0) { | |
|       this._emitStreamWithChunkSize(item, this._minChunkSize); | |
|     } else { | |
|       this._emitStream(item); | |
|     } | |
|   } else { | |
|     this._emitEnd(); | |
|   } | |
| 
 | |
|   return this; | |
| }; | |
| 
 | |
| FormStream.prototype.resume = function () { | |
|   // debug('resume'); | |
|   this.paused = false; | |
| 
 | |
|   if (!this._draining) { | |
|     this._draining = true; | |
|     this.drain(); | |
|   } | |
| 
 | |
|   return this; | |
| }; | |
| 
 | |
| FormStream.prototype.close = FormStream.prototype.destroy = function () { | |
|   this.emit('destroy'); | |
|   // debug('destroy or close'); | |
| };
 |