s3mini 0.9.0 → 0.9.2

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 {
@@ -241,18 +247,24 @@ class S3mini {
241
247
  return { filteredOpts, conditionalHeaders };
242
248
  }
243
249
 
244
- private _validateData(data: unknown): BodyInit {
245
- if (!((globalThis.Buffer && data instanceof globalThis.Buffer) || typeof data === 'string')) {
246
- this._log('error', C.ERROR_DATA_BUFFER_REQUIRED);
247
- throw new TypeError(C.ERROR_DATA_BUFFER_REQUIRED);
248
- }
249
- return data;
250
- }
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
+ // }
251
263
 
252
264
  private _validateUploadPartParams(
253
265
  key: string,
254
266
  uploadId: string,
255
- data: IT.MaybeBuffer | string,
267
+ data: IT.DataInput,
256
268
  partNumber: number,
257
269
  opts: object,
258
270
  ): BodyInit {
@@ -266,7 +278,7 @@ class S3mini {
266
278
  throw new TypeError(`${C.ERROR_PREFIX}partNumber must be a positive integer`);
267
279
  }
268
280
  this._checkOpts(opts);
269
- return this._validateData(data);
281
+ return data as BodyInit;
270
282
  }
271
283
 
272
284
  private async _sign(
@@ -564,6 +576,7 @@ class S3mini {
564
576
  });
565
577
 
566
578
  if (res.status === 404) {
579
+ void res.body?.cancel();
567
580
  return null;
568
581
  }
569
582
 
@@ -634,10 +647,16 @@ class S3mini {
634
647
 
635
648
  // Extract regular objects from Contents
636
649
  if (contents) {
637
- if (Array.isArray(contents)) {
638
- objects.push(...(contents as IT.ListObject[]));
639
- } else {
640
- objects.push(contents as IT.ListObject);
650
+ const raw = Array.isArray(contents) ? contents : [contents];
651
+ for (const item of raw) {
652
+ const o = item as Record<string, string>;
653
+ objects.push({
654
+ Key: o.Key ?? o.key ?? '',
655
+ Size: Number(o.Size ?? o.size ?? 0),
656
+ LastModified: new Date(o.LastModified ?? o.lastModified ?? 0),
657
+ ETag: o.ETag ?? o.etag ?? '',
658
+ StorageClass: o.StorageClass ?? o.storageClass ?? '',
659
+ });
641
660
  }
642
661
  }
643
662
 
@@ -742,6 +761,7 @@ class S3mini {
742
761
  if (s === 200) {
743
762
  return res.text();
744
763
  }
764
+ void res.body?.cancel();
745
765
  return null;
746
766
  }
747
767
 
@@ -766,6 +786,7 @@ class S3mini {
766
786
  if (res.status === 200) {
767
787
  return res;
768
788
  }
789
+ void res.body?.cancel();
769
790
  return null;
770
791
  }
771
792
 
@@ -790,6 +811,7 @@ class S3mini {
790
811
  if (res.status === 200) {
791
812
  return res.arrayBuffer();
792
813
  }
814
+ void res.body?.cancel();
793
815
  return null;
794
816
  }
795
817
 
@@ -814,6 +836,7 @@ class S3mini {
814
836
  if (res.status === 200) {
815
837
  return res.json() as Promise<T>;
816
838
  }
839
+ void res.body?.cancel();
817
840
  return null;
818
841
  }
819
842
 
@@ -838,6 +861,7 @@ class S3mini {
838
861
  });
839
862
  const s = res.status;
840
863
  if (s === 404 || s === 412 || s === 304) {
864
+ void res.body?.cancel();
841
865
  return { etag: null, data: null };
842
866
  }
843
867
 
@@ -900,7 +924,9 @@ class S3mini {
900
924
  return len ? +len : 0;
901
925
  } catch (err) {
902
926
  this._log('error', `Error getting content length for object ${key}: ${String(err)}`);
903
- throw new Error(`${C.ERROR_PREFIX}Error getting content length for object ${key}: ${String(err)}`);
927
+ throw new Error(`${C.ERROR_PREFIX}Error getting content length for object ${key}: ${String(err)}`, {
928
+ cause: err,
929
+ });
904
930
  }
905
931
  }
906
932
 
@@ -969,7 +995,7 @@ class S3mini {
969
995
  /**
970
996
  * Uploads an object to the S3-compatible service.
971
997
  * @param {string} key - The key/path where the object will be stored.
972
- * @param {string | Buffer} data - The data to upload (string or Buffer).
998
+ * @param {string | IT.MaybeBuffer | ReadableStream | File | Blob} data - The data to upload (string or Buffer).
973
999
  * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
974
1000
  * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
975
1001
  * @param {IT.AWSHeaders} [additionalHeaders] - Additional x-amz-* headers specific to this request, if any.
@@ -985,15 +1011,17 @@ class S3mini {
985
1011
  */
986
1012
  public async putObject(
987
1013
  key: string,
988
- data: string | IT.MaybeBuffer,
1014
+ data: string | IT.DataInput | ReadableStream | File | Blob,
989
1015
  fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
990
1016
  ssecHeaders?: IT.SSECHeaders,
991
1017
  additionalHeaders?: IT.AWSHeaders,
1018
+ contentLength?: number,
992
1019
  ): Promise<Response> {
1020
+ const size = contentLength ?? getByteSize(data);
993
1021
  return this._signedRequest('PUT', key, {
994
- body: this._validateData(data),
1022
+ body: data as BodyInit,
995
1023
  headers: {
996
- [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
1024
+ ...(size && { [C.HEADER_CONTENT_LENGTH]: size }),
997
1025
  [C.HEADER_CONTENT_TYPE]: fileType,
998
1026
  ...additionalHeaders,
999
1027
  ...ssecHeaders,
@@ -1002,6 +1030,235 @@ class S3mini {
1002
1030
  });
1003
1031
  }
1004
1032
 
1033
+ /**
1034
+ * Put object that automatically chooses single PUT vs multipart.
1035
+ * Same signature/shape as putObject so callers don't need to change.
1036
+ * @param {string} key - The key/path where the object will be stored.
1037
+ * @param {string | IT.MaybeBuffer | ReadableStream | File | Blob} data - The data to upload (string or Buffer).
1038
+ * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
1039
+ * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
1040
+ * @param {IT.AWSHeaders} [additionalHeaders] - Additional x-amz-* headers specific to this request, if any.
1041
+ * @param {number} [contentLength] - Optional known content length of data.
1042
+ * @returns {Promise<Response | { ok: boolean; status: number; headers: Map<string, string> }>} A promise that resolves to the Response object from the upload request.
1043
+ * @throws {TypeError} If data is not a string or Buffer.
1044
+ * @example
1045
+ * // Upload text file
1046
+ * await s3.putAnyObject('hello.txt', 'Hello, World!', 'text/plain');
1047
+ *
1048
+ * // Upload binary data
1049
+ * const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
1050
+ * await s3.putAnyObject('image.png', buffer, 'image/png');
1051
+ */
1052
+ public async putAnyObject(
1053
+ key: string,
1054
+ data: IT.DataInput,
1055
+ fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
1056
+ ssecHeaders?: IT.SSECHeaders,
1057
+ additionalHeaders?: IT.AWSHeaders,
1058
+ contentLength?: number,
1059
+ ): Promise<Response | { ok: boolean; status: number; headers: Map<string, string> }> {
1060
+ const size = contentLength ?? getByteSize(data);
1061
+
1062
+ // Single PUT for small files
1063
+ if (!Number.isNaN(size) && size <= this.minPartSize) {
1064
+ return this.putObject(key, data, fileType, ssecHeaders, additionalHeaders, contentLength);
1065
+ }
1066
+
1067
+ this._checkKey(key);
1068
+ return this._multipartUpload(key, data, fileType, ssecHeaders, additionalHeaders);
1069
+ }
1070
+
1071
+ private async _multipartUpload(
1072
+ key: string,
1073
+ data: IT.DataInput,
1074
+ fileType: string,
1075
+ ssecHeaders?: IT.SSECHeaders,
1076
+ additionalHeaders?: IT.AWSHeaders,
1077
+ ): Promise<Response | { ok: boolean; status: number; headers: Map<string, string> }> {
1078
+ const uploadId = await this.getMultipartUploadId(key, fileType, ssecHeaders, additionalHeaders);
1079
+
1080
+ try {
1081
+ const parts = await this._uploadPartsOptimized(key, uploadId, data, ssecHeaders, additionalHeaders);
1082
+ parts.sort((a, b) => a.partNumber - b.partNumber);
1083
+ const result = await this.completeMultipartUpload(key, uploadId, parts);
1084
+ return this._createSuccessResponse(result.etag || '');
1085
+ } catch (err) {
1086
+ await this._safeAbortUpload(key, uploadId);
1087
+ throw err;
1088
+ }
1089
+ }
1090
+
1091
+ private async _uploadKnownSizePartsParallel(
1092
+ key: string,
1093
+ uploadId: string,
1094
+ data: Uint8Array | Blob,
1095
+ ssecHeaders?: IT.SSECHeaders,
1096
+ additionalHeaders?: IT.AWSHeaders,
1097
+ concurrency: number = 4,
1098
+ maxRetries: number = 3,
1099
+ ): Promise<IT.UploadPart[]> {
1100
+ const partSize = this.minPartSize;
1101
+ const totalSize = data instanceof Blob ? data.size : data.byteLength;
1102
+ const totalParts = Math.ceil(totalSize / partSize);
1103
+ const results: IT.UploadPart[] = new Array(totalParts) as IT.UploadPart[];
1104
+ let nextIndex = 0;
1105
+
1106
+ const worker = async (): Promise<void> => {
1107
+ while (true) {
1108
+ const index = nextIndex++;
1109
+ if (index >= totalParts) {
1110
+ return;
1111
+ }
1112
+
1113
+ const start = index * partSize;
1114
+ const end = Math.min(start + partSize, totalSize);
1115
+ const part =
1116
+ data instanceof Blob
1117
+ ? await data.slice(start, end).arrayBuffer() // Must await - R2 needs actual bytes
1118
+ : data.subarray(start, end);
1119
+
1120
+ results[index] = await this._uploadPartWithRetry(
1121
+ key,
1122
+ uploadId,
1123
+ part,
1124
+ index + 1,
1125
+ ssecHeaders,
1126
+ additionalHeaders,
1127
+ maxRetries,
1128
+ );
1129
+ }
1130
+ };
1131
+
1132
+ await Promise.all(Array.from({ length: Math.min(concurrency, totalParts) }, () => worker()));
1133
+ return results;
1134
+ }
1135
+
1136
+ private async _uploadPartsOptimized(
1137
+ key: string,
1138
+ uploadId: string,
1139
+ data: IT.DataInput,
1140
+ ssecHeaders?: IT.SSECHeaders,
1141
+ additionalHeaders?: IT.AWSHeaders,
1142
+ concurrency: number = 4,
1143
+ maxRetries: number = 3,
1144
+ ): Promise<IT.UploadPart[]> {
1145
+ const bytes = toUint8Array(data);
1146
+ if (bytes) {
1147
+ return this._uploadKnownSizePartsParallel(
1148
+ key,
1149
+ uploadId,
1150
+ bytes,
1151
+ ssecHeaders,
1152
+ additionalHeaders,
1153
+ concurrency,
1154
+ maxRetries,
1155
+ );
1156
+ }
1157
+ if (data instanceof Blob) {
1158
+ return this._uploadKnownSizePartsParallel(
1159
+ key,
1160
+ uploadId,
1161
+ data,
1162
+ ssecHeaders,
1163
+ additionalHeaders,
1164
+ concurrency,
1165
+ maxRetries,
1166
+ );
1167
+ }
1168
+ return this._uploadStreamingParts(
1169
+ key,
1170
+ uploadId,
1171
+ data as ReadableStream,
1172
+ ssecHeaders,
1173
+ additionalHeaders,
1174
+ concurrency,
1175
+ maxRetries,
1176
+ );
1177
+ }
1178
+
1179
+ private async _uploadStreamingParts(
1180
+ key: string,
1181
+ uploadId: string,
1182
+ stream: ReadableStream,
1183
+ ssecHeaders?: IT.SSECHeaders,
1184
+ additionalHeaders?: IT.AWSHeaders,
1185
+ concurrency: number = 4,
1186
+ maxRetries: number = 3,
1187
+ ): Promise<IT.UploadPart[]> {
1188
+ const parts: IT.UploadPart[] = [];
1189
+ const active = new Set<Promise<void>>();
1190
+ let partNumber = 0;
1191
+
1192
+ for await (const partData of generateParts(stream, this.minPartSize)) {
1193
+ const currentPartNumber = ++partNumber;
1194
+
1195
+ while (active.size >= concurrency) {
1196
+ await Promise.race(active);
1197
+ }
1198
+
1199
+ const p = this._uploadPartWithRetry(
1200
+ key,
1201
+ uploadId,
1202
+ partData,
1203
+ currentPartNumber,
1204
+ ssecHeaders,
1205
+ additionalHeaders,
1206
+ maxRetries,
1207
+ ).then(part => {
1208
+ parts.push(part);
1209
+ active.delete(p);
1210
+ });
1211
+
1212
+ active.add(p);
1213
+ }
1214
+
1215
+ await Promise.all(active);
1216
+ return parts;
1217
+ }
1218
+
1219
+ private async _uploadPartWithRetry(
1220
+ key: string,
1221
+ uploadId: string,
1222
+ data: IT.PartData,
1223
+ partNumber: number,
1224
+ ssecHeaders?: IT.SSECHeaders,
1225
+ additionalHeaders?: IT.AWSHeaders,
1226
+ maxRetries: number = 3,
1227
+ ): Promise<IT.UploadPart> {
1228
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1229
+ try {
1230
+ return await this.uploadPart(key, uploadId, data, partNumber, {}, ssecHeaders, additionalHeaders);
1231
+ } catch (err) {
1232
+ if (attempt === maxRetries) {
1233
+ throw err;
1234
+ }
1235
+ await new Promise(r => setTimeout(r, Math.min(1000 * 2 ** attempt, 10000)));
1236
+ }
1237
+ }
1238
+ throw new Error('Unreachable');
1239
+ }
1240
+
1241
+ private async _safeAbortUpload(key: string, uploadId: string): Promise<void> {
1242
+ try {
1243
+ await this.abortMultipartUpload(key, uploadId);
1244
+ } catch (err) {
1245
+ this._log('warn', `Failed to abort multipart upload: ${String(err)}`);
1246
+ }
1247
+ }
1248
+
1249
+ private _createSuccessResponse(
1250
+ etag: string,
1251
+ ): Response | { ok: boolean; status: number; headers: Map<string, string> } {
1252
+ if (typeof Response !== 'undefined') {
1253
+ const headers = new Headers();
1254
+ if (etag) {
1255
+ headers.set('ETag', etag);
1256
+ }
1257
+ return new Response('', { status: 200, headers });
1258
+ }
1259
+ return { ok: true, status: 200, headers: new Map([['ETag', etag]]) };
1260
+ }
1261
+
1005
1262
  /**
1006
1263
  * Initiates a multipart upload and returns the upload ID.
1007
1264
  * @param {string} key - The key/path where the object will be stored.
@@ -1018,13 +1275,14 @@ class S3mini {
1018
1275
  key: string,
1019
1276
  fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
1020
1277
  ssecHeaders?: IT.SSECHeaders,
1278
+ additionalHeaders?: IT.AWSHeaders,
1021
1279
  ): Promise<string> {
1022
1280
  this._checkKey(key);
1023
1281
  if (typeof fileType !== 'string') {
1024
1282
  throw new TypeError(`${C.ERROR_PREFIX}fileType must be a string`);
1025
1283
  }
1026
1284
  const query = { uploads: '' };
1027
- const headers = { [C.HEADER_CONTENT_TYPE]: fileType, ...ssecHeaders };
1285
+ const headers = { [C.HEADER_CONTENT_TYPE]: fileType, ...ssecHeaders, ...additionalHeaders };
1028
1286
 
1029
1287
  const res = await this._signedRequest('POST', key, {
1030
1288
  query,
@@ -1056,7 +1314,7 @@ class S3mini {
1056
1314
  * Uploads a part in a multipart upload.
1057
1315
  * @param {string} key - The key of the object being uploaded.
1058
1316
  * @param {string} uploadId - The upload ID from getMultipartUploadId.
1059
- * @param {Buffer | string} data - The data for this part.
1317
+ * @param {string | IT.MaybeBuffer | ReadableStream | File | Blob} data - The data for this part.
1060
1318
  * @param {number} partNumber - The part number (must be between 1 and 10,000).
1061
1319
  * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
1062
1320
  * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
@@ -1074,20 +1332,23 @@ class S3mini {
1074
1332
  public async uploadPart(
1075
1333
  key: string,
1076
1334
  uploadId: string,
1077
- data: IT.MaybeBuffer | string,
1335
+ data: IT.DataInput,
1078
1336
  partNumber: number,
1079
1337
  opts: Record<string, unknown> = {},
1080
1338
  ssecHeaders?: IT.SSECHeaders,
1339
+ additionalHeaders?: IT.AWSHeaders,
1081
1340
  ): Promise<IT.UploadPart> {
1082
1341
  const body = this._validateUploadPartParams(key, uploadId, data, partNumber, opts);
1083
1342
 
1084
1343
  const query = { uploadId, partNumber, ...opts };
1344
+ const size = getByteSize(data);
1085
1345
  const res = await this._signedRequest('PUT', key, {
1086
1346
  query,
1087
1347
  body,
1088
1348
  headers: {
1089
- [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
1349
+ ...(size && !Number.isNaN(size) && { [C.HEADER_CONTENT_LENGTH]: size }),
1090
1350
  ...ssecHeaders,
1351
+ ...additionalHeaders,
1091
1352
  },
1092
1353
  });
1093
1354
 
@@ -1600,6 +1861,84 @@ class S3mini {
1600
1861
  .sort((a, b) => a.localeCompare(b))
1601
1862
  .join('&');
1602
1863
  }
1864
+ /**
1865
+ * Generates a pre-signed URL for direct client access to an S3 object.
1866
+ * The URL embeds authentication in query parameters instead of headers,
1867
+ * allowing unauthenticated HTTP clients to perform the specified operation.
1868
+ *
1869
+ * @param {'GET' | 'PUT'} method - HTTP method ('GET' for download, 'PUT' for upload)
1870
+ * @param {string} key - The object key/path
1871
+ * @param {number} [expiresIn=3600] - URL expiration time in seconds (1–604800)
1872
+ * @param {Record<string, string>} [queryParams={}] - Additional query parameters to sign
1873
+ * @returns {Promise<string>} Pre-signed URL string
1874
+ * @throws {TypeError} If key is empty or expiresIn is out of range
1875
+ * @example
1876
+ * // Download URL valid for 1 hour
1877
+ * const url = await s3.getPresignedUrl('GET', 'photos/vacation.jpg');
1878
+ *
1879
+ * // Upload URL valid for 5 minutes
1880
+ * const url = await s3.getPresignedUrl('PUT', 'uploads/file.bin', 300);
1881
+ *
1882
+ * // Client-side usage (no credentials needed)
1883
+ * await fetch(url, { method: 'PUT', body: data });
1884
+ */
1885
+ public async getPresignedUrl(
1886
+ method: 'GET' | 'PUT',
1887
+ key: string,
1888
+ expiresIn: number = 3600,
1889
+ queryParams: Record<string, string> = {},
1890
+ ): Promise<string> {
1891
+ this._checkKey(key);
1892
+ if (!Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > 604800) {
1893
+ throw new TypeError(`${C.ERROR_PREFIX}expiresIn must be between 1 and 604800 seconds`);
1894
+ }
1895
+ return this._presign(method, uriResourceEscape(key), Math.floor(expiresIn), queryParams);
1896
+ }
1897
+
1898
+ private async _presign(
1899
+ method: string,
1900
+ keyPath: string,
1901
+ expiresIn: number,
1902
+ queryParams: Record<string, string>,
1903
+ ): Promise<string> {
1904
+ const url = new URL(this.endpoint);
1905
+ if (keyPath.length > 0) {
1906
+ url.pathname =
1907
+ url.pathname === '/' ? `/${keyPath.replace(/^\/+/, '')}` : `${url.pathname}/${keyPath.replace(/^\/+/, '')}`;
1908
+ }
1909
+
1910
+ const d = new Date();
1911
+ const year = d.getUTCFullYear();
1912
+ const month = String(d.getUTCMonth() + 1).padStart(2, '0');
1913
+ const day = String(d.getUTCDate()).padStart(2, '0');
1914
+ const shortDatetime = `${year}${month}${day}`;
1915
+ const fullDatetime = `${shortDatetime}T${String(d.getUTCHours()).padStart(2, '0')}${String(d.getUTCMinutes()).padStart(2, '0')}${String(d.getUTCSeconds()).padStart(2, '0')}Z`;
1916
+ const credentialScope = `${shortDatetime}/${this.region}/${C.S3_SERVICE}/${C.AWS_REQUEST_TYPE}`;
1917
+
1918
+ const signedHeaders = 'host';
1919
+
1920
+ const allQueryParams: Record<string, string> = {
1921
+ ...queryParams,
1922
+ 'X-Amz-Algorithm': C.AWS_ALGORITHM,
1923
+ 'X-Amz-Credential': `${this.#accessKeyId}/${credentialScope}`,
1924
+ 'X-Amz-Date': fullDatetime,
1925
+ 'X-Amz-Expires': String(expiresIn),
1926
+ 'X-Amz-SignedHeaders': signedHeaders,
1927
+ };
1928
+
1929
+ const canonicalQueryString = this._buildCanonicalQueryString(allQueryParams);
1930
+ const canonicalRequest = `${method}\n${url.pathname}\n${canonicalQueryString}\nhost:${url.host}\n\n${signedHeaders}\n${C.UNSIGNED_PAYLOAD}`;
1931
+ const stringToSign = `${C.AWS_ALGORITHM}\n${fullDatetime}\n${credentialScope}\n${hexFromBuffer(await sha256(canonicalRequest))}`;
1932
+
1933
+ if (shortDatetime !== this.signingKeyDate || !this.signingKey) {
1934
+ this.signingKeyDate = shortDatetime;
1935
+ this.signingKey = await this._getSignatureKey(shortDatetime);
1936
+ }
1937
+
1938
+ const signature = hexFromBuffer(await hmac(this.signingKey, stringToSign));
1939
+ return `${url.origin}${url.pathname}?${canonicalQueryString}&X-Amz-Signature=${signature}`;
1940
+ }
1941
+
1603
1942
  private async _getSignatureKey(dateStamp: string): Promise<ArrayBuffer> {
1604
1943
  const kDate = await hmac(`AWS4${this.#secretAccessKey}`, dateStamp);
1605
1944
  const kRegion = await hmac(kDate, this.region);
package/src/consts.ts CHANGED
@@ -11,6 +11,7 @@ export const JSON_CONTENT_TYPE = 'application/json';
11
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/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;
@@ -151,12 +154,12 @@ export interface CopyObjectResult {
151
154
  lastModified?: Date;
152
155
  }
153
156
 
154
- /**
155
- * Where Buffer is available, e.g. when @types/node is loaded, we want to use it.
156
- * But it should be excluded in other environments (e.g. Cloudflare).
157
- */
158
- export type MaybeBuffer = typeof globalThis extends { Buffer?: infer B }
157
+ type BinaryData = ArrayBuffer | Uint8Array;
158
+
159
+ type MaybeBuffer = typeof globalThis extends { Buffer?: infer B }
159
160
  ? B extends new (...a: unknown[]) => unknown
160
- ? InstanceType<B>
161
- : ArrayBuffer | Uint8Array
162
- : ArrayBuffer | Uint8Array;
161
+ ? InstanceType<B> | BinaryData
162
+ : BinaryData
163
+ : BinaryData;
164
+
165
+ export type DataInput = string | MaybeBuffer | ReadableStream | File | Blob;