cloudflare/cloudflare-typescript

Public

mirrored from https://github.com/cloudflare/cloudflare-typescriptAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
generated-9f927a4706

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/uploads.ts

248lines · modeblame

2d51afdcstainless-app[bot]2 years ago1import { type RequestOptions } from './core';
2import {
3FormData,
4File,
5type Blob,
6type FilePropertyBag,
7getMultipartRequestOptions,
8type FsReadStream,
9isFsReadStream,
10} from './_shims/index';
11import { MultipartBody } from './_shims/MultipartBody';
12export { fileFromPath } from './_shims/index';
13
14type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | Uint8Array | DataView;
15export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | Uint8Array | DataView;
16
17/**
18* Typically, this is a native "File" class.
19*
20* We provide the {@link toFile} utility to convert a variety of objects
21* into the File class.
22*
23* For convenience, you can also pass a fetch Response, or in Node,
24* the result of fs.createReadStream().
25*/
26export type Uploadable = FileLike | ResponseLike | FsReadStream;
27
28/**
29* Intended to match web.Blob, node.Blob, node-fetch.Blob, etc.
30*/
31export interface BlobLike {
32/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
33readonly size: number;
34/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
35readonly type: string;
36/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
37text(): Promise<string>;
38/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
39slice(start?: number, end?: number): BlobLike;
40// unfortunately @types/node-fetch@^2.6.4 doesn't type the arrayBuffer method
41}
42
43/**
44* Intended to match web.File, node.File, node-fetch.File, etc.
45*/
46export interface FileLike extends BlobLike {
47/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
48readonly lastModified: number;
49/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
50readonly name: string;
51}
52
53/**
54* Intended to match web.Response, node.Response, node-fetch.Response, etc.
55*/
56export interface ResponseLike {
57url: string;
58blob(): Promise<BlobLike>;
59}
60
61export const isResponseLike = (value: any): value is ResponseLike =>
62value != null &&
63typeof value === 'object' &&
64typeof value.url === 'string' &&
65typeof value.blob === 'function';
66
67export const isFileLike = (value: any): value is FileLike =>
68value != null &&
69typeof value === 'object' &&
70typeof value.name === 'string' &&
71typeof value.lastModified === 'number' &&
72isBlobLike(value);
73
74/**
75* The BlobLike type omits arrayBuffer() because @types/node-fetch@^2.6.4 lacks it; but this check
76* adds the arrayBuffer() method type because it is available and used at runtime
77*/
78export const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
79value != null &&
80typeof value === 'object' &&
81typeof value.size === 'number' &&
82typeof value.type === 'string' &&
83typeof value.text === 'function' &&
84typeof value.slice === 'function' &&
85typeof value.arrayBuffer === 'function';
86
87export const isUploadable = (value: any): value is Uploadable => {
88return isFileLike(value) || isResponseLike(value) || isFsReadStream(value);
89};
90
91export type ToFileInput = Uploadable | Exclude<BlobLikePart, string> | AsyncIterable<BlobLikePart>;
92
93/**
94* Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
95* @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
96* @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
97* @param {Object=} options additional properties
98* @param {string=} options.type the MIME type of the content
99* @param {number=} options.lastModified the last modified timestamp
100* @returns a {@link File} with the given properties
101*/
102export async function toFile(
103value: ToFileInput | PromiseLike<ToFileInput>,
104name?: string | null | undefined,
0a9671a4stainless-app[bot]2 years ago105options?: FilePropertyBag | undefined,
2d51afdcstainless-app[bot]2 years ago106): Promise<FileLike> {
107// If it's a promise, resolve it.
108value = await value;
109
0a9671a4stainless-app[bot]2 years ago110// Use the file's options if there isn't one provided
111options ??= isFileLike(value) ? { lastModified: value.lastModified, type: value.type } : {};
112
2d51afdcstainless-app[bot]2 years ago113if (isResponseLike(value)) {
114const blob = await value.blob();
115name ||= new URL(value.url).pathname.split(/[\\/]/).pop() ?? 'unknown_file';
116
117return new File([blob as any], name, options);
118}
119
120const bits = await getBytes(value);
121
122name ||= getName(value) ?? 'unknown_file';
123
124if (!options.type) {
125const type = (bits[0] as any)?.type;
126if (typeof type === 'string') {
127options = { ...options, type };
128}
129}
130
131return new File(bits, name, options);
132}
133
134async function getBytes(value: ToFileInput): Promise<Array<BlobPart>> {
135let parts: Array<BlobPart> = [];
136if (
137typeof value === 'string' ||
138ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
139value instanceof ArrayBuffer
140) {
141parts.push(value);
142} else if (isBlobLike(value)) {
143parts.push(await value.arrayBuffer());
144} else if (
145isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc.
146) {
147for await (const chunk of value) {
148parts.push(chunk as BlobPart); // TODO, consider validating?
149}
150} else {
151throw new Error(
152`Unexpected data type: ${typeof value}; constructor: ${value?.constructor
153?.name}; props: ${propsForError(value)}`,
154);
155}
156
157return parts;
158}
159
160function propsForError(value: any): string {
161const props = Object.getOwnPropertyNames(value);
162return `[${props.map((p) => `"${p}"`).join(', ')}]`;
163}
164
165function getName(value: any): string | undefined {
166return (
167getStringFromMaybeBuffer(value.name) ||
168getStringFromMaybeBuffer(value.filename) ||
169// For fs.ReadStream
170getStringFromMaybeBuffer(value.path)?.split(/[\\/]/).pop()
171);
172}
173
174const getStringFromMaybeBuffer = (x: string | Buffer | unknown): string | undefined => {
175if (typeof x === 'string') return x;
176if (typeof Buffer !== 'undefined' && x instanceof Buffer) return String(x);
177return undefined;
178};
179
180const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator<unknown> =>
181value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
182
183export const isMultipartBody = (body: any): body is MultipartBody =>
184body && typeof body === 'object' && body.body && body[Symbol.toStringTag] === 'MultipartBody';
185
186/**
187* Returns a multipart/form-data request if any part of the given request body contains a File / Blob value.
188* Otherwise returns the request as is.
189*/
190export const maybeMultipartFormRequestOptions = async <T = Record<string, unknown>>(
191opts: RequestOptions<T>,
192): Promise<RequestOptions<T | MultipartBody>> => {
193if (!hasUploadableValue(opts.body)) return opts;
194
195const form = await createForm(opts.body);
196return getMultipartRequestOptions(form, opts);
197};
198
199export const multipartFormRequestOptions = async <T = Record<string, unknown>>(
200opts: RequestOptions<T>,
201): Promise<RequestOptions<T | MultipartBody>> => {
202const form = await createForm(opts.body);
203return getMultipartRequestOptions(form, opts);
204};
205
206export const createForm = async <T = Record<string, unknown>>(body: T | undefined): Promise<FormData> => {
207const form = new FormData();
208await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value)));
209return form;
210};
211
212const hasUploadableValue = (value: unknown): boolean => {
213if (isUploadable(value)) return true;
214if (Array.isArray(value)) return value.some(hasUploadableValue);
215if (value && typeof value === 'object') {
216for (const k in value) {
217if (hasUploadableValue((value as any)[k])) return true;
218}
219}
220return false;
221};
222
223const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
224if (value === undefined) return;
225if (value == null) {
226throw new TypeError(
227`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`,
228);
229}
230
231// TODO: make nested formats configurable
232if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
233form.append(key, String(value));
234} else if (isUploadable(value)) {
235const file = await toFile(value);
236form.append(key, file as File);
237} else if (Array.isArray(value)) {
238await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry)));
239} else if (typeof value === 'object') {
240await Promise.all(
241Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)),
242);
243} else {
244throw new TypeError(
245`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`,
246);
247}
248};