formDataToStream.js 2.9 KB

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