formstream.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. /**
  2. * Form Data format:
  3. *
  4. ```txt
  5. --FormStreamBoundary1349886663601\r\n
  6. Content-Disposition: form-data; name="foo"\r\n
  7. \r\n
  8. <FIELD-CONTENT>\r\n
  9. --FormStreamBoundary1349886663601\r\n
  10. Content-Disposition: form-data; name="data"\r\n
  11. Content-Type: application/json\r\n
  12. \r\n
  13. <JSON-FORMAT-CONTENT>\r\n
  14. --FormStreamBoundary1349886663601\r\n
  15. Content-Disposition: form-data; name="file"; filename="formstream.test.js"\r\n
  16. Content-Type: application/javascript\r\n
  17. \r\n
  18. <FILE-CONTENT-CHUNK-1>
  19. ...
  20. <FILE-CONTENT-CHUNK-N>
  21. \r\n
  22. --FormStreamBoundary1349886663601\r\n
  23. Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n
  24. Content-Type: image/png\r\n
  25. \r\n
  26. <IMAGE-CONTENT>\r\n
  27. --FormStreamBoundary1349886663601--
  28. ```
  29. *
  30. */
  31. 'use strict';
  32. var debug = require('util').debuglog('formstream');
  33. var Stream = require('stream');
  34. var crypto = require('crypto');
  35. var parseStream = require('pause-stream');
  36. var util = require('util');
  37. var mime = require('mime');
  38. var path = require('path');
  39. var fs = require('fs');
  40. var destroy = require('destroy');
  41. var hex = require('node-hex');
  42. var PADDING = '--';
  43. var NEW_LINE = '\r\n';
  44. var NEW_LINE_BUFFER = Buffer.from(NEW_LINE);
  45. function FormStream(options) {
  46. if (!(this instanceof FormStream)) {
  47. return new FormStream(options);
  48. }
  49. FormStream.super_.call(this);
  50. this._boundary = this._generateBoundary();
  51. this._streams = [];
  52. this._buffers = [];
  53. this._endData = Buffer.from(PADDING + this._boundary + PADDING + NEW_LINE);
  54. this._contentLength = 0;
  55. this._isAllStreamSizeKnown = true;
  56. this._knownStreamSize = 0;
  57. this._minChunkSize = options && options.minChunkSize || 0;
  58. this.isFormStream = true;
  59. debug('start boundary\n%s', this._boundary);
  60. }
  61. util.inherits(FormStream, Stream);
  62. module.exports = FormStream;
  63. FormStream.prototype._generateBoundary = function() {
  64. // https://github.com/form-data/form-data/blob/16e00765342106876f98a1c9703314006c9e937a/lib/form_data.js#L345
  65. return '--------------------------' + crypto.randomBytes(12).toString('hex');
  66. };
  67. FormStream.prototype.setTotalStreamSize = function (size) {
  68. // this method should not make any sense if the length of each stream is known.
  69. if (this._isAllStreamSizeKnown) {
  70. return this;
  71. }
  72. size = size || 0;
  73. for (var i = 0; i < this._streams.length; i++) {
  74. size += this._streams[i][0].length;
  75. size += NEW_LINE_BUFFER.length; // stream field end padding size
  76. }
  77. this._knownStreamSize = size;
  78. this._isAllStreamSizeKnown = true;
  79. debug('set total size: %s', size);
  80. return this;
  81. };
  82. FormStream.prototype.headers = function (options) {
  83. var headers = {
  84. 'Content-Type': 'multipart/form-data; boundary=' + this._boundary
  85. };
  86. // calculate total stream size
  87. this._contentLength += this._knownStreamSize;
  88. // calculate length of end padding
  89. this._contentLength += this._endData.length;
  90. if (this._isAllStreamSizeKnown) {
  91. headers['Content-Length'] = String(this._contentLength);
  92. }
  93. if (options) {
  94. for (var k in options) {
  95. headers[k] = options[k];
  96. }
  97. }
  98. debug('headers: %j', headers);
  99. return headers;
  100. };
  101. FormStream.prototype.file = function (name, filepath, filename, filesize) {
  102. if (typeof filename === 'number' && !filesize) {
  103. filesize = filename;
  104. filename = path.basename(filepath);
  105. }
  106. if (!filename) {
  107. filename = path.basename(filepath);
  108. }
  109. var mimeType = mime.getType(filename);
  110. var stream = fs.createReadStream(filepath);
  111. return this.stream(name, stream, filename, mimeType, filesize);
  112. };
  113. /**
  114. * Add a form field
  115. * @param {String} name field name
  116. * @param {String|Buffer} value field value
  117. * @param {String} [mimeType] field mimeType
  118. * @return {this}
  119. */
  120. FormStream.prototype.field = function (name, value, mimeType) {
  121. if (!Buffer.isBuffer(value)) {
  122. // field(String, Number)
  123. // https://github.com/qiniu/nodejs-sdk/issues/123
  124. if (typeof value === 'number') {
  125. value = String(value);
  126. }
  127. value = Buffer.from(value);
  128. }
  129. return this.buffer(name, value, null, mimeType);
  130. };
  131. FormStream.prototype.stream = function (name, stream, filename, mimeType, size) {
  132. if (typeof mimeType === 'number' && !size) {
  133. size = mimeType;
  134. mimeType = mime.getType(filename);
  135. } else if (!mimeType) {
  136. mimeType = mime.getType(filename);
  137. }
  138. stream.once('error', this.emit.bind(this, 'error'));
  139. // if form stream destroy, also destroy the source stream
  140. this.once('destroy', function () {
  141. destroy(stream);
  142. });
  143. var leading = this._leading({ name: name, filename: filename }, mimeType);
  144. var ps = parseStream().pause();
  145. stream.pipe(ps);
  146. this._streams.push([leading, ps]);
  147. // if the size of this stream is known, plus the total content-length;
  148. // otherwise, content-length is unknown.
  149. if (typeof size === 'number') {
  150. this._knownStreamSize += leading.length;
  151. this._knownStreamSize += size;
  152. this._knownStreamSize += NEW_LINE_BUFFER.length;
  153. } else {
  154. this._isAllStreamSizeKnown = false;
  155. }
  156. process.nextTick(this.resume.bind(this));
  157. return this;
  158. };
  159. FormStream.prototype.buffer = function (name, buffer, filename, mimeType) {
  160. if (filename && !mimeType) {
  161. mimeType = mime.getType(filename);
  162. }
  163. var disposition = { name: name };
  164. if (filename) {
  165. disposition.filename = filename;
  166. }
  167. var leading = this._leading(disposition, mimeType);
  168. // plus buffer length to total content-length
  169. var bufferSize = leading.length + buffer.length + NEW_LINE_BUFFER.length;
  170. this._buffers.push(Buffer.concat([leading, buffer, NEW_LINE_BUFFER], bufferSize));
  171. this._contentLength += bufferSize;
  172. process.nextTick(this.resume.bind(this));
  173. if (debug.enabled) {
  174. if (buffer.length > 512) {
  175. debug('new buffer field, content size: %d\n%s%s',
  176. buffer.length, leading.toString(), hex(buffer.slice(0, 512)));
  177. } else {
  178. debug('new buffer field, content size: %d\n%s%s',
  179. buffer.length, leading.toString(), hex(buffer));
  180. }
  181. }
  182. return this;
  183. };
  184. FormStream.prototype._leading = function (disposition, type) {
  185. var leading = [PADDING + this._boundary];
  186. var dispositions = [];
  187. if (disposition) {
  188. for (var k in disposition) {
  189. dispositions.push(k + '="' + disposition[k] + '"');
  190. }
  191. }
  192. leading.push('Content-Disposition: form-data; ' + dispositions.join('; '));
  193. if (type) {
  194. leading.push('Content-Type: ' + type);
  195. }
  196. leading.push('');
  197. leading.push('');
  198. return Buffer.from(leading.join(NEW_LINE));
  199. };
  200. FormStream.prototype._emitBuffers = function () {
  201. if (!this._buffers.length) {
  202. return;
  203. }
  204. for (var i = 0; i < this._buffers.length; i++) {
  205. this.emit('data', this._buffers[i]);
  206. }
  207. this._buffers = [];
  208. };
  209. FormStream.prototype._emitStream = function (item) {
  210. var self = this;
  211. // item: [ leading, stream ]
  212. var streamSize = 0;
  213. var chunkCount = 0;
  214. const leading = item[0];
  215. self.emit('data', leading);
  216. chunkCount++;
  217. if (debug.enabled) {
  218. debug('new stream, chunk index %d\n%s', chunkCount, leading.toString());
  219. }
  220. var stream = item[1];
  221. stream.on('data', function (data) {
  222. self.emit('data', data);
  223. streamSize += leading.length;
  224. chunkCount++;
  225. if (debug.enabled) {
  226. if (data.length > 512) {
  227. debug('stream chunk, size %d, chunk index %d, stream size %d\n%s...... only show 512 bytes ......',
  228. data.length, chunkCount, streamSize, hex(data.slice(0, 512)));
  229. } else {
  230. debug('stream chunk, size %d, chunk index %d, stream size %d\n%s',
  231. data.length, chunkCount, streamSize, hex(data));
  232. }
  233. }
  234. });
  235. stream.on('end', function () {
  236. self.emit('data', NEW_LINE_BUFFER);
  237. chunkCount++;
  238. debug('stream end, chunk index %d, stream size %d', chunkCount, streamSize);
  239. return process.nextTick(self.drain.bind(self));
  240. });
  241. stream.resume();
  242. };
  243. FormStream.prototype._emitStreamWithChunkSize = function (item, minChunkSize) {
  244. var self = this;
  245. // item: [ leading, stream ]
  246. var streamSize = 0;
  247. var chunkCount = 0;
  248. var bufferSize = 0;
  249. var buffers = [];
  250. const leading = item[0];
  251. buffers.push(leading);
  252. bufferSize += leading.length;
  253. if (debug.enabled) {
  254. debug('new stream, with min chunk size: %d\n%s', minChunkSize, leading.toString());
  255. }
  256. var stream = item[1];
  257. stream.on('data', function (data) {
  258. if (typeof data === 'string') {
  259. data = Buffer.from(data, 'utf-8');
  260. }
  261. buffers.push(data);
  262. bufferSize += data.length;
  263. streamSize += data.length;
  264. debug('got stream data size %d, buffer size %d, stream size %d',
  265. data.length, bufferSize, streamSize);
  266. if (bufferSize >= minChunkSize) {
  267. const chunk = Buffer.concat(buffers, bufferSize);
  268. buffers = [];
  269. bufferSize = 0;
  270. self.emit('data', chunk);
  271. chunkCount++;
  272. if (debug.enabled) {
  273. if (chunk.length > 512) {
  274. debug('stream chunk, size %d, chunk index %d, stream size %d\n%s...... only show 512 bytes ......',
  275. chunk.length, chunkCount, streamSize, hex(chunk.slice(0, 512)));
  276. } else {
  277. debug('stream chunk, size %d, chunk index %d, stream size %d\n%s',
  278. chunk.length, chunkCount, streamSize, hex(chunk));
  279. }
  280. }
  281. }
  282. });
  283. stream.on('end', function () {
  284. buffers.push(NEW_LINE_BUFFER);
  285. bufferSize += NEW_LINE_BUFFER.length;
  286. const chunk = Buffer.concat(buffers, bufferSize);
  287. self.emit('data', chunk);
  288. chunkCount++;
  289. if (chunk.length > 512) {
  290. debug('stream end, size %d, chunk index %d, stream size %d\n%s...... only show 512 bytes ......',
  291. chunk.length, chunkCount, streamSize, hex(chunk.slice(0, 512)));
  292. } else {
  293. debug('stream end, size %d, chunk index %d, stream size %d\n%s',
  294. chunk.length, chunkCount, streamSize, hex(chunk));
  295. }
  296. return process.nextTick(self.drain.bind(self));
  297. });
  298. stream.resume();
  299. };
  300. FormStream.prototype._emitEnd = function () {
  301. // ending format:
  302. //
  303. // --{boundary}--\r\n
  304. this.emit('data', this._endData);
  305. this.emit('end');
  306. if (debug.enabled) {
  307. debug('end boundary\n%s', this._endData.toString());
  308. }
  309. };
  310. FormStream.prototype.drain = function () {
  311. // debug('drain');
  312. this._emitBuffers();
  313. var item = this._streams.shift();
  314. if (item) {
  315. if (this._minChunkSize && this._minChunkSize > 0) {
  316. this._emitStreamWithChunkSize(item, this._minChunkSize);
  317. } else {
  318. this._emitStream(item);
  319. }
  320. } else {
  321. this._emitEnd();
  322. }
  323. return this;
  324. };
  325. FormStream.prototype.resume = function () {
  326. // debug('resume');
  327. this.paused = false;
  328. if (!this._draining) {
  329. this._draining = true;
  330. this.drain();
  331. }
  332. return this;
  333. };
  334. FormStream.prototype.close = FormStream.prototype.destroy = function () {
  335. this.emit('destroy');
  336. // debug('destroy or close');
  337. };