s3mini 0.7.0 → 0.8.0
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 +9 -2
- package/dist/s3mini.d.ts +23 -19
- package/dist/s3mini.js +90 -30
- 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 +12 -13
- package/src/S3.ts +116 -60
- package/src/utils.ts +39 -5
package/src/S3.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
import * as C from './consts.js';
|
|
4
|
+
import {
|
|
5
|
+
hexFromBuffer,
|
|
6
|
+
sha256,
|
|
7
|
+
hmac,
|
|
8
|
+
uriResourceEscape,
|
|
9
|
+
getByteSize,
|
|
10
|
+
sanitizeETag,
|
|
11
|
+
uriEscape,
|
|
12
|
+
parseXml,
|
|
13
|
+
escapeXml,
|
|
14
|
+
base64FromBuffer,
|
|
15
|
+
extractErrCode,
|
|
16
|
+
S3NetworkError,
|
|
17
|
+
S3ServiceError,
|
|
18
|
+
} from './utils.js';
|
|
4
19
|
import type * as IT from './types.js';
|
|
5
|
-
import * as U from './utils.js';
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* S3 class for interacting with S3-compatible object storage services.
|
|
@@ -11,7 +25,7 @@ import * as U from './utils.js';
|
|
|
11
25
|
*
|
|
12
26
|
* @class
|
|
13
27
|
* @example
|
|
14
|
-
* const s3 = new
|
|
28
|
+
* const s3 = new S3mini({
|
|
15
29
|
* accessKeyId: 'your-access-key',
|
|
16
30
|
* secretAccessKey: 'your-secret-key',
|
|
17
31
|
* endpoint: 'https://your-s3-endpoint.com/bucket-name',
|
|
@@ -43,15 +57,15 @@ class S3mini {
|
|
|
43
57
|
* @param {typeof fetch} [config.fetch=globalThis.fetch] - Custom fetch implementation to use for HTTP requests.
|
|
44
58
|
* @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
|
|
45
59
|
*/
|
|
46
|
-
readonly accessKeyId: string;
|
|
47
|
-
readonly secretAccessKey: string;
|
|
60
|
+
readonly #accessKeyId: string;
|
|
61
|
+
readonly #secretAccessKey: string;
|
|
48
62
|
readonly endpoint: URL;
|
|
49
63
|
readonly region: string;
|
|
50
64
|
readonly bucketName: string;
|
|
51
65
|
readonly requestSizeInBytes: number;
|
|
52
66
|
readonly requestAbortTimeout?: number;
|
|
53
67
|
readonly logger?: IT.Logger;
|
|
54
|
-
readonly
|
|
68
|
+
readonly _fetch: typeof fetch;
|
|
55
69
|
private signingKeyDate?: string;
|
|
56
70
|
private signingKey?: ArrayBuffer;
|
|
57
71
|
|
|
@@ -66,15 +80,15 @@ class S3mini {
|
|
|
66
80
|
fetch = globalThis.fetch,
|
|
67
81
|
}: IT.S3Config) {
|
|
68
82
|
this._validateConstructorParams(accessKeyId, secretAccessKey, endpoint);
|
|
69
|
-
this
|
|
70
|
-
this
|
|
83
|
+
this.#accessKeyId = accessKeyId;
|
|
84
|
+
this.#secretAccessKey = secretAccessKey;
|
|
71
85
|
this.endpoint = new URL(this._ensureValidUrl(endpoint));
|
|
72
86
|
this.region = region;
|
|
73
87
|
this.bucketName = this._extractBucketName();
|
|
74
88
|
this.requestSizeInBytes = requestSizeInBytes;
|
|
75
89
|
this.requestAbortTimeout = requestAbortTimeout;
|
|
76
90
|
this.logger = logger;
|
|
77
|
-
this.
|
|
91
|
+
this._fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => fetch(input, init);
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
private _sanitize(obj: unknown): unknown {
|
|
@@ -120,7 +134,7 @@ class S3mini {
|
|
|
120
134
|
region: this.region,
|
|
121
135
|
endpoint: this.endpoint.toString(),
|
|
122
136
|
// Only include the first few characters of the access key, if it exists
|
|
123
|
-
accessKeyId: this
|
|
137
|
+
accessKeyId: this.#accessKeyId ? `${this.#accessKeyId.substring(0, 4)}...` : undefined,
|
|
124
138
|
}),
|
|
125
139
|
};
|
|
126
140
|
|
|
@@ -292,14 +306,14 @@ class S3mini {
|
|
|
292
306
|
}
|
|
293
307
|
}
|
|
294
308
|
const canonicalRequest = `${method}\n${url.pathname}\n${this._buildCanonicalQueryString(query)}\n${canonicalHeaders}\n\n${signedHeaders}\n${C.UNSIGNED_PAYLOAD}`;
|
|
295
|
-
const stringToSign = `${C.AWS_ALGORITHM}\n${fullDatetime}\n${credentialScope}\n${
|
|
309
|
+
const stringToSign = `${C.AWS_ALGORITHM}\n${fullDatetime}\n${credentialScope}\n${hexFromBuffer(await sha256(canonicalRequest))}`;
|
|
296
310
|
if (shortDatetime !== this.signingKeyDate || !this.signingKey) {
|
|
297
311
|
this.signingKeyDate = shortDatetime;
|
|
298
312
|
this.signingKey = await this._getSignatureKey(shortDatetime);
|
|
299
313
|
}
|
|
300
|
-
const signature =
|
|
314
|
+
const signature = hexFromBuffer(await hmac(this.signingKey, stringToSign));
|
|
301
315
|
headers[C.HEADER_AUTHORIZATION] =
|
|
302
|
-
`${C.AWS_ALGORITHM} Credential=${this
|
|
316
|
+
`${C.AWS_ALGORITHM} Credential=${this.#accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
303
317
|
return { url: url.toString(), headers };
|
|
304
318
|
}
|
|
305
319
|
|
|
@@ -335,7 +349,7 @@ class S3mini {
|
|
|
335
349
|
...conditionalHeaders,
|
|
336
350
|
};
|
|
337
351
|
|
|
338
|
-
const encodedKey = key ?
|
|
352
|
+
const encodedKey = key ? uriResourceEscape(key) : '';
|
|
339
353
|
const { url, headers: signedHeaders } = await this._sign(method, encodedKey, filteredOpts, baseHeaders);
|
|
340
354
|
if (Object.keys(query).length > 0) {
|
|
341
355
|
withQuery = true; // append query string to signed URL
|
|
@@ -360,7 +374,7 @@ class S3mini {
|
|
|
360
374
|
* const cleanEtag = s3.sanitizeETag('"abc123"'); // Returns: 'abc123'
|
|
361
375
|
*/
|
|
362
376
|
public sanitizeETag(etag: string): string {
|
|
363
|
-
return
|
|
377
|
+
return sanitizeETag(etag);
|
|
364
378
|
}
|
|
365
379
|
|
|
366
380
|
/**
|
|
@@ -376,7 +390,7 @@ class S3mini {
|
|
|
376
390
|
`;
|
|
377
391
|
const headers = {
|
|
378
392
|
[C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
|
|
379
|
-
[C.HEADER_CONTENT_LENGTH]:
|
|
393
|
+
[C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
|
|
380
394
|
};
|
|
381
395
|
const res = await this._signedRequest('PUT', '', {
|
|
382
396
|
body: xmlBody,
|
|
@@ -457,7 +471,7 @@ class S3mini {
|
|
|
457
471
|
this._checkPrefix(prefix);
|
|
458
472
|
this._checkOpts(opts);
|
|
459
473
|
|
|
460
|
-
const keyPath = delimiter === '/' ? delimiter :
|
|
474
|
+
const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
|
|
461
475
|
const unlimited = !(maxKeys && maxKeys > 0);
|
|
462
476
|
let remaining = unlimited ? Infinity : maxKeys;
|
|
463
477
|
let token: string | undefined;
|
|
@@ -482,6 +496,47 @@ class S3mini {
|
|
|
482
496
|
return all;
|
|
483
497
|
}
|
|
484
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Lists objects in the bucket with optional filtering and pagination using a continuation token.
|
|
501
|
+
* This method retrieves objects matching the criteria (paginated like listObjectsV2).
|
|
502
|
+
* @param {string} [delimiter='/'] - The delimiter to use for grouping objects.
|
|
503
|
+
* @param {string} [prefix=''] - The prefix to filter objects by.
|
|
504
|
+
* @param {number} [maxKeys] - The maximum number of keys to return. Uses a default value of 100.
|
|
505
|
+
* @param {string} [nextContinuationToken] - The nextContinuationToken to continue previous results. If not provided, starts from the beginning.
|
|
506
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
507
|
+
* @returns {Promise<{objects: IT.ListObject[] | null; nextContinuationToken?: string } | undefined | null>} A promise that resolves to an array of objects or null if the bucket is empty, along with nextContinuationToken if there are more reccords.
|
|
508
|
+
* @example
|
|
509
|
+
* // List all objects
|
|
510
|
+
* const { objects, nextContinuationToken } = await s3.listObjectsPaged();
|
|
511
|
+
*
|
|
512
|
+
* // List 200 objects with prefix
|
|
513
|
+
* const photos = await s3.listObjectsPaged('/', 'photos/', 200, "token...");
|
|
514
|
+
*/
|
|
515
|
+
public async listObjectsPaged(
|
|
516
|
+
delimiter: string = '/',
|
|
517
|
+
prefix: string = '',
|
|
518
|
+
maxKeys: number = 100,
|
|
519
|
+
nextContinuationToken?: string,
|
|
520
|
+
opts: Record<string, unknown> = {},
|
|
521
|
+
): Promise<{ objects: IT.ListObject[] | null; nextContinuationToken?: string } | undefined | null> {
|
|
522
|
+
this._checkDelimiter(delimiter);
|
|
523
|
+
this._checkPrefix(prefix);
|
|
524
|
+
this._checkOpts(opts);
|
|
525
|
+
|
|
526
|
+
const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
|
|
527
|
+
let token: string | undefined = nextContinuationToken;
|
|
528
|
+
const all: IT.ListObject[] = [];
|
|
529
|
+
|
|
530
|
+
const batchResult = await this._fetchObjectBatch(keyPath, prefix, maxKeys, token, opts);
|
|
531
|
+
if (batchResult === null) {
|
|
532
|
+
return null; // 404 - bucket not found
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
all.push(...batchResult.objects);
|
|
536
|
+
token = batchResult.continuationToken;
|
|
537
|
+
return { objects: all, nextContinuationToken: token };
|
|
538
|
+
}
|
|
539
|
+
|
|
485
540
|
private async _fetchObjectBatch(
|
|
486
541
|
keyPath: string,
|
|
487
542
|
prefix: string,
|
|
@@ -546,7 +601,7 @@ class S3mini {
|
|
|
546
601
|
objects: IT.ListObject[];
|
|
547
602
|
continuationToken?: string;
|
|
548
603
|
} {
|
|
549
|
-
const raw =
|
|
604
|
+
const raw = parseXml(xmlText) as Record<string, unknown>;
|
|
550
605
|
|
|
551
606
|
if (typeof raw !== 'object' || !raw || 'error' in raw) {
|
|
552
607
|
this._log('error', `${C.ERROR_PREFIX}Unexpected listObjects response shape: ${JSON.stringify(raw)}`);
|
|
@@ -604,7 +659,7 @@ class S3mini {
|
|
|
604
659
|
this._checkOpts(opts);
|
|
605
660
|
|
|
606
661
|
const query = { uploads: '', ...opts };
|
|
607
|
-
const keyPath = delimiter === '/' ? delimiter :
|
|
662
|
+
const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
|
|
608
663
|
|
|
609
664
|
const res = await this._signedRequest(method, keyPath, {
|
|
610
665
|
query,
|
|
@@ -618,7 +673,7 @@ class S3mini {
|
|
|
618
673
|
// etag: res.headers.get(C.HEADER_ETAG) ?? '',
|
|
619
674
|
// };
|
|
620
675
|
// }
|
|
621
|
-
const raw =
|
|
676
|
+
const raw = parseXml(await res.text()) as unknown;
|
|
622
677
|
if (typeof raw !== 'object' || raw === null) {
|
|
623
678
|
throw new Error(`${C.ERROR_PREFIX}Unexpected listMultipartUploads response shape`);
|
|
624
679
|
}
|
|
@@ -647,10 +702,11 @@ class S3mini {
|
|
|
647
702
|
tolerated: [200, 404, 412, 304],
|
|
648
703
|
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
649
704
|
});
|
|
650
|
-
|
|
651
|
-
|
|
705
|
+
const s = res.status;
|
|
706
|
+
if (s === 200) {
|
|
707
|
+
return res.text();
|
|
652
708
|
}
|
|
653
|
-
return
|
|
709
|
+
return null;
|
|
654
710
|
}
|
|
655
711
|
|
|
656
712
|
/**
|
|
@@ -671,10 +727,10 @@ class S3mini {
|
|
|
671
727
|
tolerated: [200, 404, 412, 304],
|
|
672
728
|
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
673
729
|
});
|
|
674
|
-
if (
|
|
675
|
-
return
|
|
730
|
+
if (res.status === 200) {
|
|
731
|
+
return res;
|
|
676
732
|
}
|
|
677
|
-
return
|
|
733
|
+
return null;
|
|
678
734
|
}
|
|
679
735
|
|
|
680
736
|
/**
|
|
@@ -695,10 +751,10 @@ class S3mini {
|
|
|
695
751
|
tolerated: [200, 404, 412, 304],
|
|
696
752
|
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
697
753
|
});
|
|
698
|
-
if (
|
|
699
|
-
return
|
|
754
|
+
if (res.status === 200) {
|
|
755
|
+
return res.arrayBuffer();
|
|
700
756
|
}
|
|
701
|
-
return
|
|
757
|
+
return null;
|
|
702
758
|
}
|
|
703
759
|
|
|
704
760
|
/**
|
|
@@ -719,10 +775,10 @@ class S3mini {
|
|
|
719
775
|
tolerated: [200, 404, 412, 304],
|
|
720
776
|
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
721
777
|
});
|
|
722
|
-
if (
|
|
723
|
-
return
|
|
778
|
+
if (res.status === 200) {
|
|
779
|
+
return res.json() as Promise<T>;
|
|
724
780
|
}
|
|
725
|
-
return
|
|
781
|
+
return null;
|
|
726
782
|
}
|
|
727
783
|
|
|
728
784
|
/**
|
|
@@ -744,8 +800,8 @@ class S3mini {
|
|
|
744
800
|
tolerated: [200, 404, 412, 304],
|
|
745
801
|
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
746
802
|
});
|
|
747
|
-
|
|
748
|
-
if (
|
|
803
|
+
const s = res.status;
|
|
804
|
+
if (s === 404 || s === 412 || s === 304) {
|
|
749
805
|
return { etag: null, data: null };
|
|
750
806
|
}
|
|
751
807
|
|
|
@@ -753,7 +809,7 @@ class S3mini {
|
|
|
753
809
|
if (!etag) {
|
|
754
810
|
throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
|
|
755
811
|
}
|
|
756
|
-
return { etag:
|
|
812
|
+
return { etag: sanitizeETag(etag), data: await res.arrayBuffer() };
|
|
757
813
|
} catch (err) {
|
|
758
814
|
this._log('error', `Error getting object ${key} with ETag: ${String(err)}`);
|
|
759
815
|
throw err;
|
|
@@ -867,7 +923,7 @@ class S3mini {
|
|
|
867
923
|
throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
|
|
868
924
|
}
|
|
869
925
|
|
|
870
|
-
return
|
|
926
|
+
return sanitizeETag(etag);
|
|
871
927
|
}
|
|
872
928
|
|
|
873
929
|
/**
|
|
@@ -897,7 +953,7 @@ class S3mini {
|
|
|
897
953
|
return this._signedRequest('PUT', key, {
|
|
898
954
|
body: this._validateData(data),
|
|
899
955
|
headers: {
|
|
900
|
-
[C.HEADER_CONTENT_LENGTH]:
|
|
956
|
+
[C.HEADER_CONTENT_LENGTH]: getByteSize(data),
|
|
901
957
|
[C.HEADER_CONTENT_TYPE]: fileType,
|
|
902
958
|
...additionalHeaders,
|
|
903
959
|
...ssecHeaders,
|
|
@@ -935,7 +991,7 @@ class S3mini {
|
|
|
935
991
|
headers,
|
|
936
992
|
withQuery: true,
|
|
937
993
|
});
|
|
938
|
-
const parsed =
|
|
994
|
+
const parsed = parseXml(await res.text()) as Record<string, unknown>;
|
|
939
995
|
|
|
940
996
|
if (parsed && typeof parsed === 'object') {
|
|
941
997
|
// Check for both cases of InitiateMultipartUploadResult
|
|
@@ -990,12 +1046,12 @@ class S3mini {
|
|
|
990
1046
|
query,
|
|
991
1047
|
body,
|
|
992
1048
|
headers: {
|
|
993
|
-
[C.HEADER_CONTENT_LENGTH]:
|
|
1049
|
+
[C.HEADER_CONTENT_LENGTH]: getByteSize(data),
|
|
994
1050
|
...ssecHeaders,
|
|
995
1051
|
},
|
|
996
1052
|
});
|
|
997
1053
|
|
|
998
|
-
return { partNumber, etag:
|
|
1054
|
+
return { partNumber, etag: sanitizeETag(res.headers.get('etag') || '') };
|
|
999
1055
|
}
|
|
1000
1056
|
|
|
1001
1057
|
/**
|
|
@@ -1025,7 +1081,7 @@ class S3mini {
|
|
|
1025
1081
|
const xmlBody = this._buildCompleteMultipartUploadXml(parts);
|
|
1026
1082
|
const headers = {
|
|
1027
1083
|
[C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
|
|
1028
|
-
[C.HEADER_CONTENT_LENGTH]:
|
|
1084
|
+
[C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
|
|
1029
1085
|
};
|
|
1030
1086
|
|
|
1031
1087
|
const res = await this._signedRequest('POST', key, {
|
|
@@ -1035,7 +1091,7 @@ class S3mini {
|
|
|
1035
1091
|
withQuery: true,
|
|
1036
1092
|
});
|
|
1037
1093
|
|
|
1038
|
-
const parsed =
|
|
1094
|
+
const parsed = parseXml(await res.text()) as Record<string, unknown>;
|
|
1039
1095
|
if (parsed && typeof parsed === 'object') {
|
|
1040
1096
|
// Check for both cases
|
|
1041
1097
|
const result = parsed.completeMultipartUploadResult || parsed.CompleteMultipartUploadResult || parsed;
|
|
@@ -1048,7 +1104,7 @@ class S3mini {
|
|
|
1048
1104
|
if (etag && typeof etag === 'string') {
|
|
1049
1105
|
return {
|
|
1050
1106
|
...resultObj,
|
|
1051
|
-
etag:
|
|
1107
|
+
etag: sanitizeETag(etag),
|
|
1052
1108
|
} as IT.CompleteMultipartUploadResult;
|
|
1053
1109
|
}
|
|
1054
1110
|
|
|
@@ -1089,7 +1145,7 @@ class S3mini {
|
|
|
1089
1145
|
headers,
|
|
1090
1146
|
withQuery: true,
|
|
1091
1147
|
});
|
|
1092
|
-
const parsed =
|
|
1148
|
+
const parsed = parseXml(await res.text()) as Record<string, unknown>;
|
|
1093
1149
|
if (
|
|
1094
1150
|
parsed &&
|
|
1095
1151
|
'error' in parsed &&
|
|
@@ -1220,7 +1276,7 @@ class S3mini {
|
|
|
1220
1276
|
this._checkKey(sourceKey);
|
|
1221
1277
|
this._checkKey(destinationKey);
|
|
1222
1278
|
|
|
1223
|
-
const copySource = `/${this.bucketName}/${
|
|
1279
|
+
const copySource = `/${this.bucketName}/${uriEscape(sourceKey)}`;
|
|
1224
1280
|
|
|
1225
1281
|
return this._executeCopyOperation(destinationKey, copySource, options);
|
|
1226
1282
|
}
|
|
@@ -1312,7 +1368,7 @@ class S3mini {
|
|
|
1312
1368
|
}
|
|
1313
1369
|
|
|
1314
1370
|
private _parseCopyObjectResponse(xmlText: string): IT.CopyObjectResult {
|
|
1315
|
-
const parsed =
|
|
1371
|
+
const parsed = parseXml(xmlText) as Record<string, unknown>;
|
|
1316
1372
|
if (!parsed || typeof parsed !== 'object') {
|
|
1317
1373
|
throw new Error(`${C.ERROR_PREFIX}Unexpected copyObject response format`);
|
|
1318
1374
|
}
|
|
@@ -1323,7 +1379,7 @@ class S3mini {
|
|
|
1323
1379
|
throw new Error(`${C.ERROR_PREFIX}ETag not found in copyObject response`);
|
|
1324
1380
|
}
|
|
1325
1381
|
return {
|
|
1326
|
-
etag:
|
|
1382
|
+
etag: sanitizeETag(etag),
|
|
1327
1383
|
lastModified: lastModified ? new Date(lastModified as string) : undefined,
|
|
1328
1384
|
};
|
|
1329
1385
|
}
|
|
@@ -1340,13 +1396,13 @@ class S3mini {
|
|
|
1340
1396
|
}
|
|
1341
1397
|
|
|
1342
1398
|
private async _deleteObjectsProcess(keys: string[]): Promise<boolean[]> {
|
|
1343
|
-
const objectsXml = keys.map(key => `<Object><Key>${
|
|
1399
|
+
const objectsXml = keys.map(key => `<Object><Key>${escapeXml(key)}</Key></Object>`).join('');
|
|
1344
1400
|
const xmlBody = '<Delete>' + objectsXml + '</Delete>';
|
|
1345
1401
|
const query = { delete: '' };
|
|
1346
|
-
const sha256base64 =
|
|
1402
|
+
const sha256base64 = base64FromBuffer(await sha256(xmlBody));
|
|
1347
1403
|
const headers = {
|
|
1348
1404
|
[C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
|
|
1349
|
-
[C.HEADER_CONTENT_LENGTH]:
|
|
1405
|
+
[C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
|
|
1350
1406
|
[C.HEADER_AMZ_CHECKSUM_SHA256]: sha256base64,
|
|
1351
1407
|
};
|
|
1352
1408
|
|
|
@@ -1356,7 +1412,7 @@ class S3mini {
|
|
|
1356
1412
|
headers,
|
|
1357
1413
|
withQuery: true,
|
|
1358
1414
|
});
|
|
1359
|
-
const parsed =
|
|
1415
|
+
const parsed = parseXml(await res.text()) as Record<string, unknown>;
|
|
1360
1416
|
if (!parsed || typeof parsed !== 'object') {
|
|
1361
1417
|
throw new Error(`${C.ERROR_PREFIX}Failed to delete objects: ${JSON.stringify(parsed)}`);
|
|
1362
1418
|
}
|
|
@@ -1441,10 +1497,10 @@ class S3mini {
|
|
|
1441
1497
|
): Promise<Response> {
|
|
1442
1498
|
this._log('info', `Sending ${method} request to ${url}`, `headers: ${JSON.stringify(headers)}`);
|
|
1443
1499
|
try {
|
|
1444
|
-
const res = await this.
|
|
1500
|
+
const res = await this._fetch(url, {
|
|
1445
1501
|
method,
|
|
1446
1502
|
headers,
|
|
1447
|
-
body:
|
|
1503
|
+
body: method === 'GET' || method === 'HEAD' ? undefined : body,
|
|
1448
1504
|
signal: this.requestAbortTimeout ? AbortSignal.timeout(this.requestAbortTimeout) : undefined,
|
|
1449
1505
|
});
|
|
1450
1506
|
this._log('info', `Response status: ${res.status}, tolerated: ${toleratedStatusCodes.join(',')}`);
|
|
@@ -1454,9 +1510,9 @@ class S3mini {
|
|
|
1454
1510
|
await this._handleErrorResponse(res);
|
|
1455
1511
|
return res;
|
|
1456
1512
|
} catch (err: unknown) {
|
|
1457
|
-
const code =
|
|
1513
|
+
const code = extractErrCode(err);
|
|
1458
1514
|
if (code && ['ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'ECONNREFUSED'].includes(code)) {
|
|
1459
|
-
throw new
|
|
1515
|
+
throw new S3NetworkError(`S3 network error: ${code}`, code, err);
|
|
1460
1516
|
}
|
|
1461
1517
|
throw err;
|
|
1462
1518
|
}
|
|
@@ -1466,7 +1522,7 @@ class S3mini {
|
|
|
1466
1522
|
if (headers.get('content-type') !== 'application/xml') {
|
|
1467
1523
|
return {};
|
|
1468
1524
|
}
|
|
1469
|
-
const parsedBody =
|
|
1525
|
+
const parsedBody = parseXml(body);
|
|
1470
1526
|
if (
|
|
1471
1527
|
!parsedBody ||
|
|
1472
1528
|
typeof parsedBody !== 'object' ||
|
|
@@ -1492,7 +1548,7 @@ class S3mini {
|
|
|
1492
1548
|
'error',
|
|
1493
1549
|
`${C.ERROR_PREFIX}Request failed with status ${res.status}: ${svcCode} - ${errorMessage},err body: ${errorBody}`,
|
|
1494
1550
|
);
|
|
1495
|
-
throw new
|
|
1551
|
+
throw new S3ServiceError(`S3 returned ${res.status} – ${svcCode}`, res.status, svcCode, errorBody);
|
|
1496
1552
|
}
|
|
1497
1553
|
|
|
1498
1554
|
private _buildCanonicalQueryString(queryParams: Record<string, unknown>): string {
|
|
@@ -1505,10 +1561,10 @@ class S3mini {
|
|
|
1505
1561
|
.join('&');
|
|
1506
1562
|
}
|
|
1507
1563
|
private async _getSignatureKey(dateStamp: string): Promise<ArrayBuffer> {
|
|
1508
|
-
const kDate = await
|
|
1509
|
-
const kRegion = await
|
|
1510
|
-
const kService = await
|
|
1511
|
-
return await
|
|
1564
|
+
const kDate = await hmac(`AWS4${this.#secretAccessKey}`, dateStamp);
|
|
1565
|
+
const kRegion = await hmac(kDate, this.region);
|
|
1566
|
+
const kService = await hmac(kRegion, C.S3_SERVICE);
|
|
1567
|
+
return await hmac(kService, C.AWS_REQUEST_TYPE);
|
|
1512
1568
|
}
|
|
1513
1569
|
}
|
|
1514
1570
|
|
package/src/utils.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { XmlValue, XmlMap, ListBucketResponse, ErrorWithCode } from './type
|
|
|
3
3
|
|
|
4
4
|
const ENCODR = new TextEncoder();
|
|
5
5
|
const chunkSize = 0x8000; // 32KB chunks
|
|
6
|
-
const
|
|
6
|
+
const HEX_CHARS = new Uint8Array([48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102]);
|
|
7
7
|
|
|
8
8
|
export const getByteSize = (data: unknown): number => {
|
|
9
9
|
if (typeof data === 'string') {
|
|
@@ -23,13 +23,15 @@ export const getByteSize = (data: unknown): number => {
|
|
|
23
23
|
* @param {ArrayBuffer} buffer The raw bytes.
|
|
24
24
|
* @returns {string} Hexadecimal string
|
|
25
25
|
*/
|
|
26
|
+
|
|
26
27
|
export const hexFromBuffer = (buffer: ArrayBuffer): string => {
|
|
27
28
|
const bytes = new Uint8Array(buffer);
|
|
28
|
-
|
|
29
|
-
for (
|
|
30
|
-
hex
|
|
29
|
+
const hex = new Uint8Array(bytes.length * 2);
|
|
30
|
+
for (let i = 0, j = 0; i < bytes.length; i++) {
|
|
31
|
+
hex[j++] = HEX_CHARS[bytes[i]! >> 4]!;
|
|
32
|
+
hex[j++] = HEX_CHARS[bytes[i]! & 0x0f]!;
|
|
31
33
|
}
|
|
32
|
-
return hex;
|
|
34
|
+
return String.fromCharCode(...hex);
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -153,6 +155,38 @@ export const parseXml = (input: string): XmlValue => {
|
|
|
153
155
|
return Object.keys(result).length > 0 ? result : unescapeXml(xmlContent.trim());
|
|
154
156
|
};
|
|
155
157
|
|
|
158
|
+
// export const parseXml = (input: string): XmlValue => {
|
|
159
|
+
// const xml = input.replace(/<\?xml[^?]*\?>\s*/, '');
|
|
160
|
+
// const result: XmlMap = {};
|
|
161
|
+
// let i = 0;
|
|
162
|
+
// const len = xml.length;
|
|
163
|
+
|
|
164
|
+
// while (i < len) {
|
|
165
|
+
// const tagStart = xml.indexOf('<', i);
|
|
166
|
+
// if (tagStart === -1 || xml[tagStart + 1] === '/') {
|
|
167
|
+
// break;
|
|
168
|
+
// }
|
|
169
|
+
|
|
170
|
+
// const tagEnd = xml.indexOf('>', tagStart);
|
|
171
|
+
// const tag = xml.slice(tagStart + 1, tagEnd);
|
|
172
|
+
// const closeTag = `</${tag}>`;
|
|
173
|
+
// const closeIdx = xml.indexOf(closeTag, tagEnd);
|
|
174
|
+
|
|
175
|
+
// if (closeIdx === -1) {
|
|
176
|
+
// i = tagEnd + 1;
|
|
177
|
+
// continue;
|
|
178
|
+
// }
|
|
179
|
+
|
|
180
|
+
// const inner = xml.slice(tagEnd + 1, closeIdx);
|
|
181
|
+
// const node = inner.includes('<') ? parseXml(inner) : unescapeXml(inner);
|
|
182
|
+
|
|
183
|
+
// const cur = result[tag];
|
|
184
|
+
// result[tag] = cur === undefined ? node : Array.isArray(cur) ? [...cur, node] : [cur, node];
|
|
185
|
+
// i = closeIdx + closeTag.length;
|
|
186
|
+
// }
|
|
187
|
+
// return Object.keys(result).length ? result : unescapeXml(xml.trim());
|
|
188
|
+
// };
|
|
189
|
+
|
|
156
190
|
/**
|
|
157
191
|
* Encode a character as a URI percent-encoded hex value
|
|
158
192
|
* @param c Character to encode
|