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/README.md +3 -5
- package/dist/s3mini.d.ts +49 -16
- package/dist/s3mini.js +267 -26
- package/dist/s3mini.js.map +1 -1
- package/dist/s3mini.min.js +1 -1
- package/dist/s3mini.min.js.map +1 -1
- package/package.json +9 -7
- package/src/S3.ts +281 -23
- package/src/consts.ts +2 -1
- package/src/index.ts +2 -0
- package/src/types.ts +17 -11
- package/src/utils.ts +165 -3
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'
|
|
153
|
+
if (typeof accessKeyId !== 'string') {
|
|
148
154
|
throw new TypeError(C.ERROR_ACCESS_KEY_REQUIRED);
|
|
149
155
|
}
|
|
150
|
-
if (typeof secretAccessKey !== 'string'
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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.
|
|
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
|
|
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}?${
|
|
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 |
|
|
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.
|
|
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:
|
|
1008
|
+
body: data as BodyInit,
|
|
984
1009
|
headers: {
|
|
985
|
-
[C.HEADER_CONTENT_LENGTH]:
|
|
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 {
|
|
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.
|
|
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]:
|
|
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(['
|
|
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
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: {
|
|
57
|
+
export interface ListBucketError {
|
|
58
|
+
error: {
|
|
59
|
+
code: string;
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
export type ListBucketResponse = { listBucketResult: ListBucketResult } |
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
:
|
|
159
|
-
:
|
|
161
|
+
? InstanceType<B> | BinaryData
|
|
162
|
+
: BinaryData
|
|
163
|
+
: BinaryData;
|
|
164
|
+
|
|
165
|
+
export type DataInput = string | MaybeBuffer | ReadableStream | File | Blob;
|