formDataToStream.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import util from 'util';
  2. import { Readable } from 'stream';
  3. import utils from '../utils.js';
  4. import readBlob from './readBlob.js';
  5. import platform from '../platform/index.js';
  6. const BOUNDARY_ALPHABET = platform.ALPHABET.ALPHA_DIGIT + '-_';
  7. const textEncoder = typeof TextEncoder === 'function' ? new TextEncoder() : new util.TextEncoder();
  8. const CRLF = '\r\n';
  9. const CRLF_BYTES = textEncoder.encode(CRLF);
  10. const CRLF_BYTES_COUNT = 2;
  11. class FormDataPart {
  12. constructor(name, value) {
  13. const { escapeName } = this.constructor;
  14. const isStringValue = utils.isString(value);
  15. let headers = `Content-Disposition: form-data; name="${escapeName(name)}"${
  16. !isStringValue && value.name ? `; filename="${escapeName(value.name)}"` : ''
  17. }${CRLF}`;
  18. if (isStringValue) {
  19. value = textEncoder.encode(String(value).replace(/\r?\n|\r\n?/g, CRLF));
  20. } else {
  21. const safeType = String(value.type || 'application/octet-stream').replace(/[\r\n]/g, '');
  22. headers += `Content-Type: ${safeType}${CRLF}`;
  23. }
  24. this.headers = textEncoder.encode(headers + CRLF);
  25. this.contentLength = isStringValue ? value.byteLength : value.size;
  26. this.size = this.headers.byteLength + this.contentLength + CRLF_BYTES_COUNT;
  27. this.name = name;
  28. this.value = value;
  29. }
  30. async *encode() {
  31. yield this.headers;
  32. const { value } = this;
  33. if (utils.isTypedArray(value)) {
  34. yield value;
  35. } else {
  36. yield* readBlob(value);
  37. }
  38. yield CRLF_BYTES;
  39. }
  40. static escapeName(name) {
  41. return String(name).replace(
  42. /[\r\n"]/g,
  43. (match) =>
  44. ({
  45. '\r': '%0D',
  46. '\n': '%0A',
  47. '"': '%22',
  48. })[match]
  49. );
  50. }
  51. }
  52. const formDataToStream = (form, headersHandler, options) => {
  53. const {
  54. tag = 'form-data-boundary',
  55. size = 25,
  56. boundary = tag + '-' + platform.generateString(size, BOUNDARY_ALPHABET),
  57. } = options || {};
  58. if (!utils.isFormData(form)) {
  59. throw TypeError('FormData instance required');
  60. }
  61. if (boundary.length < 1 || boundary.length > 70) {
  62. throw Error('boundary must be 10-70 characters long');
  63. }
  64. const boundaryBytes = textEncoder.encode('--' + boundary + CRLF);
  65. const footerBytes = textEncoder.encode('--' + boundary + '--' + CRLF);
  66. let contentLength = footerBytes.byteLength;
  67. const parts = Array.from(form.entries()).map(([name, value]) => {
  68. const part = new FormDataPart(name, value);
  69. contentLength += part.size;
  70. return part;
  71. });
  72. contentLength += boundaryBytes.byteLength * parts.length;
  73. contentLength = utils.toFiniteNumber(contentLength);
  74. const computedHeaders = {
  75. 'Content-Type': `multipart/form-data; boundary=${boundary}`,
  76. };
  77. if (Number.isFinite(contentLength)) {
  78. computedHeaders['Content-Length'] = contentLength;
  79. }
  80. headersHandler && headersHandler(computedHeaders);
  81. return Readable.from(
  82. (async function* () {
  83. for (const part of parts) {
  84. yield boundaryBytes;
  85. yield* part.encode();
  86. }
  87. yield footerBytes;
  88. })()
  89. );
  90. };
  91. export default formDataToStream;