uploads.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { FormData, File, getMultipartRequestOptions, isFsReadStream, } from "./_shims/index.mjs";
  2. export { fileFromPath } from "./_shims/index.mjs";
  3. export const isResponseLike = (value) => value != null &&
  4. typeof value === 'object' &&
  5. typeof value.url === 'string' &&
  6. typeof value.blob === 'function';
  7. export const isFileLike = (value) => value != null &&
  8. typeof value === 'object' &&
  9. typeof value.name === 'string' &&
  10. typeof value.lastModified === 'number' &&
  11. isBlobLike(value);
  12. /**
  13. * The BlobLike type omits arrayBuffer() because @types/node-fetch@^2.6.4 lacks it; but this check
  14. * adds the arrayBuffer() method type because it is available and used at runtime
  15. */
  16. export const isBlobLike = (value) => value != null &&
  17. typeof value === 'object' &&
  18. typeof value.size === 'number' &&
  19. typeof value.type === 'string' &&
  20. typeof value.text === 'function' &&
  21. typeof value.slice === 'function' &&
  22. typeof value.arrayBuffer === 'function';
  23. export const isUploadable = (value) => {
  24. return isFileLike(value) || isResponseLike(value) || isFsReadStream(value);
  25. };
  26. /**
  27. * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
  28. * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
  29. * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
  30. * @param {Object=} options additional properties
  31. * @param {string=} options.type the MIME type of the content
  32. * @param {number=} options.lastModified the last modified timestamp
  33. * @returns a {@link File} with the given properties
  34. */
  35. export async function toFile(value, name, options) {
  36. // If it's a promise, resolve it.
  37. value = await value;
  38. // If we've been given a `File` we don't need to do anything
  39. if (isFileLike(value)) {
  40. return value;
  41. }
  42. if (isResponseLike(value)) {
  43. const blob = await value.blob();
  44. name || (name = new URL(value.url).pathname.split(/[\\/]/).pop() ?? 'unknown_file');
  45. // we need to convert the `Blob` into an array buffer because the `Blob` class
  46. // that `node-fetch` defines is incompatible with the web standard which results
  47. // in `new File` interpreting it as a string instead of binary data.
  48. const data = isBlobLike(blob) ? [(await blob.arrayBuffer())] : [blob];
  49. return new File(data, name, options);
  50. }
  51. const bits = await getBytes(value);
  52. name || (name = getName(value) ?? 'unknown_file');
  53. if (!options?.type) {
  54. const type = bits[0]?.type;
  55. if (typeof type === 'string') {
  56. options = { ...options, type };
  57. }
  58. }
  59. return new File(bits, name, options);
  60. }
  61. async function getBytes(value) {
  62. let parts = [];
  63. if (typeof value === 'string' ||
  64. ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
  65. value instanceof ArrayBuffer) {
  66. parts.push(value);
  67. }
  68. else if (isBlobLike(value)) {
  69. parts.push(await value.arrayBuffer());
  70. }
  71. else if (isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc.
  72. ) {
  73. for await (const chunk of value) {
  74. parts.push(chunk); // TODO, consider validating?
  75. }
  76. }
  77. else {
  78. throw new Error(`Unexpected data type: ${typeof value}; constructor: ${value?.constructor
  79. ?.name}; props: ${propsForError(value)}`);
  80. }
  81. return parts;
  82. }
  83. function propsForError(value) {
  84. const props = Object.getOwnPropertyNames(value);
  85. return `[${props.map((p) => `"${p}"`).join(', ')}]`;
  86. }
  87. function getName(value) {
  88. return (getStringFromMaybeBuffer(value.name) ||
  89. getStringFromMaybeBuffer(value.filename) ||
  90. // For fs.ReadStream
  91. getStringFromMaybeBuffer(value.path)?.split(/[\\/]/).pop());
  92. }
  93. const getStringFromMaybeBuffer = (x) => {
  94. if (typeof x === 'string')
  95. return x;
  96. if (typeof Buffer !== 'undefined' && x instanceof Buffer)
  97. return String(x);
  98. return undefined;
  99. };
  100. const isAsyncIterableIterator = (value) => value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
  101. export const isMultipartBody = (body) => body && typeof body === 'object' && body.body && body[Symbol.toStringTag] === 'MultipartBody';
  102. /**
  103. * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value.
  104. * Otherwise returns the request as is.
  105. */
  106. export const maybeMultipartFormRequestOptions = async (opts) => {
  107. if (!hasUploadableValue(opts.body))
  108. return opts;
  109. const form = await createForm(opts.body);
  110. return getMultipartRequestOptions(form, opts);
  111. };
  112. export const multipartFormRequestOptions = async (opts) => {
  113. const form = await createForm(opts.body);
  114. return getMultipartRequestOptions(form, opts);
  115. };
  116. export const createForm = async (body) => {
  117. const form = new FormData();
  118. await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value)));
  119. return form;
  120. };
  121. const hasUploadableValue = (value) => {
  122. if (isUploadable(value))
  123. return true;
  124. if (Array.isArray(value))
  125. return value.some(hasUploadableValue);
  126. if (value && typeof value === 'object') {
  127. for (const k in value) {
  128. if (hasUploadableValue(value[k]))
  129. return true;
  130. }
  131. }
  132. return false;
  133. };
  134. const addFormValue = async (form, key, value) => {
  135. if (value === undefined)
  136. return;
  137. if (value == null) {
  138. throw new TypeError(`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`);
  139. }
  140. // TODO: make nested formats configurable
  141. if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
  142. form.append(key, String(value));
  143. }
  144. else if (isUploadable(value)) {
  145. const file = await toFile(value);
  146. form.append(key, file);
  147. }
  148. else if (Array.isArray(value)) {
  149. await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry)));
  150. }
  151. else if (typeof value === 'object') {
  152. await Promise.all(Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)));
  153. }
  154. else {
  155. throw new TypeError(`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`);
  156. }
  157. };
  158. //# sourceMappingURL=uploads.mjs.map