s3mini 0.2.0 → 0.4.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 +14 -3
- package/dist/s3mini.d.ts +272 -15
- package/dist/s3mini.js +474 -75
- 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 +18 -18
- package/src/S3.ts +490 -77
- package/src/consts.ts +3 -3
- package/src/index.ts +3 -3
- package/src/types.ts +14 -0
- package/src/utils.ts +35 -14
package/src/S3.ts
CHANGED
|
@@ -27,7 +27,7 @@ import * as U from './utils.js';
|
|
|
27
27
|
* // Delete a file
|
|
28
28
|
* await s3.deleteObject('example.txt');
|
|
29
29
|
*/
|
|
30
|
-
class
|
|
30
|
+
class S3mini {
|
|
31
31
|
/**
|
|
32
32
|
* Creates an instance of the S3 class.
|
|
33
33
|
*
|
|
@@ -156,7 +156,7 @@ class s3mini {
|
|
|
156
156
|
private _validateMethodIsGetOrHead(method: string): void {
|
|
157
157
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
158
158
|
this._log('error', `${C.ERROR_PREFIX}method must be either GET or HEAD`);
|
|
159
|
-
throw new Error(
|
|
159
|
+
throw new Error(`${C.ERROR_PREFIX}method must be either GET or HEAD`);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -260,8 +260,17 @@ class s3mini {
|
|
|
260
260
|
headers[C.HEADER_AMZ_CONTENT_SHA256] = C.UNSIGNED_PAYLOAD; // body ? U.hash(body) : C.UNSIGNED_PAYLOAD;
|
|
261
261
|
headers[C.HEADER_AMZ_DATE] = fullDatetime;
|
|
262
262
|
headers[C.HEADER_HOST] = url.host;
|
|
263
|
-
|
|
264
|
-
const
|
|
263
|
+
// sort headers alphabetically by key
|
|
264
|
+
const ignoredHeaders = ['authorization', 'content-length', 'content-type', 'user-agent'];
|
|
265
|
+
let headersForSigning = Object.fromEntries(
|
|
266
|
+
Object.entries(headers).filter(([key]) => !ignoredHeaders.includes(key.toLowerCase())),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
headersForSigning = Object.fromEntries(
|
|
270
|
+
Object.entries(headersForSigning).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)),
|
|
271
|
+
);
|
|
272
|
+
const canonicalHeaders = this._buildCanonicalHeaders(headersForSigning);
|
|
273
|
+
const signedHeaders = Object.keys(headersForSigning)
|
|
265
274
|
.map(key => key.toLowerCase())
|
|
266
275
|
.sort()
|
|
267
276
|
.join(';');
|
|
@@ -277,7 +286,6 @@ class s3mini {
|
|
|
277
286
|
private _buildCanonicalHeaders(headers: Record<string, string | number>): string {
|
|
278
287
|
return Object.entries(headers)
|
|
279
288
|
.map(([key, value]) => `${key.toLowerCase()}:${String(value).trim()}`)
|
|
280
|
-
.sort()
|
|
281
289
|
.join('\n');
|
|
282
290
|
}
|
|
283
291
|
|
|
@@ -288,14 +296,15 @@ class s3mini {
|
|
|
288
296
|
canonicalHeaders: string,
|
|
289
297
|
signedHeaders: string,
|
|
290
298
|
): string {
|
|
291
|
-
|
|
299
|
+
const parts = [
|
|
292
300
|
method,
|
|
293
301
|
url.pathname,
|
|
294
302
|
this._buildCanonicalQueryString(query),
|
|
295
|
-
|
|
303
|
+
canonicalHeaders + '\n', // Canonical headers end with extra newline
|
|
296
304
|
signedHeaders,
|
|
297
305
|
C.UNSIGNED_PAYLOAD,
|
|
298
|
-
]
|
|
306
|
+
];
|
|
307
|
+
return parts.join('\n');
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
private _buildCredentialScope(shortDatetime: string): string {
|
|
@@ -332,9 +341,9 @@ class s3mini {
|
|
|
332
341
|
tolerated = [], // [200, 404] etc.
|
|
333
342
|
withQuery = false, // append query string to signed URL
|
|
334
343
|
}: {
|
|
335
|
-
query?: Record<string, unknown
|
|
344
|
+
query?: Record<string, unknown> | undefined;
|
|
336
345
|
body?: string | Buffer | undefined;
|
|
337
|
-
headers?: Record<string, string | number | undefined
|
|
346
|
+
headers?: Record<string, string | number | undefined> | IT.SSECHeaders | undefined;
|
|
338
347
|
tolerated?: number[] | undefined;
|
|
339
348
|
withQuery?: boolean | undefined;
|
|
340
349
|
} = {},
|
|
@@ -343,14 +352,10 @@ class s3mini {
|
|
|
343
352
|
if (!['GET', 'HEAD', 'PUT', 'POST', 'DELETE'].includes(method)) {
|
|
344
353
|
throw new Error(`${C.ERROR_PREFIX}Unsupported HTTP method ${method as string}`);
|
|
345
354
|
}
|
|
346
|
-
if (key) {
|
|
347
|
-
this._checkKey(key); // allow '' for bucket‑level
|
|
348
|
-
}
|
|
349
355
|
|
|
350
356
|
const { filteredOpts, conditionalHeaders } = ['GET', 'HEAD'].includes(method)
|
|
351
357
|
? this._filterIfHeaders(query)
|
|
352
358
|
: { filteredOpts: query, conditionalHeaders: {} };
|
|
353
|
-
|
|
354
359
|
const baseHeaders: Record<string, string | number> = {
|
|
355
360
|
[C.HEADER_AMZ_CONTENT_SHA256]: C.UNSIGNED_PAYLOAD,
|
|
356
361
|
// ...(['GET', 'HEAD'].includes(method) ? { [C.HEADER_CONTENT_TYPE]: C.JSON_CONTENT_TYPE } : {}),
|
|
@@ -374,6 +379,13 @@ class s3mini {
|
|
|
374
379
|
return this._sendRequest(finalUrl, method, signedHeadersString, body, tolerated);
|
|
375
380
|
}
|
|
376
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Gets the current configuration properties of the S3 instance.
|
|
384
|
+
* @returns {IT.S3Config} The current S3 configuration object containing all settings.
|
|
385
|
+
* @example
|
|
386
|
+
* const config = s3.getProps();
|
|
387
|
+
* console.log(config.endpoint); // 'https://s3.amazonaws.com/my-bucket'
|
|
388
|
+
*/
|
|
377
389
|
public getProps(): IT.S3Config {
|
|
378
390
|
return {
|
|
379
391
|
accessKeyId: this.accessKeyId,
|
|
@@ -385,6 +397,26 @@ class s3mini {
|
|
|
385
397
|
logger: this.logger,
|
|
386
398
|
};
|
|
387
399
|
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Updates the configuration properties of the S3 instance.
|
|
403
|
+
* @param {IT.S3Config} props - The new configuration object.
|
|
404
|
+
* @param {string} props.accessKeyId - The access key ID for authentication.
|
|
405
|
+
* @param {string} props.secretAccessKey - The secret access key for authentication.
|
|
406
|
+
* @param {string} props.endpoint - The endpoint URL of the S3-compatible service.
|
|
407
|
+
* @param {string} [props.region='auto'] - The region of the S3 service.
|
|
408
|
+
* @param {number} [props.requestSizeInBytes=8388608] - The request size of a single request in bytes.
|
|
409
|
+
* @param {number} [props.requestAbortTimeout] - The timeout in milliseconds after which a request should be aborted.
|
|
410
|
+
* @param {IT.Logger} [props.logger] - A logger object with methods like info, warn, error.
|
|
411
|
+
* @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
|
|
412
|
+
* @example
|
|
413
|
+
* s3.setProps({
|
|
414
|
+
* accessKeyId: 'new-access-key',
|
|
415
|
+
* secretAccessKey: 'new-secret-key',
|
|
416
|
+
* endpoint: 'https://new-endpoint.com/my-bucket',
|
|
417
|
+
* region: 'us-west-2' // by default is auto
|
|
418
|
+
* });
|
|
419
|
+
*/
|
|
388
420
|
public setProps(props: IT.S3Config): void {
|
|
389
421
|
this._validateConstructorParams(props.accessKeyId, props.secretAccessKey, props.endpoint);
|
|
390
422
|
this.accessKeyId = props.accessKeyId;
|
|
@@ -396,11 +428,23 @@ class s3mini {
|
|
|
396
428
|
this.logger = props.logger;
|
|
397
429
|
}
|
|
398
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Sanitizes an ETag value by removing surrounding quotes and whitespace.
|
|
433
|
+
* Still returns RFC compliant ETag. https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3
|
|
434
|
+
* @param {string} etag - The ETag value to sanitize.
|
|
435
|
+
* @returns {string} The sanitized ETag value.
|
|
436
|
+
* @example
|
|
437
|
+
* const cleanEtag = s3.sanitizeETag('"abc123"'); // Returns: 'abc123'
|
|
438
|
+
*/
|
|
399
439
|
public sanitizeETag(etag: string): string {
|
|
400
440
|
return U.sanitizeETag(etag);
|
|
401
441
|
}
|
|
402
442
|
|
|
403
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Creates a new bucket.
|
|
445
|
+
* This method sends a request to create a new bucket in the specified in endpoint.
|
|
446
|
+
* @returns A promise that resolves to true if the bucket was created successfully, false otherwise.
|
|
447
|
+
*/
|
|
404
448
|
public async createBucket(): Promise<boolean> {
|
|
405
449
|
const xmlBody = `
|
|
406
450
|
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
@@ -419,18 +463,38 @@ class s3mini {
|
|
|
419
463
|
return res.status === 200;
|
|
420
464
|
}
|
|
421
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Checks if a bucket exists.
|
|
468
|
+
* This method sends a request to check if the specified bucket exists in the S3-compatible service.
|
|
469
|
+
* @returns A promise that resolves to true if the bucket exists, false otherwise.
|
|
470
|
+
*/
|
|
422
471
|
public async bucketExists(): Promise<boolean> {
|
|
423
472
|
const res = await this._signedRequest('HEAD', '', { tolerated: [200, 404, 403] });
|
|
424
473
|
return res.status === 200;
|
|
425
474
|
}
|
|
426
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Lists objects in the bucket with optional filtering and no pagination.
|
|
478
|
+
* This method retrieves all objects matching the criteria (not paginated like listObjectsV2).
|
|
479
|
+
* @param {string} [delimiter='/'] - The delimiter to use for grouping objects.
|
|
480
|
+
* @param {string} [prefix=''] - The prefix to filter objects by.
|
|
481
|
+
* @param {number} [maxKeys] - The maximum number of keys to return. If not provided, all keys will be returned.
|
|
482
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
483
|
+
* @returns {Promise<IT.ListObject[] | null>} A promise that resolves to an array of objects or null if the bucket is empty.
|
|
484
|
+
* @example
|
|
485
|
+
* // List all objects
|
|
486
|
+
* const objects = await s3.listObjects();
|
|
487
|
+
*
|
|
488
|
+
* // List objects with prefix
|
|
489
|
+
* const photos = await s3.listObjects('/', 'photos/', 100);
|
|
490
|
+
*/
|
|
427
491
|
public async listObjects(
|
|
428
492
|
delimiter: string = '/',
|
|
429
493
|
prefix: string = '',
|
|
430
494
|
maxKeys?: number,
|
|
431
495
|
// method: IT.HttpMethod = 'GET', // 'GET' or 'HEAD'
|
|
432
496
|
opts: Record<string, unknown> = {},
|
|
433
|
-
): Promise<
|
|
497
|
+
): Promise<IT.ListObject[] | null> {
|
|
434
498
|
this._checkDelimiter(delimiter);
|
|
435
499
|
this._checkPrefix(prefix);
|
|
436
500
|
this._checkOpts(opts);
|
|
@@ -440,7 +504,7 @@ class s3mini {
|
|
|
440
504
|
const unlimited = !(maxKeys && maxKeys > 0);
|
|
441
505
|
let remaining = unlimited ? Infinity : maxKeys;
|
|
442
506
|
let token: string | undefined;
|
|
443
|
-
const all:
|
|
507
|
+
const all: IT.ListObject[] = [];
|
|
444
508
|
|
|
445
509
|
do {
|
|
446
510
|
const batchSize = Math.min(remaining, 1000); // S3 ceiling
|
|
@@ -473,26 +537,24 @@ class s3mini {
|
|
|
473
537
|
`${C.ERROR_PREFIX}Request failed with status ${res.status}: ${errorCode} - ${errorMessage}, err body: ${errorBody}`,
|
|
474
538
|
);
|
|
475
539
|
}
|
|
476
|
-
|
|
477
540
|
const raw = U.parseXml(await res.text()) as Record<string, unknown>;
|
|
478
541
|
if (typeof raw !== 'object' || !raw || 'error' in raw) {
|
|
479
542
|
this._log('error', `${C.ERROR_PREFIX}Unexpected listObjects response shape: ${JSON.stringify(raw)}`);
|
|
480
543
|
throw new Error(`${C.ERROR_PREFIX}Unexpected listObjects response shape`);
|
|
481
544
|
}
|
|
482
|
-
const out = (
|
|
483
|
-
|
|
545
|
+
const out = (raw.ListBucketResult || raw.listBucketResult || raw) as Record<string, unknown>;
|
|
484
546
|
/* accumulate Contents */
|
|
485
|
-
const contents = out.contents;
|
|
547
|
+
const contents = out.Contents || out.contents; // S3 v2 vs v1
|
|
486
548
|
if (contents) {
|
|
487
549
|
const batch = Array.isArray(contents) ? contents : [contents];
|
|
488
|
-
all.push(...(batch as
|
|
550
|
+
all.push(...(batch as IT.ListObject[]));
|
|
489
551
|
if (!unlimited) {
|
|
490
552
|
remaining -= batch.length;
|
|
491
553
|
}
|
|
492
554
|
}
|
|
493
|
-
const truncated = out.
|
|
555
|
+
const truncated = out.IsTruncated === 'true' || out.isTruncated === 'true' || false;
|
|
494
556
|
token = truncated
|
|
495
|
-
? ((out.
|
|
557
|
+
? ((out.NextContinuationToken || out.nextContinuationToken || out.NextMarker || out.nextMarker) as
|
|
496
558
|
| string
|
|
497
559
|
| undefined)
|
|
498
560
|
: undefined;
|
|
@@ -501,6 +563,15 @@ class s3mini {
|
|
|
501
563
|
return all;
|
|
502
564
|
}
|
|
503
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Lists multipart uploads in the bucket.
|
|
568
|
+
* This method sends a request to list multipart uploads in the specified bucket.
|
|
569
|
+
* @param {string} [delimiter='/'] - The delimiter to use for grouping uploads.
|
|
570
|
+
* @param {string} [prefix=''] - The prefix to filter uploads by.
|
|
571
|
+
* @param {IT.HttpMethod} [method='GET'] - The HTTP method to use for the request (GET or HEAD).
|
|
572
|
+
* @param {Record<string, string | number | boolean | undefined>} [opts={}] - Additional options for the request.
|
|
573
|
+
* @returns A promise that resolves to a list of multipart uploads or an error.
|
|
574
|
+
*/
|
|
504
575
|
public async listMultipartUploads(
|
|
505
576
|
delimiter: string = '/',
|
|
506
577
|
prefix: string = '',
|
|
@@ -527,7 +598,6 @@ class s3mini {
|
|
|
527
598
|
// etag: res.headers.get(C.HEADER_ETAG) ?? '',
|
|
528
599
|
// };
|
|
529
600
|
// }
|
|
530
|
-
|
|
531
601
|
const raw = U.parseXml(await res.text()) as unknown;
|
|
532
602
|
if (typeof raw !== 'object' || raw === null) {
|
|
533
603
|
throw new Error(`${C.ERROR_PREFIX}Unexpected listMultipartUploads response shape`);
|
|
@@ -538,44 +608,122 @@ class s3mini {
|
|
|
538
608
|
return raw as IT.MultipartUploadError;
|
|
539
609
|
}
|
|
540
610
|
|
|
541
|
-
|
|
542
|
-
|
|
611
|
+
/**
|
|
612
|
+
* Get an object from the S3-compatible service.
|
|
613
|
+
* This method sends a request to retrieve the specified object from the S3-compatible service.
|
|
614
|
+
* @param {string} key - The key of the object to retrieve.
|
|
615
|
+
* @param {Record<string, unknown>} [opts] - Additional options for the request.
|
|
616
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
617
|
+
* @returns A promise that resolves to the object data (string) or null if not found.
|
|
618
|
+
*/
|
|
619
|
+
public async getObject(
|
|
620
|
+
key: string,
|
|
621
|
+
opts: Record<string, unknown> = {},
|
|
622
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
623
|
+
): Promise<string | null> {
|
|
624
|
+
// if ssecHeaders is set, add it to headers
|
|
625
|
+
const res = await this._signedRequest('GET', key, {
|
|
626
|
+
query: opts, // use opts.query if it exists, otherwise use an empty object
|
|
627
|
+
tolerated: [200, 404, 412, 304],
|
|
628
|
+
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
629
|
+
});
|
|
543
630
|
if ([404, 412, 304].includes(res.status)) {
|
|
544
631
|
return null;
|
|
545
632
|
}
|
|
546
633
|
return res.text();
|
|
547
634
|
}
|
|
548
635
|
|
|
549
|
-
|
|
550
|
-
|
|
636
|
+
/**
|
|
637
|
+
* Get an object response from the S3-compatible service.
|
|
638
|
+
* This method sends a request to retrieve the specified object and returns the full response.
|
|
639
|
+
* @param {string} key - The key of the object to retrieve.
|
|
640
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
641
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
642
|
+
* @returns A promise that resolves to the Response object or null if not found.
|
|
643
|
+
*/
|
|
644
|
+
public async getObjectResponse(
|
|
645
|
+
key: string,
|
|
646
|
+
opts: Record<string, unknown> = {},
|
|
647
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
648
|
+
): Promise<Response | null> {
|
|
649
|
+
const res = await this._signedRequest('GET', key, {
|
|
650
|
+
query: opts,
|
|
651
|
+
tolerated: [200, 404, 412, 304],
|
|
652
|
+
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
653
|
+
});
|
|
551
654
|
if ([404, 412, 304].includes(res.status)) {
|
|
552
655
|
return null;
|
|
553
656
|
}
|
|
554
657
|
return res;
|
|
555
658
|
}
|
|
556
659
|
|
|
557
|
-
|
|
558
|
-
|
|
660
|
+
/**
|
|
661
|
+
* Get an object as an ArrayBuffer from the S3-compatible service.
|
|
662
|
+
* This method sends a request to retrieve the specified object and returns it as an ArrayBuffer.
|
|
663
|
+
* @param {string} key - The key of the object to retrieve.
|
|
664
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
665
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
666
|
+
* @returns A promise that resolves to the object data as an ArrayBuffer or null if not found.
|
|
667
|
+
*/
|
|
668
|
+
public async getObjectArrayBuffer(
|
|
669
|
+
key: string,
|
|
670
|
+
opts: Record<string, unknown> = {},
|
|
671
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
672
|
+
): Promise<ArrayBuffer | null> {
|
|
673
|
+
const res = await this._signedRequest('GET', key, {
|
|
674
|
+
query: opts,
|
|
675
|
+
tolerated: [200, 404, 412, 304],
|
|
676
|
+
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
677
|
+
});
|
|
559
678
|
if ([404, 412, 304].includes(res.status)) {
|
|
560
679
|
return null;
|
|
561
680
|
}
|
|
562
681
|
return res.arrayBuffer();
|
|
563
682
|
}
|
|
564
683
|
|
|
565
|
-
|
|
566
|
-
|
|
684
|
+
/**
|
|
685
|
+
* Get an object as JSON from the S3-compatible service.
|
|
686
|
+
* This method sends a request to retrieve the specified object and returns it as JSON.
|
|
687
|
+
* @param {string} key - The key of the object to retrieve.
|
|
688
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
689
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
690
|
+
* @returns A promise that resolves to the object data as JSON or null if not found.
|
|
691
|
+
*/
|
|
692
|
+
public async getObjectJSON<T = unknown>(
|
|
693
|
+
key: string,
|
|
694
|
+
opts: Record<string, unknown> = {},
|
|
695
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
696
|
+
): Promise<T | null> {
|
|
697
|
+
const res = await this._signedRequest('GET', key, {
|
|
698
|
+
query: opts,
|
|
699
|
+
tolerated: [200, 404, 412, 304],
|
|
700
|
+
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
701
|
+
});
|
|
567
702
|
if ([404, 412, 304].includes(res.status)) {
|
|
568
703
|
return null;
|
|
569
704
|
}
|
|
570
705
|
return res.json() as Promise<T>;
|
|
571
706
|
}
|
|
572
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Get an object with its ETag from the S3-compatible service.
|
|
710
|
+
* This method sends a request to retrieve the specified object and its ETag.
|
|
711
|
+
* @param {string} key - The key of the object to retrieve.
|
|
712
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
713
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
714
|
+
* @returns A promise that resolves to an object containing the ETag and the object data as an ArrayBuffer or null if not found.
|
|
715
|
+
*/
|
|
573
716
|
public async getObjectWithETag(
|
|
574
717
|
key: string,
|
|
575
718
|
opts: Record<string, unknown> = {},
|
|
719
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
576
720
|
): Promise<{ etag: string | null; data: ArrayBuffer | null }> {
|
|
577
721
|
try {
|
|
578
|
-
const res = await this._signedRequest('GET', key, {
|
|
722
|
+
const res = await this._signedRequest('GET', key, {
|
|
723
|
+
query: opts,
|
|
724
|
+
tolerated: [200, 404, 412, 304],
|
|
725
|
+
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
726
|
+
});
|
|
579
727
|
|
|
580
728
|
if ([404, 412, 304].includes(res.status)) {
|
|
581
729
|
return { etag: null, data: null };
|
|
@@ -583,7 +731,7 @@ class s3mini {
|
|
|
583
731
|
|
|
584
732
|
const etag = res.headers.get(C.HEADER_ETAG);
|
|
585
733
|
if (!etag) {
|
|
586
|
-
throw new Error(
|
|
734
|
+
throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
|
|
587
735
|
}
|
|
588
736
|
return { etag: U.sanitizeETag(etag), data: await res.arrayBuffer() };
|
|
589
737
|
} catch (err) {
|
|
@@ -592,28 +740,61 @@ class s3mini {
|
|
|
592
740
|
}
|
|
593
741
|
}
|
|
594
742
|
|
|
743
|
+
/**
|
|
744
|
+
* Get an object as a raw response from the S3-compatible service.
|
|
745
|
+
* This method sends a request to retrieve the specified object and returns the raw response.
|
|
746
|
+
* @param {string} key - The key of the object to retrieve.
|
|
747
|
+
* @param {boolean} [wholeFile=true] - Whether to retrieve the whole file or a range.
|
|
748
|
+
* @param {number} [rangeFrom=0] - The starting byte for the range (if not whole file).
|
|
749
|
+
* @param {number} [rangeTo=this.requestSizeInBytes] - The ending byte for the range (if not whole file).
|
|
750
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
751
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
752
|
+
* @returns A promise that resolves to the Response object.
|
|
753
|
+
*/
|
|
595
754
|
public async getObjectRaw(
|
|
596
755
|
key: string,
|
|
597
756
|
wholeFile = true,
|
|
598
757
|
rangeFrom = 0,
|
|
599
758
|
rangeTo = this.requestSizeInBytes,
|
|
600
759
|
opts: Record<string, unknown> = {},
|
|
760
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
601
761
|
): Promise<Response> {
|
|
602
762
|
const rangeHdr: Record<string, string | number> = wholeFile ? {} : { range: `bytes=${rangeFrom}-${rangeTo - 1}` };
|
|
603
763
|
|
|
604
764
|
return this._signedRequest('GET', key, {
|
|
605
765
|
query: { ...opts },
|
|
606
|
-
headers: rangeHdr,
|
|
766
|
+
headers: { ...rangeHdr, ...ssecHeaders },
|
|
607
767
|
withQuery: true, // keep ?query=string behaviour
|
|
608
768
|
});
|
|
609
769
|
}
|
|
610
770
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
771
|
+
/**
|
|
772
|
+
* Get the content length of an object.
|
|
773
|
+
* This method sends a HEAD request to retrieve the content length of the specified object.
|
|
774
|
+
* @param {string} key - The key of the object to retrieve the content length for.
|
|
775
|
+
* @returns A promise that resolves to the content length of the object in bytes, or 0 if not found.
|
|
776
|
+
* @throws {Error} If the content length header is not found in the response.
|
|
777
|
+
*/
|
|
778
|
+
public async getContentLength(key: string, ssecHeaders?: IT.SSECHeaders): Promise<number> {
|
|
779
|
+
try {
|
|
780
|
+
const res = await this._signedRequest('HEAD', key, {
|
|
781
|
+
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
782
|
+
});
|
|
783
|
+
const len = res.headers.get(C.HEADER_CONTENT_LENGTH);
|
|
784
|
+
return len ? +len : 0;
|
|
785
|
+
} catch (err) {
|
|
786
|
+
this._log('error', `Error getting content length for object ${key}: ${String(err)}`);
|
|
787
|
+
throw new Error(`${C.ERROR_PREFIX}Error getting content length for object ${key}: ${String(err)}`);
|
|
788
|
+
}
|
|
615
789
|
}
|
|
616
790
|
|
|
791
|
+
/**
|
|
792
|
+
* Checks if an object exists in the S3-compatible service.
|
|
793
|
+
* This method sends a HEAD request to check if the specified object exists.
|
|
794
|
+
* @param {string} key - The key of the object to check.
|
|
795
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
796
|
+
* @returns A promise that resolves to true if the object exists, false if not found, or null if ETag mismatch.
|
|
797
|
+
*/
|
|
617
798
|
public async objectExists(key: string, opts: Record<string, unknown> = {}): Promise<IT.ExistResponseCode> {
|
|
618
799
|
const res = await this._signedRequest('HEAD', key, {
|
|
619
800
|
query: opts,
|
|
@@ -629,28 +810,67 @@ class s3mini {
|
|
|
629
810
|
return true; // found (200)
|
|
630
811
|
}
|
|
631
812
|
|
|
632
|
-
|
|
813
|
+
/**
|
|
814
|
+
* Retrieves the ETag of an object without downloading its content.
|
|
815
|
+
* @param {string} key - The key of the object to retrieve the ETag for.
|
|
816
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
817
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
818
|
+
* @returns {Promise<string | null>} A promise that resolves to the ETag value or null if the object is not found.
|
|
819
|
+
* @throws {Error} If the ETag header is not found in the response.
|
|
820
|
+
* @example
|
|
821
|
+
* const etag = await s3.getEtag('path/to/file.txt');
|
|
822
|
+
* if (etag) {
|
|
823
|
+
* console.log(`File ETag: ${etag}`);
|
|
824
|
+
* }
|
|
825
|
+
*/
|
|
826
|
+
public async getEtag(
|
|
827
|
+
key: string,
|
|
828
|
+
opts: Record<string, unknown> = {},
|
|
829
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
830
|
+
): Promise<string | null> {
|
|
633
831
|
const res = await this._signedRequest('HEAD', key, {
|
|
634
832
|
query: opts,
|
|
635
|
-
tolerated: [200, 404],
|
|
833
|
+
tolerated: [200, 304, 404, 412],
|
|
834
|
+
headers: ssecHeaders ? { ...ssecHeaders } : undefined,
|
|
636
835
|
});
|
|
637
836
|
|
|
638
837
|
if (res.status === 404) {
|
|
639
838
|
return null;
|
|
640
839
|
}
|
|
641
840
|
|
|
841
|
+
if (res.status === 412 || res.status === 304) {
|
|
842
|
+
return null; // ETag mismatch
|
|
843
|
+
}
|
|
844
|
+
|
|
642
845
|
const etag = res.headers.get(C.HEADER_ETAG);
|
|
643
846
|
if (!etag) {
|
|
644
|
-
throw new Error(
|
|
847
|
+
throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
|
|
645
848
|
}
|
|
646
849
|
|
|
647
850
|
return U.sanitizeETag(etag);
|
|
648
851
|
}
|
|
649
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Uploads an object to the S3-compatible service.
|
|
855
|
+
* @param {string} key - The key/path where the object will be stored.
|
|
856
|
+
* @param {string | Buffer} data - The data to upload (string or Buffer).
|
|
857
|
+
* @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
|
|
858
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
859
|
+
* @returns {Promise<Response>} A promise that resolves to the Response object from the upload request.
|
|
860
|
+
* @throws {TypeError} If data is not a string or Buffer.
|
|
861
|
+
* @example
|
|
862
|
+
* // Upload text file
|
|
863
|
+
* await s3.putObject('hello.txt', 'Hello, World!', 'text/plain');
|
|
864
|
+
*
|
|
865
|
+
* // Upload binary data
|
|
866
|
+
* const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
|
867
|
+
* await s3.putObject('image.png', buffer, 'image/png');
|
|
868
|
+
*/
|
|
650
869
|
public async putObject(
|
|
651
870
|
key: string,
|
|
652
871
|
data: string | Buffer,
|
|
653
872
|
fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
|
|
873
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
654
874
|
): Promise<Response> {
|
|
655
875
|
if (!(data instanceof Buffer || typeof data === 'string')) {
|
|
656
876
|
throw new TypeError(C.ERROR_DATA_BUFFER_REQUIRED);
|
|
@@ -660,46 +880,98 @@ class s3mini {
|
|
|
660
880
|
headers: {
|
|
661
881
|
[C.HEADER_CONTENT_LENGTH]: typeof data === 'string' ? Buffer.byteLength(data) : data.length,
|
|
662
882
|
[C.HEADER_CONTENT_TYPE]: fileType,
|
|
883
|
+
...ssecHeaders,
|
|
663
884
|
},
|
|
664
885
|
tolerated: [200],
|
|
665
886
|
});
|
|
666
887
|
}
|
|
667
888
|
|
|
668
|
-
|
|
889
|
+
/**
|
|
890
|
+
* Initiates a multipart upload and returns the upload ID.
|
|
891
|
+
* @param {string} key - The key/path where the object will be stored.
|
|
892
|
+
* @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
|
|
893
|
+
* @param {IT.SSECHeaders?} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
894
|
+
* @returns {Promise<string>} A promise that resolves to the upload ID for the multipart upload.
|
|
895
|
+
* @throws {TypeError} If key is invalid or fileType is not a string.
|
|
896
|
+
* @throws {Error} If the multipart upload fails to initialize.
|
|
897
|
+
* @example
|
|
898
|
+
* const uploadId = await s3.getMultipartUploadId('large-file.zip', 'application/zip');
|
|
899
|
+
* console.log(`Started multipart upload: ${uploadId}`);
|
|
900
|
+
*/
|
|
901
|
+
public async getMultipartUploadId(
|
|
902
|
+
key: string,
|
|
903
|
+
fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
|
|
904
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
905
|
+
): Promise<string> {
|
|
669
906
|
this._checkKey(key);
|
|
670
907
|
if (typeof fileType !== 'string') {
|
|
671
908
|
throw new TypeError(`${C.ERROR_PREFIX}fileType must be a string`);
|
|
672
909
|
}
|
|
673
910
|
const query = { uploads: '' };
|
|
674
|
-
const headers = { [C.HEADER_CONTENT_TYPE]: fileType };
|
|
911
|
+
const headers = { [C.HEADER_CONTENT_TYPE]: fileType, ...ssecHeaders };
|
|
675
912
|
|
|
676
913
|
const res = await this._signedRequest('POST', key, {
|
|
677
914
|
query,
|
|
678
915
|
headers,
|
|
679
916
|
withQuery: true,
|
|
680
917
|
});
|
|
918
|
+
const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
|
|
919
|
+
|
|
920
|
+
// if (
|
|
921
|
+
// parsed &&
|
|
922
|
+
// typeof parsed === 'object' &&
|
|
923
|
+
// 'initiateMultipartUploadResult' in parsed &&
|
|
924
|
+
// parsed.initiateMultipartUploadResult &&
|
|
925
|
+
// 'uploadId' in (parsed.initiateMultipartUploadResult as { uploadId: string })
|
|
926
|
+
// ) {
|
|
927
|
+
// return (parsed.initiateMultipartUploadResult as { uploadId: string }).uploadId;
|
|
928
|
+
// }
|
|
681
929
|
|
|
682
|
-
|
|
930
|
+
if (parsed && typeof parsed === 'object') {
|
|
931
|
+
// Check for both cases of InitiateMultipartUploadResult
|
|
932
|
+
const uploadResult =
|
|
933
|
+
(parsed.initiateMultipartUploadResult as Record<string, unknown>) ||
|
|
934
|
+
(parsed.InitiateMultipartUploadResult as Record<string, unknown>);
|
|
683
935
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
936
|
+
if (uploadResult && typeof uploadResult === 'object') {
|
|
937
|
+
// Check for both cases of uploadId
|
|
938
|
+
const uploadId = uploadResult.uploadId || uploadResult.UploadId;
|
|
939
|
+
|
|
940
|
+
if (uploadId && typeof uploadId === 'string') {
|
|
941
|
+
return uploadId;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
692
944
|
}
|
|
693
945
|
|
|
694
946
|
throw new Error(`${C.ERROR_PREFIX}Failed to create multipart upload: ${JSON.stringify(parsed)}`);
|
|
695
947
|
}
|
|
696
948
|
|
|
949
|
+
/**
|
|
950
|
+
* Uploads a part in a multipart upload.
|
|
951
|
+
* @param {string} key - The key of the object being uploaded.
|
|
952
|
+
* @param {string} uploadId - The upload ID from getMultipartUploadId.
|
|
953
|
+
* @param {Buffer | string} data - The data for this part.
|
|
954
|
+
* @param {number} partNumber - The part number (must be between 1 and 10,000).
|
|
955
|
+
* @param {Record<string, unknown>} [opts={}] - Additional options for the request.
|
|
956
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
957
|
+
* @returns {Promise<IT.UploadPart>} A promise that resolves to an object containing the partNumber and etag.
|
|
958
|
+
* @throws {TypeError} If any parameter is invalid.
|
|
959
|
+
* @example
|
|
960
|
+
* const part = await s3.uploadPart(
|
|
961
|
+
* 'large-file.zip',
|
|
962
|
+
* uploadId,
|
|
963
|
+
* partData,
|
|
964
|
+
* 1
|
|
965
|
+
* );
|
|
966
|
+
* console.log(`Part ${part.partNumber} uploaded with ETag: ${part.etag}`);
|
|
967
|
+
*/
|
|
697
968
|
public async uploadPart(
|
|
698
969
|
key: string,
|
|
699
970
|
uploadId: string,
|
|
700
971
|
data: Buffer | string,
|
|
701
972
|
partNumber: number,
|
|
702
973
|
opts: Record<string, unknown> = {},
|
|
974
|
+
ssecHeaders?: IT.SSECHeaders,
|
|
703
975
|
): Promise<IT.UploadPart> {
|
|
704
976
|
this._validateUploadPartParams(key, uploadId, data, partNumber, opts);
|
|
705
977
|
|
|
@@ -707,19 +979,38 @@ class s3mini {
|
|
|
707
979
|
const res = await this._signedRequest('PUT', key, {
|
|
708
980
|
query,
|
|
709
981
|
body: data,
|
|
710
|
-
headers: {
|
|
982
|
+
headers: {
|
|
983
|
+
[C.HEADER_CONTENT_LENGTH]: typeof data === 'string' ? Buffer.byteLength(data) : data.length,
|
|
984
|
+
...ssecHeaders,
|
|
985
|
+
},
|
|
711
986
|
});
|
|
712
987
|
|
|
713
988
|
return { partNumber, etag: U.sanitizeETag(res.headers.get('etag') || '') };
|
|
714
989
|
}
|
|
715
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Completes a multipart upload by combining all uploaded parts.
|
|
993
|
+
* @param {string} key - The key of the object being uploaded.
|
|
994
|
+
* @param {string} uploadId - The upload ID from getMultipartUploadId.
|
|
995
|
+
* @param {Array<IT.UploadPart>} parts - Array of uploaded parts with partNumber and etag.
|
|
996
|
+
* @returns {Promise<IT.CompleteMultipartUploadResult>} A promise that resolves to the completion result containing the final ETag.
|
|
997
|
+
* @throws {Error} If the multipart upload fails to complete.
|
|
998
|
+
* @example
|
|
999
|
+
* const result = await s3.completeMultipartUpload(
|
|
1000
|
+
* 'large-file.zip',
|
|
1001
|
+
* uploadId,
|
|
1002
|
+
* [
|
|
1003
|
+
* { partNumber: 1, etag: 'abc123' },
|
|
1004
|
+
* { partNumber: 2, etag: 'def456' }
|
|
1005
|
+
* ]
|
|
1006
|
+
* );
|
|
1007
|
+
* console.log(`Upload completed with ETag: ${result.etag}`);
|
|
1008
|
+
*/
|
|
716
1009
|
public async completeMultipartUpload(
|
|
717
1010
|
key: string,
|
|
718
1011
|
uploadId: string,
|
|
719
1012
|
parts: Array<IT.UploadPart>,
|
|
720
1013
|
): Promise<IT.CompleteMultipartUploadResult> {
|
|
721
|
-
// …existing validation left untouched …
|
|
722
|
-
|
|
723
1014
|
const query = { uploadId };
|
|
724
1015
|
const xmlBody = this._buildCompleteMultipartUploadXml(parts);
|
|
725
1016
|
const headers = {
|
|
@@ -734,40 +1025,61 @@ class s3mini {
|
|
|
734
1025
|
withQuery: true,
|
|
735
1026
|
});
|
|
736
1027
|
|
|
737
|
-
const parsed = U.parseXml(await res.text()) as unknown
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1028
|
+
const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
|
|
1029
|
+
if (parsed && typeof parsed === 'object') {
|
|
1030
|
+
// Check for both cases
|
|
1031
|
+
const result = parsed.completeMultipartUploadResult || parsed.CompleteMultipartUploadResult || parsed;
|
|
1032
|
+
|
|
1033
|
+
if (result && typeof result === 'object') {
|
|
1034
|
+
const resultObj = result as Record<string, unknown>;
|
|
1035
|
+
|
|
1036
|
+
// Handle ETag in all its variations
|
|
1037
|
+
const etag = resultObj.ETag || resultObj.eTag || resultObj.etag;
|
|
1038
|
+
if (etag && typeof etag === 'string') {
|
|
1039
|
+
return {
|
|
1040
|
+
...resultObj,
|
|
1041
|
+
etag: this.sanitizeETag(etag),
|
|
1042
|
+
} as IT.CompleteMultipartUploadResult;
|
|
1043
|
+
}
|
|
743
1044
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
if ('ETag' in result || 'eTag' in result) {
|
|
748
|
-
(result as IT.CompleteMultipartUploadResult).etag = this.sanitizeETag(
|
|
749
|
-
(result as IT.CompleteMultipartUploadResult).eTag ?? (result as IT.CompleteMultipartUploadResult).ETag,
|
|
750
|
-
);
|
|
1045
|
+
return result as IT.CompleteMultipartUploadResult;
|
|
1046
|
+
}
|
|
751
1047
|
}
|
|
752
|
-
|
|
1048
|
+
|
|
1049
|
+
throw new Error(`${C.ERROR_PREFIX}Failed to complete multipart upload: ${JSON.stringify(parsed)}`);
|
|
753
1050
|
}
|
|
754
1051
|
|
|
755
|
-
|
|
1052
|
+
/**
|
|
1053
|
+
* Aborts a multipart upload and removes all uploaded parts.
|
|
1054
|
+
* @param {string} key - The key of the object being uploaded.
|
|
1055
|
+
* @param {string} uploadId - The upload ID to abort.
|
|
1056
|
+
* @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
|
|
1057
|
+
* @returns {Promise<object>} A promise that resolves to an object containing the abort status and details.
|
|
1058
|
+
* @throws {TypeError} If key or uploadId is invalid.
|
|
1059
|
+
* @throws {Error} If the abort operation fails.
|
|
1060
|
+
* @example
|
|
1061
|
+
* try {
|
|
1062
|
+
* const result = await s3.abortMultipartUpload('large-file.zip', uploadId);
|
|
1063
|
+
* console.log('Upload aborted:', result.status);
|
|
1064
|
+
* } catch (error) {
|
|
1065
|
+
* console.error('Failed to abort upload:', error);
|
|
1066
|
+
* }
|
|
1067
|
+
*/
|
|
1068
|
+
public async abortMultipartUpload(key: string, uploadId: string, ssecHeaders?: IT.SSECHeaders): Promise<object> {
|
|
756
1069
|
this._checkKey(key);
|
|
757
1070
|
if (!uploadId) {
|
|
758
1071
|
throw new TypeError(C.ERROR_UPLOAD_ID_REQUIRED);
|
|
759
1072
|
}
|
|
760
1073
|
|
|
761
1074
|
const query = { uploadId };
|
|
762
|
-
const headers = { [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE };
|
|
1075
|
+
const headers = { [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE, ...(ssecHeaders ? { ...ssecHeaders } : {}) };
|
|
763
1076
|
|
|
764
1077
|
const res = await this._signedRequest('DELETE', key, {
|
|
765
1078
|
query,
|
|
766
1079
|
headers,
|
|
767
1080
|
withQuery: true,
|
|
768
1081
|
});
|
|
769
|
-
|
|
770
|
-
const parsed = U.parseXml(await res.text()) as object;
|
|
1082
|
+
const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
|
|
771
1083
|
if (
|
|
772
1084
|
parsed &&
|
|
773
1085
|
'error' in parsed &&
|
|
@@ -798,11 +1110,107 @@ class s3mini {
|
|
|
798
1110
|
`;
|
|
799
1111
|
}
|
|
800
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* Deletes an object from the bucket.
|
|
1115
|
+
* This method sends a request to delete the specified object from the bucket.
|
|
1116
|
+
* @param {string} key - The key of the object to delete.
|
|
1117
|
+
* @returns A promise that resolves to true if the object was deleted successfully, false otherwise.
|
|
1118
|
+
*/
|
|
801
1119
|
public async deleteObject(key: string): Promise<boolean> {
|
|
802
1120
|
const res = await this._signedRequest('DELETE', key, { tolerated: [200, 204] });
|
|
803
1121
|
return res.status === 200 || res.status === 204;
|
|
804
1122
|
}
|
|
805
1123
|
|
|
1124
|
+
private async _deleteObjectsProcess(keys: string[]): Promise<boolean[]> {
|
|
1125
|
+
const xmlBody = `<Delete>${keys.map(key => `<Object><Key>${U.escapeXml(key)}</Key></Object>`).join('')}</Delete>`;
|
|
1126
|
+
const query = { delete: '' };
|
|
1127
|
+
const md5Base64 = U.md5base64(xmlBody);
|
|
1128
|
+
const headers = {
|
|
1129
|
+
[C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
|
|
1130
|
+
[C.HEADER_CONTENT_LENGTH]: Buffer.byteLength(xmlBody).toString(),
|
|
1131
|
+
'Content-MD5': md5Base64,
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const res = await this._signedRequest('POST', '', {
|
|
1135
|
+
query,
|
|
1136
|
+
body: xmlBody,
|
|
1137
|
+
headers,
|
|
1138
|
+
withQuery: true,
|
|
1139
|
+
});
|
|
1140
|
+
const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
|
|
1141
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1142
|
+
throw new Error(`${C.ERROR_PREFIX}Failed to delete objects: ${JSON.stringify(parsed)}`);
|
|
1143
|
+
}
|
|
1144
|
+
const out = (parsed.DeleteResult || parsed.deleteResult || parsed) as Record<string, unknown>;
|
|
1145
|
+
const resultMap = new Map<string, boolean>();
|
|
1146
|
+
keys.forEach(key => resultMap.set(key, false));
|
|
1147
|
+
const deleted = out.deleted || out.Deleted;
|
|
1148
|
+
if (deleted) {
|
|
1149
|
+
const deletedArray = Array.isArray(deleted) ? deleted : [deleted];
|
|
1150
|
+
deletedArray.forEach((item: unknown) => {
|
|
1151
|
+
if (item && typeof item === 'object') {
|
|
1152
|
+
const obj = item as Record<string, unknown>;
|
|
1153
|
+
// Check both key and Key
|
|
1154
|
+
const key = obj.key || obj.Key;
|
|
1155
|
+
if (key && typeof key === 'string') {
|
|
1156
|
+
resultMap.set(key, true);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Handle errors (check both cases)
|
|
1163
|
+
const errors = out.error || out.Error;
|
|
1164
|
+
if (errors) {
|
|
1165
|
+
const errorsArray = Array.isArray(errors) ? errors : [errors];
|
|
1166
|
+
errorsArray.forEach((item: unknown) => {
|
|
1167
|
+
if (item && typeof item === 'object') {
|
|
1168
|
+
const obj = item as Record<string, unknown>;
|
|
1169
|
+
// Check both cases for all properties
|
|
1170
|
+
const key = obj.key || obj.Key;
|
|
1171
|
+
const code = obj.code || obj.Code;
|
|
1172
|
+
const message = obj.message || obj.Message;
|
|
1173
|
+
|
|
1174
|
+
if (key && typeof key === 'string') {
|
|
1175
|
+
resultMap.set(key, false);
|
|
1176
|
+
// Optionally log the error for debugging
|
|
1177
|
+
this._log('warn', `Failed to delete object: ${key}`, {
|
|
1178
|
+
code: code || 'Unknown',
|
|
1179
|
+
message: message || 'Unknown error',
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Return boolean array in the same order as input keys
|
|
1187
|
+
return keys.map(key => resultMap.get(key) || false);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Deletes multiple objects from the bucket.
|
|
1192
|
+
* @param {string[]} keys - An array of object keys to delete.
|
|
1193
|
+
* @returns A promise that resolves to an array of booleans indicating success for each key in order.
|
|
1194
|
+
*/
|
|
1195
|
+
public async deleteObjects(keys: string[]): Promise<boolean[]> {
|
|
1196
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
1197
|
+
return [];
|
|
1198
|
+
}
|
|
1199
|
+
const maxBatchSize = 1000; // S3 limit for delete batch size
|
|
1200
|
+
if (keys.length > maxBatchSize) {
|
|
1201
|
+
const allPromises = [];
|
|
1202
|
+
for (let i = 0; i < keys.length; i += maxBatchSize) {
|
|
1203
|
+
const batch = keys.slice(i, i + maxBatchSize);
|
|
1204
|
+
allPromises.push(this._deleteObjectsProcess(batch));
|
|
1205
|
+
}
|
|
1206
|
+
const results = await Promise.all(allPromises);
|
|
1207
|
+
// Flatten the results array
|
|
1208
|
+
return results.flat();
|
|
1209
|
+
} else {
|
|
1210
|
+
return await this._deleteObjectsProcess(keys);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
806
1214
|
private async _sendRequest(
|
|
807
1215
|
url: string,
|
|
808
1216
|
method: IT.HttpMethod,
|
|
@@ -860,5 +1268,10 @@ class s3mini {
|
|
|
860
1268
|
}
|
|
861
1269
|
}
|
|
862
1270
|
|
|
863
|
-
|
|
864
|
-
|
|
1271
|
+
/**
|
|
1272
|
+
* @deprecated Use `S3mini` instead.
|
|
1273
|
+
*/
|
|
1274
|
+
const s3mini = S3mini;
|
|
1275
|
+
|
|
1276
|
+
export { S3mini, s3mini };
|
|
1277
|
+
export default S3mini;
|