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.

112 lines
2.9 KiB

1 month ago
  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(/[\r\n"]/g, (match) => ({
  41. '\r' : '%0D',
  42. '\n' : '%0A',
  43. '"' : '%22',
  44. }[match]));
  45. }
  46. }
  47. const formDataToStream = (form, headersHandler, options) => {
  48. const {
  49. tag = 'form-data-boundary',
  50. size = 25,
  51. boundary = tag + '-' + platform.generateString(size, BOUNDARY_ALPHABET)
  52. } = options || {};
  53. if(!utils.isFormData(form)) {
  54. throw TypeError('FormData instance required');
  55. }
  56. if (boundary.length < 1 || boundary.length > 70) {
  57. throw Error('boundary must be 10-70 characters long')
  58. }
  59. const boundaryBytes = textEncoder.encode('--' + boundary + CRLF);
  60. const footerBytes = textEncoder.encode('--' + boundary + '--' + CRLF + CRLF);
  61. let contentLength = footerBytes.byteLength;
  62. const parts = Array.from(form.entries()).map(([name, value]) => {
  63. const part = new FormDataPart(name, value);
  64. contentLength += part.size;
  65. return part;
  66. });
  67. contentLength += boundaryBytes.byteLength * parts.length;
  68. contentLength = utils.toFiniteNumber(contentLength);
  69. const computedHeaders = {
  70. 'Content-Type': `multipart/form-data; boundary=${boundary}`
  71. }
  72. if (Number.isFinite(contentLength)) {
  73. computedHeaders['Content-Length'] = contentLength;
  74. }
  75. headersHandler && headersHandler(computedHeaders);
  76. return Readable.from((async function *() {
  77. for(const part of parts) {
  78. yield boundaryBytes;
  79. yield* part.encode();
  80. }
  81. yield footerBytes;
  82. })());
  83. };
  84. export default formDataToStream;