s3mini 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/S3.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  extractErrCode,
16
16
  S3NetworkError,
17
17
  S3ServiceError,
18
+ generateParts,
19
+ toUint8Array,
18
20
  } from './utils.js';
19
21
  import type * as IT from './types.js';
20
22
 
@@ -55,6 +57,7 @@ class S3mini {
55
57
  * @param {number} [config.requestAbortTimeout=undefined] - The timeout in milliseconds after which a request should be aborted (careful on streamed requests).
56
58
  * @param {Object} [config.logger=null] - A logger object with methods like info, warn, error.
57
59
  * @param {typeof fetch} [config.fetch=globalThis.fetch] - Custom fetch implementation to use for HTTP requests.
60
+ * @param {number} [config.minPartSize=8388608] - The minimum part size for multipart uploads in bytes (default is 8MB).
58
61
  * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
59
62
  */
60
63
  readonly #accessKeyId: string;
@@ -66,6 +69,7 @@ class S3mini {
66
69
  readonly requestAbortTimeout?: number;
67
70
  readonly logger?: IT.Logger;
68
71
  readonly _fetch: typeof fetch;
72
+ readonly minPartSize: number;
69
73
  private signingKeyDate?: string;
70
74
  private signingKey?: ArrayBuffer;
71
75
 
@@ -78,6 +82,7 @@ class S3mini {
78
82
  requestAbortTimeout = undefined,
79
83
  logger = undefined,
80
84
  fetch = globalThis.fetch,
85
+ minPartSize = C.MIN_PART_SIZE,
81
86
  }: IT.S3Config) {
82
87
  this._validateConstructorParams(accessKeyId, secretAccessKey, endpoint);
83
88
  this.#accessKeyId = accessKeyId;
@@ -89,6 +94,7 @@ class S3mini {
89
94
  this.requestAbortTimeout = requestAbortTimeout;
90
95
  this.logger = logger;
91
96
  this._fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => fetch(input, init);
97
+ this.minPartSize = minPartSize;
92
98
  }
93
99
 
94
100
  private _sanitize(obj: unknown): unknown {
@@ -144,10 +150,10 @@ class S3mini {
144
150
  }
145
151
 
146
152
  private _validateConstructorParams(accessKeyId: string, secretAccessKey: string, endpoint: string): void {
147
- if (typeof accessKeyId !== 'string' || accessKeyId.trim().length === 0) {
153
+ if (typeof accessKeyId !== 'string') {
148
154
  throw new TypeError(C.ERROR_ACCESS_KEY_REQUIRED);
149
155
  }
150
- if (typeof secretAccessKey !== 'string' || secretAccessKey.trim().length === 0) {
156
+ if (typeof secretAccessKey !== 'string') {
151
157
  throw new TypeError(C.ERROR_SECRET_KEY_REQUIRED);
152
158
  }
153
159
  if (typeof endpoint !== 'string' || endpoint.trim().length === 0) {
@@ -155,6 +161,14 @@ class S3mini {
155
161
  }
156
162
  }
157
163
 
164
+ /**
165
+ * Check if credentials are configured (non-empty).
166
+ * @returns true if both accessKeyId and secretAccessKey are non-empty.
167
+ */
168
+ private _hasCredentials(): boolean {
169
+ return this.#accessKeyId.trim().length > 0 && this.#secretAccessKey.trim().length > 0;
170
+ }
171
+
158
172
  private _ensureValidUrl(raw: string): string {
159
173
  const candidate = /^(https?:)?\/\//i.test(raw) ? raw : `https://${raw}`;
160
174
  try {
@@ -233,18 +247,24 @@ class S3mini {
233
247
  return { filteredOpts, conditionalHeaders };
234
248
  }
235
249
 
236
- private _validateData(data: unknown): BodyInit {
237
- if (!((globalThis.Buffer && data instanceof globalThis.Buffer) || typeof data === 'string')) {
238
- this._log('error', C.ERROR_DATA_BUFFER_REQUIRED);
239
- throw new TypeError(C.ERROR_DATA_BUFFER_REQUIRED);
240
- }
241
- return data;
242
- }
250
+ // private _validateData(data: unknown): BodyInit {
251
+ // if (data instanceof ArrayBuffer) {
252
+ // return data;
253
+ // }
254
+ // if (data instanceof Uint8Array) {
255
+ // return data as unknown as BodyInit;
256
+ // }
257
+ // if ((globalThis.Buffer && data instanceof globalThis.Buffer) || typeof data === 'string') {
258
+ // return data as BodyInit;
259
+ // }
260
+ // this._log('error', C.ERROR_DATA_BUFFER_REQUIRED);
261
+ // throw new TypeError(C.ERROR_DATA_BUFFER_REQUIRED);
262
+ // }
243
263
 
244
264
  private _validateUploadPartParams(
245
265
  key: string,
246
266
  uploadId: string,
247
- data: IT.MaybeBuffer | string,
267
+ data: IT.DataInput,
248
268
  partNumber: number,
249
269
  opts: object,
250
270
  ): BodyInit {
@@ -258,7 +278,7 @@ class S3mini {
258
278
  throw new TypeError(`${C.ERROR_PREFIX}partNumber must be a positive integer`);
259
279
  }
260
280
  this._checkOpts(opts);
261
- return this._validateData(data);
281
+ return data as BodyInit;
262
282
  }
263
283
 
264
284
  private async _sign(
@@ -276,6 +296,12 @@ class S3mini {
276
296
  url.pathname === '/' ? `/${keyPath.replace(/^\/+/, '')}` : `${url.pathname}/${keyPath.replace(/^\/+/, '')}`;
277
297
  }
278
298
 
299
+ // If no credentials, return unsigned request (for public bucket access)
300
+ if (!this._hasCredentials()) {
301
+ headers[C.HEADER_HOST] = url.host;
302
+ return { url: url.toString(), headers };
303
+ }
304
+
279
305
  const d = new Date();
280
306
  const year = d.getUTCFullYear();
281
307
  const month = String(d.getUTCMonth() + 1).padStart(2, '0');
@@ -354,11 +380,8 @@ class S3mini {
354
380
  if (Object.keys(query).length > 0) {
355
381
  withQuery = true; // append query string to signed URL
356
382
  }
357
- const filteredOptsStrings = Object.fromEntries(
358
- Object.entries(filteredOpts).map(([k, v]) => [k, String(v)]),
359
- ) as Record<string, string>;
360
383
  const finalUrl =
361
- withQuery && Object.keys(filteredOpts).length ? `${url}?${new URLSearchParams(filteredOptsStrings)}` : url;
384
+ withQuery && Object.keys(filteredOpts).length ? `${url}?${this._buildCanonicalQueryString(filteredOpts)}` : url;
362
385
  const signedHeadersString = Object.fromEntries(
363
386
  Object.entries(signedHeaders).map(([k, v]) => [k, String(v)]),
364
387
  ) as Record<string, string>;
@@ -958,7 +981,7 @@ class S3mini {
958
981
  /**
959
982
  * Uploads an object to the S3-compatible service.
960
983
  * @param {string} key - The key/path where the object will be stored.
961
- * @param {string | Buffer} data - The data to upload (string or Buffer).
984
+ * @param {string | IT.MaybeBuffer | ReadableStream | File | Blob} data - The data to upload (string or Buffer).
962
985
  * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
963
986
  * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
964
987
  * @param {IT.AWSHeaders} [additionalHeaders] - Additional x-amz-* headers specific to this request, if any.
@@ -974,15 +997,17 @@ class S3mini {
974
997
  */
975
998
  public async putObject(
976
999
  key: string,
977
- data: string | IT.MaybeBuffer,
1000
+ data: string | IT.DataInput | ReadableStream | File | Blob,
978
1001
  fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
979
1002
  ssecHeaders?: IT.SSECHeaders,
980
1003
  additionalHeaders?: IT.AWSHeaders,
1004
+ contentLength?: number,
981
1005
  ): Promise<Response> {
1006
+ const size = contentLength ?? getByteSize(data);
982
1007
  return this._signedRequest('PUT', key, {
983
- body: this._validateData(data),
1008
+ body: data as BodyInit,
984
1009
  headers: {
985
- [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
1010
+ ...(size && { [C.HEADER_CONTENT_LENGTH]: size }),
986
1011
  [C.HEADER_CONTENT_TYPE]: fileType,
987
1012
  ...additionalHeaders,
988
1013
  ...ssecHeaders,
@@ -991,6 +1016,235 @@ class S3mini {
991
1016
  });
992
1017
  }
993
1018
 
1019
+ /**
1020
+ * Put object that automatically chooses single PUT vs multipart.
1021
+ * Same signature/shape as putObject so callers don't need to change.
1022
+ * @param {string} key - The key/path where the object will be stored.
1023
+ * @param {string | IT.MaybeBuffer | ReadableStream | File | Blob} data - The data to upload (string or Buffer).
1024
+ * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
1025
+ * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
1026
+ * @param {IT.AWSHeaders} [additionalHeaders] - Additional x-amz-* headers specific to this request, if any.
1027
+ * @param {number} [contentLength] - Optional known content length of data.
1028
+ * @returns {Promise<Response | { ok: boolean; status: number; headers: Map<string, string> }>} A promise that resolves to the Response object from the upload request.
1029
+ * @throws {TypeError} If data is not a string or Buffer.
1030
+ * @example
1031
+ * // Upload text file
1032
+ * await s3.putAnyObject('hello.txt', 'Hello, World!', 'text/plain');
1033
+ *
1034
+ * // Upload binary data
1035
+ * const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
1036
+ * await s3.putAnyObject('image.png', buffer, 'image/png');
1037
+ */
1038
+ public async putAnyObject(
1039
+ key: string,
1040
+ data: IT.DataInput,
1041
+ fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
1042
+ ssecHeaders?: IT.SSECHeaders,
1043
+ additionalHeaders?: IT.AWSHeaders,
1044
+ contentLength?: number,
1045
+ ): Promise<Response | { ok: boolean; status: number; headers: Map<string, string> }> {
1046
+ const size = contentLength ?? getByteSize(data);
1047
+
1048
+ // Single PUT for small files
1049
+ if (!Number.isNaN(size) && size <= this.minPartSize) {
1050
+ return this.putObject(key, data, fileType, ssecHeaders, additionalHeaders, contentLength);
1051
+ }
1052
+
1053
+ this._checkKey(key);
1054
+ return this._multipartUpload(key, data, fileType, ssecHeaders, additionalHeaders);
1055
+ }
1056
+
1057
+ private async _multipartUpload(
1058
+ key: string,
1059
+ data: IT.DataInput,
1060
+ fileType: string,
1061
+ ssecHeaders?: IT.SSECHeaders,
1062
+ additionalHeaders?: IT.AWSHeaders,
1063
+ ): Promise<Response | { ok: boolean; status: number; headers: Map<string, string> }> {
1064
+ const uploadId = await this.getMultipartUploadId(key, fileType, ssecHeaders, additionalHeaders);
1065
+
1066
+ try {
1067
+ const parts = await this._uploadPartsOptimized(key, uploadId, data, ssecHeaders, additionalHeaders);
1068
+ parts.sort((a, b) => a.partNumber - b.partNumber);
1069
+ const result = await this.completeMultipartUpload(key, uploadId, parts);
1070
+ return this._createSuccessResponse(result.etag || '');
1071
+ } catch (err) {
1072
+ await this._safeAbortUpload(key, uploadId);
1073
+ throw err;
1074
+ }
1075
+ }
1076
+
1077
+ private async _uploadKnownSizePartsParallel(
1078
+ key: string,
1079
+ uploadId: string,
1080
+ data: Uint8Array | Blob,
1081
+ ssecHeaders?: IT.SSECHeaders,
1082
+ additionalHeaders?: IT.AWSHeaders,
1083
+ concurrency: number = 4,
1084
+ maxRetries: number = 3,
1085
+ ): Promise<IT.UploadPart[]> {
1086
+ const partSize = this.minPartSize;
1087
+ const totalSize = data instanceof Blob ? data.size : data.byteLength;
1088
+ const totalParts = Math.ceil(totalSize / partSize);
1089
+ const results: IT.UploadPart[] = new Array(totalParts) as IT.UploadPart[];
1090
+ let nextIndex = 0;
1091
+
1092
+ const worker = async (): Promise<void> => {
1093
+ while (true) {
1094
+ const index = nextIndex++;
1095
+ if (index >= totalParts) {
1096
+ return;
1097
+ }
1098
+
1099
+ const start = index * partSize;
1100
+ const end = Math.min(start + partSize, totalSize);
1101
+ const part =
1102
+ data instanceof Blob
1103
+ ? await data.slice(start, end).arrayBuffer() // Must await - R2 needs actual bytes
1104
+ : data.subarray(start, end);
1105
+
1106
+ results[index] = await this._uploadPartWithRetry(
1107
+ key,
1108
+ uploadId,
1109
+ part,
1110
+ index + 1,
1111
+ ssecHeaders,
1112
+ additionalHeaders,
1113
+ maxRetries,
1114
+ );
1115
+ }
1116
+ };
1117
+
1118
+ await Promise.all(Array.from({ length: Math.min(concurrency, totalParts) }, () => worker()));
1119
+ return results;
1120
+ }
1121
+
1122
+ private async _uploadPartsOptimized(
1123
+ key: string,
1124
+ uploadId: string,
1125
+ data: IT.DataInput,
1126
+ ssecHeaders?: IT.SSECHeaders,
1127
+ additionalHeaders?: IT.AWSHeaders,
1128
+ concurrency: number = 4,
1129
+ maxRetries: number = 3,
1130
+ ): Promise<IT.UploadPart[]> {
1131
+ const bytes = toUint8Array(data);
1132
+ if (bytes) {
1133
+ return this._uploadKnownSizePartsParallel(
1134
+ key,
1135
+ uploadId,
1136
+ bytes,
1137
+ ssecHeaders,
1138
+ additionalHeaders,
1139
+ concurrency,
1140
+ maxRetries,
1141
+ );
1142
+ }
1143
+ if (data instanceof Blob) {
1144
+ return this._uploadKnownSizePartsParallel(
1145
+ key,
1146
+ uploadId,
1147
+ data,
1148
+ ssecHeaders,
1149
+ additionalHeaders,
1150
+ concurrency,
1151
+ maxRetries,
1152
+ );
1153
+ }
1154
+ return this._uploadStreamingParts(
1155
+ key,
1156
+ uploadId,
1157
+ data as ReadableStream,
1158
+ ssecHeaders,
1159
+ additionalHeaders,
1160
+ concurrency,
1161
+ maxRetries,
1162
+ );
1163
+ }
1164
+
1165
+ private async _uploadStreamingParts(
1166
+ key: string,
1167
+ uploadId: string,
1168
+ stream: ReadableStream,
1169
+ ssecHeaders?: IT.SSECHeaders,
1170
+ additionalHeaders?: IT.AWSHeaders,
1171
+ concurrency: number = 4,
1172
+ maxRetries: number = 3,
1173
+ ): Promise<IT.UploadPart[]> {
1174
+ const parts: IT.UploadPart[] = [];
1175
+ const active = new Set<Promise<void>>();
1176
+ let partNumber = 0;
1177
+
1178
+ for await (const partData of generateParts(stream, this.minPartSize)) {
1179
+ const currentPartNumber = ++partNumber;
1180
+
1181
+ while (active.size >= concurrency) {
1182
+ await Promise.race(active);
1183
+ }
1184
+
1185
+ const p = this._uploadPartWithRetry(
1186
+ key,
1187
+ uploadId,
1188
+ partData,
1189
+ currentPartNumber,
1190
+ ssecHeaders,
1191
+ additionalHeaders,
1192
+ maxRetries,
1193
+ ).then(part => {
1194
+ parts.push(part);
1195
+ active.delete(p);
1196
+ });
1197
+
1198
+ active.add(p);
1199
+ }
1200
+
1201
+ await Promise.all(active);
1202
+ return parts;
1203
+ }
1204
+
1205
+ private async _uploadPartWithRetry(
1206
+ key: string,
1207
+ uploadId: string,
1208
+ data: IT.PartData,
1209
+ partNumber: number,
1210
+ ssecHeaders?: IT.SSECHeaders,
1211
+ additionalHeaders?: IT.AWSHeaders,
1212
+ maxRetries: number = 3,
1213
+ ): Promise<IT.UploadPart> {
1214
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1215
+ try {
1216
+ return await this.uploadPart(key, uploadId, data, partNumber, {}, ssecHeaders, additionalHeaders);
1217
+ } catch (err) {
1218
+ if (attempt === maxRetries) {
1219
+ throw err;
1220
+ }
1221
+ await new Promise(r => setTimeout(r, Math.min(1000 * 2 ** attempt, 10000)));
1222
+ }
1223
+ }
1224
+ throw new Error('Unreachable');
1225
+ }
1226
+
1227
+ private async _safeAbortUpload(key: string, uploadId: string): Promise<void> {
1228
+ try {
1229
+ await this.abortMultipartUpload(key, uploadId);
1230
+ } catch (err) {
1231
+ this._log('warn', `Failed to abort multipart upload: ${String(err)}`);
1232
+ }
1233
+ }
1234
+
1235
+ private _createSuccessResponse(
1236
+ etag: string,
1237
+ ): Response | { ok: boolean; status: number; headers: Map<string, string> } {
1238
+ if (typeof Response !== 'undefined') {
1239
+ const headers = new Headers();
1240
+ if (etag) {
1241
+ headers.set('ETag', etag);
1242
+ }
1243
+ return new Response('', { status: 200, headers });
1244
+ }
1245
+ return { ok: true, status: 200, headers: new Map([['ETag', etag]]) };
1246
+ }
1247
+
994
1248
  /**
995
1249
  * Initiates a multipart upload and returns the upload ID.
996
1250
  * @param {string} key - The key/path where the object will be stored.
@@ -1007,13 +1261,14 @@ class S3mini {
1007
1261
  key: string,
1008
1262
  fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
1009
1263
  ssecHeaders?: IT.SSECHeaders,
1264
+ additionalHeaders?: IT.AWSHeaders,
1010
1265
  ): Promise<string> {
1011
1266
  this._checkKey(key);
1012
1267
  if (typeof fileType !== 'string') {
1013
1268
  throw new TypeError(`${C.ERROR_PREFIX}fileType must be a string`);
1014
1269
  }
1015
1270
  const query = { uploads: '' };
1016
- const headers = { [C.HEADER_CONTENT_TYPE]: fileType, ...ssecHeaders };
1271
+ const headers = { [C.HEADER_CONTENT_TYPE]: fileType, ...ssecHeaders, ...additionalHeaders };
1017
1272
 
1018
1273
  const res = await this._signedRequest('POST', key, {
1019
1274
  query,
@@ -1045,7 +1300,7 @@ class S3mini {
1045
1300
  * Uploads a part in a multipart upload.
1046
1301
  * @param {string} key - The key of the object being uploaded.
1047
1302
  * @param {string} uploadId - The upload ID from getMultipartUploadId.
1048
- * @param {Buffer | string} data - The data for this part.
1303
+ * @param {string | IT.MaybeBuffer | ReadableStream | File | Blob} data - The data for this part.
1049
1304
  * @param {number} partNumber - The part number (must be between 1 and 10,000).
1050
1305
  * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
1051
1306
  * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
@@ -1063,20 +1318,23 @@ class S3mini {
1063
1318
  public async uploadPart(
1064
1319
  key: string,
1065
1320
  uploadId: string,
1066
- data: IT.MaybeBuffer | string,
1321
+ data: IT.DataInput,
1067
1322
  partNumber: number,
1068
1323
  opts: Record<string, unknown> = {},
1069
1324
  ssecHeaders?: IT.SSECHeaders,
1325
+ additionalHeaders?: IT.AWSHeaders,
1070
1326
  ): Promise<IT.UploadPart> {
1071
1327
  const body = this._validateUploadPartParams(key, uploadId, data, partNumber, opts);
1072
1328
 
1073
1329
  const query = { uploadId, partNumber, ...opts };
1330
+ const size = getByteSize(data);
1074
1331
  const res = await this._signedRequest('PUT', key, {
1075
1332
  query,
1076
1333
  body,
1077
1334
  headers: {
1078
- [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
1335
+ ...(size && !Number.isNaN(size) && { [C.HEADER_CONTENT_LENGTH]: size }),
1079
1336
  ...ssecHeaders,
1337
+ ...additionalHeaders,
1080
1338
  },
1081
1339
  });
1082
1340
 
package/src/consts.ts CHANGED
@@ -8,9 +8,10 @@ export const DEFAULT_STREAM_CONTENT_TYPE = 'application/octet-stream';
8
8
  export const XML_CONTENT_TYPE = 'application/xml';
9
9
  export const JSON_CONTENT_TYPE = 'application/json';
10
10
  // List of keys that might contain sensitive information
11
- export const SENSITIVE_KEYS_REDACTED = new Set(['accessKeyId', 'secretAccessKey', 'sessionToken', 'password', 'token']);
11
+ export const SENSITIVE_KEYS_REDACTED = new Set(['accesskeyid', 'secretaccesskey', 'sessiontoken', 'password', 'token']);
12
12
  export const IFHEADERS = new Set(['if-match', 'if-none-match', 'if-modified-since', 'if-unmodified-since']);
13
13
  export const DEFAULT_REQUEST_SIZE_IN_BYTES = 8 * 1024 * 1024;
14
+ export const MIN_PART_SIZE = 8 * 1024 * 1024;
14
15
 
15
16
  // Headers
16
17
  export const HEADER_AMZ_CONTENT_SHA256 = 'x-amz-content-sha256';
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  // Export the S3 class as default export and named export
4
4
  export { S3mini } from './S3.js';
5
+ export { S3mini as default } from './S3.js';
6
+
5
7
  export { sanitizeETag, runInBatches } from './utils.js';
6
8
 
7
9
  // Re-export types
package/src/types.ts CHANGED
@@ -7,8 +7,11 @@ export interface S3Config {
7
7
  requestAbortTimeout?: number;
8
8
  logger?: Logger;
9
9
  fetch?: typeof fetch;
10
+ minPartSize?: number;
10
11
  }
11
12
 
13
+ export type PartData = Uint8Array | Blob | ArrayBuffer;
14
+
12
15
  export interface SSECHeaders {
13
16
  'x-amz-server-side-encryption-customer-algorithm': string;
14
17
  'x-amz-server-side-encryption-customer-key': string;
@@ -51,11 +54,14 @@ interface ListBucketResult {
51
54
  keyCount: string;
52
55
  contents?: Array<Record<string, unknown>>;
53
56
  }
54
- interface ListBucketError {
55
- error: { code: string; message: string };
57
+ export interface ListBucketError {
58
+ error: {
59
+ code: string;
60
+ message: string;
61
+ };
56
62
  }
57
63
 
58
- export type ListBucketResponse = { listBucketResult: ListBucketResult } | { error: ListBucketError };
64
+ export type ListBucketResponse = { listBucketResult: ListBucketResult } | ListBucketError;
59
65
 
60
66
  export interface ListMultipartUploadSuccess {
61
67
  listMultipartUploadsResult: {
@@ -148,12 +154,12 @@ export interface CopyObjectResult {
148
154
  lastModified?: Date;
149
155
  }
150
156
 
151
- /**
152
- * Where Buffer is available, e.g. when @types/node is loaded, we want to use it.
153
- * But it should be excluded in other environments (e.g. Cloudflare).
154
- */
155
- export type MaybeBuffer = typeof globalThis extends { Buffer?: infer B }
157
+ type BinaryData = ArrayBuffer | Uint8Array;
158
+
159
+ type MaybeBuffer = typeof globalThis extends { Buffer?: infer B }
156
160
  ? B extends new (...a: unknown[]) => unknown
157
- ? InstanceType<B>
158
- : ArrayBuffer | Uint8Array
159
- : ArrayBuffer | Uint8Array;
161
+ ? InstanceType<B> | BinaryData
162
+ : BinaryData
163
+ : BinaryData;
164
+
165
+ export type DataInput = string | MaybeBuffer | ReadableStream | File | Blob;