formstream.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * Data format:
  3. *
  4. --FormStreamBoundary1349886663601\r\n
  5. Content-Disposition: form-data; name="foo"\r\n
  6. \r\n
  7. <FIELD-CONTENT>\r\n
  8. --FormStreamBoundary1349886663601\r\n
  9. Content-Disposition: form-data; name="file"; filename="formstream.test.js"\r\n
  10. Content-Type: application/javascript\r\n
  11. \r\n
  12. <FILE-CONTENT>\r\n
  13. --FormStreamBoundary1349886663601\r\n
  14. Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n
  15. Content-Type: image/png\r\n
  16. \r\n
  17. <IMAGE-CONTENT>\r\n
  18. --FormStreamBoundary1349886663601--
  19. *
  20. */
  21. 'use strict';
  22. var Stream = require('stream');
  23. var parseStream = require('pause-stream');
  24. var util = require('util');
  25. var mime = require('mime');
  26. var path = require('path');
  27. var fs = require('fs');
  28. var destroy = require('destroy');
  29. var PADDING = '--';
  30. var NEW_LINE = '\r\n';
  31. var NEW_LINE_BUFFER = new Buffer(NEW_LINE);
  32. function FormStream() {
  33. if (!(this instanceof FormStream)) {
  34. return new FormStream();
  35. }
  36. FormStream.super_.call(this);
  37. this._boundary = this._generateBoundary();
  38. this._streams = [];
  39. this._buffers = [];
  40. this._endData = new Buffer(PADDING + this._boundary + PADDING + NEW_LINE);
  41. this._contentLength = 0;
  42. this._isAllStreamSizeKnown = true;
  43. this._knownStreamSize = 0;
  44. }
  45. util.inherits(FormStream, Stream);
  46. module.exports = FormStream;
  47. FormStream.prototype._generateBoundary = function() {
  48. // https://github.com/felixge/node-form-data/blob/master/lib/form_data.js#L162
  49. // This generates a 50 character boundary similar to those used by Firefox.
  50. // They are optimized for boyer-moore parsing.
  51. var boundary = '--------------------------';
  52. for (var i = 0; i < 24; i++) {
  53. boundary += Math.floor(Math.random() * 10).toString(16);
  54. }
  55. return boundary;
  56. };
  57. FormStream.prototype.setTotalStreamSize = function (size) {
  58. // this method should not make any sense if the length of each stream is known.
  59. if (this._isAllStreamSizeKnown) {
  60. return this;
  61. }
  62. size = size || 0;
  63. for (var i = 0; i < this._streams.length; i++) {
  64. size += this._streams[i][0].length;
  65. size += NEW_LINE_BUFFER.length; // stream field end pedding size
  66. }
  67. this._knownStreamSize = size;
  68. this._isAllStreamSizeKnown = true;
  69. return this;
  70. };
  71. FormStream.prototype.headers = function (options) {
  72. var headers = {
  73. 'Content-Type': 'multipart/form-data; boundary=' + this._boundary
  74. };
  75. // calculate total stream size
  76. this._contentLength += this._knownStreamSize;
  77. // calculate length of end padding
  78. this._contentLength += this._endData.length;
  79. if (this._isAllStreamSizeKnown) {
  80. headers['Content-Length'] = String(this._contentLength);
  81. }
  82. if (options) {
  83. for (var k in options) {
  84. headers[k] = options[k];
  85. }
  86. }
  87. return headers;
  88. };
  89. FormStream.prototype.file = function (name, filepath, filename, filesize) {
  90. var mimeType = mime.lookup(filepath);
  91. if (typeof filename === 'number' && !filesize) {
  92. filesize = filename;
  93. filename = path.basename(filepath);
  94. } else if (!filename) {
  95. filename = path.basename(filepath);
  96. }
  97. var stream = fs.createReadStream(filepath);
  98. return this.stream(name, stream, filename, mimeType, filesize);
  99. };
  100. /**
  101. * Add a form field
  102. * @param {String} name field name
  103. * @param {String|Buffer} value field value
  104. * @return {this}
  105. */
  106. FormStream.prototype.field = function (name, value) {
  107. if (!Buffer.isBuffer(value)) {
  108. // field(String, Number)
  109. // https://github.com/qiniu/nodejs-sdk/issues/123
  110. if (typeof value === 'number') {
  111. value = String(value);
  112. }
  113. value = new Buffer(value);
  114. }
  115. return this.buffer(name, value);
  116. };
  117. FormStream.prototype.stream = function (name, stream, filename, mimeType, size) {
  118. if (typeof mimeType === 'number' && !size) {
  119. size = mimeType;
  120. mimeType = mime.lookup(filename);
  121. } else if (!mimeType) {
  122. mimeType = mime.lookup(filename);
  123. }
  124. stream.once('error', this.emit.bind(this, 'error'));
  125. // if form stream destroy, also destroy the source stream
  126. this.once('destroy', function () {
  127. destroy(stream);
  128. });
  129. var leading = this._leading({ name: name, filename: filename }, mimeType);
  130. var ps = parseStream().pause();
  131. stream.pipe(ps);
  132. this._streams.push([leading, ps]);
  133. // if the size of this stream is known, plus the total content-length;
  134. // otherwise, content-length is unknown.
  135. if (typeof size === 'number') {
  136. this._knownStreamSize += leading.length;
  137. this._knownStreamSize += size;
  138. this._knownStreamSize += NEW_LINE_BUFFER.length;
  139. } else {
  140. this._isAllStreamSizeKnown = false;
  141. }
  142. process.nextTick(this.resume.bind(this));
  143. return this;
  144. };
  145. FormStream.prototype.buffer = function (name, buffer, filename, mimeType) {
  146. if (filename && !mimeType) {
  147. mimeType = mime.lookup(filename);
  148. }
  149. var disposition = { name: name };
  150. if (filename) {
  151. disposition.filename = filename;
  152. }
  153. var leading = this._leading(disposition, mimeType);
  154. this._buffers.push([leading, buffer]);
  155. // plus buffer length to total content-length
  156. this._contentLength += leading.length;
  157. this._contentLength += buffer.length;
  158. this._contentLength += NEW_LINE_BUFFER.length;
  159. process.nextTick(this.resume.bind(this));
  160. return this;
  161. };
  162. FormStream.prototype._leading = function (disposition, type) {
  163. var leading = [PADDING + this._boundary];
  164. var disps = [];
  165. if (disposition) {
  166. for (var k in disposition) {
  167. disps.push(k + '="' + disposition[k] + '"');
  168. }
  169. }
  170. leading.push('Content-Disposition: form-data; ' + disps.join('; '));
  171. if (type) {
  172. leading.push('Content-Type: ' + type);
  173. }
  174. leading.push('');
  175. leading.push('');
  176. return new Buffer(leading.join(NEW_LINE));
  177. };
  178. FormStream.prototype._emitBuffers = function () {
  179. if (!this._buffers.length) {
  180. return;
  181. }
  182. for (var i = 0; i < this._buffers.length; i++) {
  183. var item = this._buffers[i];
  184. this.emit('data', item[0]); // part leading
  185. this.emit('data', item[1]); // part content
  186. this.emit('data', NEW_LINE_BUFFER);
  187. }
  188. this._buffers = [];
  189. };
  190. FormStream.prototype._emitStream = function (item) {
  191. var self = this;
  192. // item: [ fieldData, stream ]
  193. self.emit('data', item[0]);
  194. var stream = item[1];
  195. stream.on('data', function (data) {
  196. self.emit('data', data);
  197. });
  198. stream.on('end', function () {
  199. self.emit('data', NEW_LINE_BUFFER);
  200. return process.nextTick(self.drain.bind(self));
  201. });
  202. stream.resume();
  203. };
  204. FormStream.prototype._emitEnd = function () {
  205. // ending format:
  206. //
  207. // --{boundary}--\r\n
  208. this.emit('data', this._endData);
  209. this.emit('end');
  210. };
  211. FormStream.prototype.drain = function () {
  212. this._emitBuffers();
  213. var item = this._streams.shift();
  214. if (item) {
  215. this._emitStream(item);
  216. } else {
  217. this._emitEnd();
  218. }
  219. return this;
  220. };
  221. FormStream.prototype.resume = function () {
  222. this.paused = false;
  223. if (!this._draining) {
  224. this._draining = true;
  225. this.drain();
  226. }
  227. return this;
  228. };
  229. FormStream.prototype.close = FormStream.prototype.destroy = function () {
  230. this.emit('destroy');
  231. };