๐Ÿ“ฆ kitten / multitars

๐Ÿ“„ multipartOutput.ts ยท 82 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82import { encodeName } from './multipartEncoding';
import {
  streamToIterator,
  BOUNDARY_ID,
  type ReadableStreamLike,
  streamLikeToIterator,
} from './conversions';

const CRLF = '\r\n';
const BOUNDARY_HYPHEN_CHARS = '--';

const FORM_FOOTER =
  BOUNDARY_HYPHEN_CHARS + BOUNDARY_ID + BOUNDARY_HYPHEN_CHARS + CRLF + CRLF;

const isBlob = (value: unknown): value is Blob =>
  typeof value === 'object' &&
  value != null &&
  (value instanceof Blob || 'type' in value);

interface ContentDispositionParams {
  name: string;
  filename?: string;
}

interface FormExtraHeaders {
  'Content-Type'?: string;
  'Content-Length'?: number;
}

const makeFormHeader = (
  params: ContentDispositionParams,
  headers: FormExtraHeaders | undefined
): string => {
  let header = BOUNDARY_HYPHEN_CHARS + BOUNDARY_ID + CRLF;
  header += `Content-Disposition: form-data; name="${encodeName(params.name)}"`;
  if (params.filename != null)
    header += `; filename="${encodeName(params.filename)}"`;
  if (headers?.['Content-Type'])
    header += `${CRLF}Content-Type: ${headers['Content-Type']}`;
  // NOTE(@kitten): When size is zero, we don't send it. Since we're streaming
  // files, some files may not have a known size (See: StreamFile)
  if (headers?.['Content-Length'])
    header += `${CRLF}Content-Length: ${headers['Content-Length']}`;
  header += CRLF;
  header += CRLF;
  return header;
};

type FormValue = string | Uint8Array<ArrayBuffer> | Blob | File;
export type FormEntry = readonly [name: string, value: FormValue];

export const multipartContentType = `multipart/form-data; boundary=${BOUNDARY_ID}`;

export async function* streamMultipart(
  entries: ReadableStreamLike<FormEntry>
): AsyncGenerator<Uint8Array<ArrayBuffer>> {
  const next = streamLikeToIterator(entries);
  const encoder = new TextEncoder();
  let result: Awaited<ReturnType<typeof next>>;
  while (!(result = await next()).done && result.value) {
    const name = result.value[0];
    const value = result.value[1];
    if (isBlob(value)) {
      yield encoder.encode(
        makeFormHeader(
          { name, filename: 'name' in value ? value.name : name },
          {
            'Content-Type': value.type,
            'Content-Length': value.size,
          }
        )
      );
      yield* streamToIterator(value.stream());
    } else {
      yield encoder.encode(makeFormHeader({ name }, undefined));
      yield typeof value === 'string' ? encoder.encode(value) : value;
    }
    yield encoder.encode(CRLF);
  }
  yield encoder.encode(FORM_FOOTER);
}