租房小程序前端代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

399 lines
11 KiB

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