s3mini 0.2.0 → 0.3.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/src/S3.ts CHANGED
@@ -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('method must be either GET or HEAD');
159
+ throw new Error(`${C.ERROR_PREFIX}method must be either GET or HEAD`);
160
160
  }
161
161
  }
162
162
 
@@ -374,6 +374,13 @@ class s3mini {
374
374
  return this._sendRequest(finalUrl, method, signedHeadersString, body, tolerated);
375
375
  }
376
376
 
377
+ /**
378
+ * Gets the current configuration properties of the S3 instance.
379
+ * @returns {IT.S3Config} The current S3 configuration object containing all settings.
380
+ * @example
381
+ * const config = s3.getProps();
382
+ * console.log(config.endpoint); // 'https://s3.amazonaws.com/my-bucket'
383
+ */
377
384
  public getProps(): IT.S3Config {
378
385
  return {
379
386
  accessKeyId: this.accessKeyId,
@@ -385,6 +392,26 @@ class s3mini {
385
392
  logger: this.logger,
386
393
  };
387
394
  }
395
+
396
+ /**
397
+ * Updates the configuration properties of the S3 instance.
398
+ * @param {IT.S3Config} props - The new configuration object.
399
+ * @param {string} props.accessKeyId - The access key ID for authentication.
400
+ * @param {string} props.secretAccessKey - The secret access key for authentication.
401
+ * @param {string} props.endpoint - The endpoint URL of the S3-compatible service.
402
+ * @param {string} [props.region='auto'] - The region of the S3 service.
403
+ * @param {number} [props.requestSizeInBytes=8388608] - The request size of a single request in bytes.
404
+ * @param {number} [props.requestAbortTimeout] - The timeout in milliseconds after which a request should be aborted.
405
+ * @param {IT.Logger} [props.logger] - A logger object with methods like info, warn, error.
406
+ * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
407
+ * @example
408
+ * s3.setProps({
409
+ * accessKeyId: 'new-access-key',
410
+ * secretAccessKey: 'new-secret-key',
411
+ * endpoint: 'https://new-endpoint.com/my-bucket',
412
+ * region: 'us-west-2' // by default is auto
413
+ * });
414
+ */
388
415
  public setProps(props: IT.S3Config): void {
389
416
  this._validateConstructorParams(props.accessKeyId, props.secretAccessKey, props.endpoint);
390
417
  this.accessKeyId = props.accessKeyId;
@@ -396,11 +423,23 @@ class s3mini {
396
423
  this.logger = props.logger;
397
424
  }
398
425
 
426
+ /**
427
+ * Sanitizes an ETag value by removing surrounding quotes and whitespace.
428
+ * Still returns RFC compliant ETag. https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3
429
+ * @param {string} etag - The ETag value to sanitize.
430
+ * @returns {string} The sanitized ETag value.
431
+ * @example
432
+ * const cleanEtag = s3.sanitizeETag('"abc123"'); // Returns: 'abc123'
433
+ */
399
434
  public sanitizeETag(etag: string): string {
400
435
  return U.sanitizeETag(etag);
401
436
  }
402
437
 
403
- // TBD
438
+ /**
439
+ * Creates a new bucket.
440
+ * This method sends a request to create a new bucket in the specified in endpoint.
441
+ * @returns A promise that resolves to true if the bucket was created successfully, false otherwise.
442
+ */
404
443
  public async createBucket(): Promise<boolean> {
405
444
  const xmlBody = `
406
445
  <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
@@ -419,11 +458,31 @@ class s3mini {
419
458
  return res.status === 200;
420
459
  }
421
460
 
461
+ /**
462
+ * Checks if a bucket exists.
463
+ * This method sends a request to check if the specified bucket exists in the S3-compatible service.
464
+ * @returns A promise that resolves to true if the bucket exists, false otherwise.
465
+ */
422
466
  public async bucketExists(): Promise<boolean> {
423
467
  const res = await this._signedRequest('HEAD', '', { tolerated: [200, 404, 403] });
424
468
  return res.status === 200;
425
469
  }
426
470
 
471
+ /**
472
+ * Lists objects in the bucket with optional filtering and no pagination.
473
+ * This method retrieves all objects matching the criteria (not paginated like listObjectsV2).
474
+ * @param {string} [delimiter='/'] - The delimiter to use for grouping objects.
475
+ * @param {string} [prefix=''] - The prefix to filter objects by.
476
+ * @param {number} [maxKeys] - The maximum number of keys to return. If not provided, all keys will be returned.
477
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
478
+ * @returns {Promise<object[] | null>} A promise that resolves to an array of objects or null if the bucket is empty.
479
+ * @example
480
+ * // List all objects
481
+ * const objects = await s3.listObjects();
482
+ *
483
+ * // List objects with prefix
484
+ * const photos = await s3.listObjects('/', 'photos/', 100);
485
+ */
427
486
  public async listObjects(
428
487
  delimiter: string = '/',
429
488
  prefix: string = '',
@@ -473,16 +532,14 @@ class s3mini {
473
532
  `${C.ERROR_PREFIX}Request failed with status ${res.status}: ${errorCode} - ${errorMessage}, err body: ${errorBody}`,
474
533
  );
475
534
  }
476
-
477
535
  const raw = U.parseXml(await res.text()) as Record<string, unknown>;
478
536
  if (typeof raw !== 'object' || !raw || 'error' in raw) {
479
537
  this._log('error', `${C.ERROR_PREFIX}Unexpected listObjects response shape: ${JSON.stringify(raw)}`);
480
538
  throw new Error(`${C.ERROR_PREFIX}Unexpected listObjects response shape`);
481
539
  }
482
- const out = ('listBucketResult' in raw ? raw.listBucketResult : raw) as Record<string, unknown>;
483
-
540
+ const out = (raw.ListBucketResult || raw.listBucketResult || raw) as Record<string, unknown>;
484
541
  /* accumulate Contents */
485
- const contents = out.contents;
542
+ const contents = out.Contents || out.contents; // S3 v2 vs v1
486
543
  if (contents) {
487
544
  const batch = Array.isArray(contents) ? contents : [contents];
488
545
  all.push(...(batch as object[]));
@@ -490,9 +547,9 @@ class s3mini {
490
547
  remaining -= batch.length;
491
548
  }
492
549
  }
493
- const truncated = out.isTruncated === 'true' || out.IsTruncated === 'true';
550
+ const truncated = out.IsTruncated === 'true' || out.isTruncated === 'true' || false;
494
551
  token = truncated
495
- ? ((out.nextContinuationToken || out.NextContinuationToken || out.nextMarker || out.NextMarker) as
552
+ ? ((out.NextContinuationToken || out.nextContinuationToken || out.NextMarker || out.nextMarker) as
496
553
  | string
497
554
  | undefined)
498
555
  : undefined;
@@ -501,6 +558,15 @@ class s3mini {
501
558
  return all;
502
559
  }
503
560
 
561
+ /**
562
+ * Lists multipart uploads in the bucket.
563
+ * This method sends a request to list multipart uploads in the specified bucket.
564
+ * @param {string} [delimiter='/'] - The delimiter to use for grouping uploads.
565
+ * @param {string} [prefix=''] - The prefix to filter uploads by.
566
+ * @param {IT.HttpMethod} [method='GET'] - The HTTP method to use for the request (GET or HEAD).
567
+ * @param {Record<string, string | number | boolean | undefined>} [opts={}] - Additional options for the request.
568
+ * @returns A promise that resolves to a list of multipart uploads or an error.
569
+ */
504
570
  public async listMultipartUploads(
505
571
  delimiter: string = '/',
506
572
  prefix: string = '',
@@ -527,7 +593,6 @@ class s3mini {
527
593
  // etag: res.headers.get(C.HEADER_ETAG) ?? '',
528
594
  // };
529
595
  // }
530
-
531
596
  const raw = U.parseXml(await res.text()) as unknown;
532
597
  if (typeof raw !== 'object' || raw === null) {
533
598
  throw new Error(`${C.ERROR_PREFIX}Unexpected listMultipartUploads response shape`);
@@ -538,6 +603,13 @@ class s3mini {
538
603
  return raw as IT.MultipartUploadError;
539
604
  }
540
605
 
606
+ /**
607
+ * Get an object from the S3-compatible service.
608
+ * This method sends a request to retrieve the specified object from the S3-compatible service.
609
+ * @param {string} key - The key of the object to retrieve.
610
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
611
+ * @returns A promise that resolves to the object data (string) or null if not found.
612
+ */
541
613
  public async getObject(key: string, opts: Record<string, unknown> = {}): Promise<string | null> {
542
614
  const res = await this._signedRequest('GET', key, { query: opts, tolerated: [200, 404, 412, 304] });
543
615
  if ([404, 412, 304].includes(res.status)) {
@@ -546,6 +618,13 @@ class s3mini {
546
618
  return res.text();
547
619
  }
548
620
 
621
+ /**
622
+ * Get an object response from the S3-compatible service.
623
+ * This method sends a request to retrieve the specified object and returns the full response.
624
+ * @param {string} key - The key of the object to retrieve.
625
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
626
+ * @returns A promise that resolves to the Response object or null if not found.
627
+ */
549
628
  public async getObjectResponse(key: string, opts: Record<string, unknown> = {}): Promise<Response | null> {
550
629
  const res = await this._signedRequest('GET', key, { query: opts, tolerated: [200, 404, 412, 304] });
551
630
  if ([404, 412, 304].includes(res.status)) {
@@ -554,6 +633,13 @@ class s3mini {
554
633
  return res;
555
634
  }
556
635
 
636
+ /**
637
+ * Get an object as an ArrayBuffer from the S3-compatible service.
638
+ * This method sends a request to retrieve the specified object and returns it as an ArrayBuffer.
639
+ * @param {string} key - The key of the object to retrieve.
640
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
641
+ * @returns A promise that resolves to the object data as an ArrayBuffer or null if not found.
642
+ */
557
643
  public async getObjectArrayBuffer(key: string, opts: Record<string, unknown> = {}): Promise<ArrayBuffer | null> {
558
644
  const res = await this._signedRequest('GET', key, { query: opts, tolerated: [200, 404, 412, 304] });
559
645
  if ([404, 412, 304].includes(res.status)) {
@@ -562,6 +648,13 @@ class s3mini {
562
648
  return res.arrayBuffer();
563
649
  }
564
650
 
651
+ /**
652
+ * Get an object as JSON from the S3-compatible service.
653
+ * This method sends a request to retrieve the specified object and returns it as JSON.
654
+ * @param {string} key - The key of the object to retrieve.
655
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
656
+ * @returns A promise that resolves to the object data as JSON or null if not found.
657
+ */
565
658
  public async getObjectJSON<T = unknown>(key: string, opts: Record<string, unknown> = {}): Promise<T | null> {
566
659
  const res = await this._signedRequest('GET', key, { query: opts, tolerated: [200, 404, 412, 304] });
567
660
  if ([404, 412, 304].includes(res.status)) {
@@ -570,6 +663,13 @@ class s3mini {
570
663
  return res.json() as Promise<T>;
571
664
  }
572
665
 
666
+ /**
667
+ * Get an object with its ETag from the S3-compatible service.
668
+ * This method sends a request to retrieve the specified object and its ETag.
669
+ * @param {string} key - The key of the object to retrieve.
670
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
671
+ * @returns A promise that resolves to an object containing the ETag and the object data as an ArrayBuffer or null if not found.
672
+ */
573
673
  public async getObjectWithETag(
574
674
  key: string,
575
675
  opts: Record<string, unknown> = {},
@@ -583,7 +683,7 @@ class s3mini {
583
683
 
584
684
  const etag = res.headers.get(C.HEADER_ETAG);
585
685
  if (!etag) {
586
- throw new Error('ETag not found in response headers');
686
+ throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
587
687
  }
588
688
  return { etag: U.sanitizeETag(etag), data: await res.arrayBuffer() };
589
689
  } catch (err) {
@@ -592,6 +692,16 @@ class s3mini {
592
692
  }
593
693
  }
594
694
 
695
+ /**
696
+ * Get an object as a raw response from the S3-compatible service.
697
+ * This method sends a request to retrieve the specified object and returns the raw response.
698
+ * @param {string} key - The key of the object to retrieve.
699
+ * @param {boolean} [wholeFile=true] - Whether to retrieve the whole file or a range.
700
+ * @param {number} [rangeFrom=0] - The starting byte for the range (if not whole file).
701
+ * @param {number} [rangeTo=this.requestSizeInBytes] - The ending byte for the range (if not whole file).
702
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
703
+ * @returns A promise that resolves to the Response object.
704
+ */
595
705
  public async getObjectRaw(
596
706
  key: string,
597
707
  wholeFile = true,
@@ -608,12 +718,31 @@ class s3mini {
608
718
  });
609
719
  }
610
720
 
721
+ /**
722
+ * Get the content length of an object.
723
+ * This method sends a HEAD request to retrieve the content length of the specified object.
724
+ * @param {string} key - The key of the object to retrieve the content length for.
725
+ * @returns A promise that resolves to the content length of the object in bytes, or 0 if not found.
726
+ * @throws {Error} If the content length header is not found in the response.
727
+ */
611
728
  public async getContentLength(key: string): Promise<number> {
612
- const res = await this._signedRequest('HEAD', key);
613
- const len = res.headers.get(C.HEADER_CONTENT_LENGTH);
614
- return len ? +len : 0;
729
+ try {
730
+ const res = await this._signedRequest('HEAD', key);
731
+ const len = res.headers.get(C.HEADER_CONTENT_LENGTH);
732
+ return len ? +len : 0;
733
+ } catch (err) {
734
+ this._log('error', `Error getting content length for object ${key}: ${String(err)}`);
735
+ throw new Error(`${C.ERROR_PREFIX}Error getting content length for object ${key}: ${String(err)}`);
736
+ }
615
737
  }
616
738
 
739
+ /**
740
+ * Checks if an object exists in the S3-compatible service.
741
+ * This method sends a HEAD request to check if the specified object exists.
742
+ * @param {string} key - The key of the object to check.
743
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
744
+ * @returns A promise that resolves to true if the object exists, false if not found, or null if ETag mismatch.
745
+ */
617
746
  public async objectExists(key: string, opts: Record<string, unknown> = {}): Promise<IT.ExistResponseCode> {
618
747
  const res = await this._signedRequest('HEAD', key, {
619
748
  query: opts,
@@ -629,6 +758,18 @@ class s3mini {
629
758
  return true; // found (200)
630
759
  }
631
760
 
761
+ /**
762
+ * Retrieves the ETag of an object without downloading its content.
763
+ * @param {string} key - The key of the object to retrieve the ETag for.
764
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
765
+ * @returns {Promise<string | null>} A promise that resolves to the ETag value or null if the object is not found.
766
+ * @throws {Error} If the ETag header is not found in the response.
767
+ * @example
768
+ * const etag = await s3.getEtag('path/to/file.txt');
769
+ * if (etag) {
770
+ * console.log(`File ETag: ${etag}`);
771
+ * }
772
+ */
632
773
  public async getEtag(key: string, opts: Record<string, unknown> = {}): Promise<string | null> {
633
774
  const res = await this._signedRequest('HEAD', key, {
634
775
  query: opts,
@@ -641,12 +782,27 @@ class s3mini {
641
782
 
642
783
  const etag = res.headers.get(C.HEADER_ETAG);
643
784
  if (!etag) {
644
- throw new Error('ETag not found in response headers');
785
+ throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
645
786
  }
646
787
 
647
788
  return U.sanitizeETag(etag);
648
789
  }
649
790
 
791
+ /**
792
+ * Uploads an object to the S3-compatible service.
793
+ * @param {string} key - The key/path where the object will be stored.
794
+ * @param {string | Buffer} data - The data to upload (string or Buffer).
795
+ * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
796
+ * @returns {Promise<Response>} A promise that resolves to the Response object from the upload request.
797
+ * @throws {TypeError} If data is not a string or Buffer.
798
+ * @example
799
+ * // Upload text file
800
+ * await s3.putObject('hello.txt', 'Hello, World!', 'text/plain');
801
+ *
802
+ * // Upload binary data
803
+ * const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
804
+ * await s3.putObject('image.png', buffer, 'image/png');
805
+ */
650
806
  public async putObject(
651
807
  key: string,
652
808
  data: string | Buffer,
@@ -665,6 +821,17 @@ class s3mini {
665
821
  });
666
822
  }
667
823
 
824
+ /**
825
+ * Initiates a multipart upload and returns the upload ID.
826
+ * @param {string} key - The key/path where the object will be stored.
827
+ * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
828
+ * @returns {Promise<string>} A promise that resolves to the upload ID for the multipart upload.
829
+ * @throws {TypeError} If key is invalid or fileType is not a string.
830
+ * @throws {Error} If the multipart upload fails to initialize.
831
+ * @example
832
+ * const uploadId = await s3.getMultipartUploadId('large-file.zip', 'application/zip');
833
+ * console.log(`Started multipart upload: ${uploadId}`);
834
+ */
668
835
  public async getMultipartUploadId(key: string, fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE): Promise<string> {
669
836
  this._checkKey(key);
670
837
  if (typeof fileType !== 'string') {
@@ -678,22 +845,55 @@ class s3mini {
678
845
  headers,
679
846
  withQuery: true,
680
847
  });
848
+ const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
849
+
850
+ // if (
851
+ // parsed &&
852
+ // typeof parsed === 'object' &&
853
+ // 'initiateMultipartUploadResult' in parsed &&
854
+ // parsed.initiateMultipartUploadResult &&
855
+ // 'uploadId' in (parsed.initiateMultipartUploadResult as { uploadId: string })
856
+ // ) {
857
+ // return (parsed.initiateMultipartUploadResult as { uploadId: string }).uploadId;
858
+ // }
681
859
 
682
- const parsed = U.parseXml(await res.text()) as unknown;
860
+ if (parsed && typeof parsed === 'object') {
861
+ // Check for both cases of InitiateMultipartUploadResult
862
+ const uploadResult =
863
+ (parsed.initiateMultipartUploadResult as Record<string, unknown>) ||
864
+ (parsed.InitiateMultipartUploadResult as Record<string, unknown>);
683
865
 
684
- if (
685
- parsed &&
686
- typeof parsed === 'object' &&
687
- 'initiateMultipartUploadResult' in parsed &&
688
- parsed.initiateMultipartUploadResult &&
689
- 'uploadId' in (parsed.initiateMultipartUploadResult as { uploadId: string })
690
- ) {
691
- return (parsed.initiateMultipartUploadResult as { uploadId: string }).uploadId;
866
+ if (uploadResult && typeof uploadResult === 'object') {
867
+ // Check for both cases of uploadId
868
+ const uploadId = uploadResult.uploadId || uploadResult.UploadId;
869
+
870
+ if (uploadId && typeof uploadId === 'string') {
871
+ return uploadId;
872
+ }
873
+ }
692
874
  }
693
875
 
694
876
  throw new Error(`${C.ERROR_PREFIX}Failed to create multipart upload: ${JSON.stringify(parsed)}`);
695
877
  }
696
878
 
879
+ /**
880
+ * Uploads a part in a multipart upload.
881
+ * @param {string} key - The key of the object being uploaded.
882
+ * @param {string} uploadId - The upload ID from getMultipartUploadId.
883
+ * @param {Buffer | string} data - The data for this part.
884
+ * @param {number} partNumber - The part number (must be between 1 and 10,000).
885
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
886
+ * @returns {Promise<IT.UploadPart>} A promise that resolves to an object containing the partNumber and etag.
887
+ * @throws {TypeError} If any parameter is invalid.
888
+ * @example
889
+ * const part = await s3.uploadPart(
890
+ * 'large-file.zip',
891
+ * uploadId,
892
+ * partData,
893
+ * 1
894
+ * );
895
+ * console.log(`Part ${part.partNumber} uploaded with ETag: ${part.etag}`);
896
+ */
697
897
  public async uploadPart(
698
898
  key: string,
699
899
  uploadId: string,
@@ -713,13 +913,29 @@ class s3mini {
713
913
  return { partNumber, etag: U.sanitizeETag(res.headers.get('etag') || '') };
714
914
  }
715
915
 
916
+ /**
917
+ * Completes a multipart upload by combining all uploaded parts.
918
+ * @param {string} key - The key of the object being uploaded.
919
+ * @param {string} uploadId - The upload ID from getMultipartUploadId.
920
+ * @param {Array<IT.UploadPart>} parts - Array of uploaded parts with partNumber and etag.
921
+ * @returns {Promise<IT.CompleteMultipartUploadResult>} A promise that resolves to the completion result containing the final ETag.
922
+ * @throws {Error} If the multipart upload fails to complete.
923
+ * @example
924
+ * const result = await s3.completeMultipartUpload(
925
+ * 'large-file.zip',
926
+ * uploadId,
927
+ * [
928
+ * { partNumber: 1, etag: 'abc123' },
929
+ * { partNumber: 2, etag: 'def456' }
930
+ * ]
931
+ * );
932
+ * console.log(`Upload completed with ETag: ${result.etag}`);
933
+ */
716
934
  public async completeMultipartUpload(
717
935
  key: string,
718
936
  uploadId: string,
719
937
  parts: Array<IT.UploadPart>,
720
938
  ): Promise<IT.CompleteMultipartUploadResult> {
721
- // …existing validation left untouched …
722
-
723
939
  const query = { uploadId };
724
940
  const xmlBody = this._buildCompleteMultipartUploadXml(parts);
725
941
  const headers = {
@@ -734,24 +950,45 @@ class s3mini {
734
950
  withQuery: true,
735
951
  });
736
952
 
737
- const parsed = U.parseXml(await res.text()) as unknown;
738
-
739
- const result: unknown =
740
- parsed && typeof parsed === 'object' && 'completeMultipartUploadResult' in parsed
741
- ? (parsed as { completeMultipartUploadResult: unknown }).completeMultipartUploadResult
742
- : parsed;
953
+ const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
954
+ if (parsed && typeof parsed === 'object') {
955
+ // Check for both cases
956
+ const result = parsed.completeMultipartUploadResult || parsed.CompleteMultipartUploadResult || parsed;
957
+
958
+ if (result && typeof result === 'object') {
959
+ const resultObj = result as Record<string, unknown>;
960
+
961
+ // Handle ETag in all its variations
962
+ const etag = resultObj.ETag || resultObj.eTag || resultObj.etag;
963
+ if (etag && typeof etag === 'string') {
964
+ return {
965
+ ...resultObj,
966
+ etag: this.sanitizeETag(etag),
967
+ } as IT.CompleteMultipartUploadResult;
968
+ }
743
969
 
744
- if (!result || typeof result !== 'object') {
745
- throw new Error(`${C.ERROR_PREFIX}Failed to complete multipart upload: ${JSON.stringify(parsed)}`);
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
- );
970
+ return result as IT.CompleteMultipartUploadResult;
971
+ }
751
972
  }
752
- return result as IT.CompleteMultipartUploadResult;
973
+
974
+ throw new Error(`${C.ERROR_PREFIX}Failed to complete multipart upload: ${JSON.stringify(parsed)}`);
753
975
  }
754
976
 
977
+ /**
978
+ * Aborts a multipart upload and removes all uploaded parts.
979
+ * @param {string} key - The key of the object being uploaded.
980
+ * @param {string} uploadId - The upload ID to abort.
981
+ * @returns {Promise<object>} A promise that resolves to an object containing the abort status and details.
982
+ * @throws {TypeError} If key or uploadId is invalid.
983
+ * @throws {Error} If the abort operation fails.
984
+ * @example
985
+ * try {
986
+ * const result = await s3.abortMultipartUpload('large-file.zip', uploadId);
987
+ * console.log('Upload aborted:', result.status);
988
+ * } catch (error) {
989
+ * console.error('Failed to abort upload:', error);
990
+ * }
991
+ */
755
992
  public async abortMultipartUpload(key: string, uploadId: string): Promise<object> {
756
993
  this._checkKey(key);
757
994
  if (!uploadId) {
@@ -766,8 +1003,7 @@ class s3mini {
766
1003
  headers,
767
1004
  withQuery: true,
768
1005
  });
769
-
770
- const parsed = U.parseXml(await res.text()) as object;
1006
+ const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
771
1007
  if (
772
1008
  parsed &&
773
1009
  'error' in parsed &&
@@ -798,11 +1034,107 @@ class s3mini {
798
1034
  `;
799
1035
  }
800
1036
 
1037
+ /**
1038
+ * Deletes an object from the bucket.
1039
+ * This method sends a request to delete the specified object from the bucket.
1040
+ * @param {string} key - The key of the object to delete.
1041
+ * @returns A promise that resolves to true if the object was deleted successfully, false otherwise.
1042
+ */
801
1043
  public async deleteObject(key: string): Promise<boolean> {
802
1044
  const res = await this._signedRequest('DELETE', key, { tolerated: [200, 204] });
803
1045
  return res.status === 200 || res.status === 204;
804
1046
  }
805
1047
 
1048
+ private async _deleteObjectsProcess(keys: string[]): Promise<boolean[]> {
1049
+ const xmlBody = `<Delete>${keys.map(key => `<Object><Key>${U.escapeXml(key)}</Key></Object>`).join('')}</Delete>`;
1050
+ const query = { delete: '' };
1051
+ const md5Base64 = U.md5base64(xmlBody);
1052
+ const headers = {
1053
+ [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
1054
+ [C.HEADER_CONTENT_LENGTH]: Buffer.byteLength(xmlBody).toString(),
1055
+ 'Content-MD5': md5Base64,
1056
+ };
1057
+
1058
+ const res = await this._signedRequest('POST', '', {
1059
+ query,
1060
+ body: xmlBody,
1061
+ headers,
1062
+ withQuery: true,
1063
+ });
1064
+ const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1065
+ if (!parsed || typeof parsed !== 'object') {
1066
+ throw new Error(`${C.ERROR_PREFIX}Failed to delete objects: ${JSON.stringify(parsed)}`);
1067
+ }
1068
+ const out = (parsed.DeleteResult || parsed.deleteResult || parsed) as Record<string, unknown>;
1069
+ const resultMap = new Map<string, boolean>();
1070
+ keys.forEach(key => resultMap.set(key, false));
1071
+ const deleted = out.deleted || out.Deleted;
1072
+ if (deleted) {
1073
+ const deletedArray = Array.isArray(deleted) ? deleted : [deleted];
1074
+ deletedArray.forEach((item: unknown) => {
1075
+ if (item && typeof item === 'object') {
1076
+ const obj = item as Record<string, unknown>;
1077
+ // Check both key and Key
1078
+ const key = obj.key || obj.Key;
1079
+ if (key && typeof key === 'string') {
1080
+ resultMap.set(key, true);
1081
+ }
1082
+ }
1083
+ });
1084
+ }
1085
+
1086
+ // Handle errors (check both cases)
1087
+ const errors = out.error || out.Error;
1088
+ if (errors) {
1089
+ const errorsArray = Array.isArray(errors) ? errors : [errors];
1090
+ errorsArray.forEach((item: unknown) => {
1091
+ if (item && typeof item === 'object') {
1092
+ const obj = item as Record<string, unknown>;
1093
+ // Check both cases for all properties
1094
+ const key = obj.key || obj.Key;
1095
+ const code = obj.code || obj.Code;
1096
+ const message = obj.message || obj.Message;
1097
+
1098
+ if (key && typeof key === 'string') {
1099
+ resultMap.set(key, false);
1100
+ // Optionally log the error for debugging
1101
+ this._log('warn', `Failed to delete object: ${key}`, {
1102
+ code: code || 'Unknown',
1103
+ message: message || 'Unknown error',
1104
+ });
1105
+ }
1106
+ }
1107
+ });
1108
+ }
1109
+
1110
+ // Return boolean array in the same order as input keys
1111
+ return keys.map(key => resultMap.get(key) || false);
1112
+ }
1113
+
1114
+ /**
1115
+ * Deletes multiple objects from the bucket.
1116
+ * @param {string[]} keys - An array of object keys to delete.
1117
+ * @returns A promise that resolves to an array of booleans indicating success for each key in order.
1118
+ */
1119
+ public async deleteObjects(keys: string[]): Promise<boolean[]> {
1120
+ if (!Array.isArray(keys) || keys.length === 0) {
1121
+ return [];
1122
+ }
1123
+ const maxBatchSize = 1000; // S3 limit for delete batch size
1124
+ if (keys.length > maxBatchSize) {
1125
+ const allPromises = [];
1126
+ for (let i = 0; i < keys.length; i += maxBatchSize) {
1127
+ const batch = keys.slice(i, i + maxBatchSize);
1128
+ allPromises.push(this._deleteObjectsProcess(batch));
1129
+ }
1130
+ const results = await Promise.all(allPromises);
1131
+ // Flatten the results array
1132
+ return results.flat();
1133
+ } else {
1134
+ return await this._deleteObjectsProcess(keys);
1135
+ }
1136
+ }
1137
+
806
1138
  private async _sendRequest(
807
1139
  url: string,
808
1140
  method: IT.HttpMethod,