formDataToStream.js 3.0 KB

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