s3mini 0.4.0 → 0.5.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 CHANGED
@@ -23,6 +23,12 @@ Dev:
23
23
  ![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/good-lly/s3mini/dev?color=greeen)
24
24
  ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/good-lly/s3mini)
25
25
  [![CodeQL Advanced](https://github.com/good-lly/s3mini/actions/workflows/codeql.yml/badge.svg?branch=dev)](https://github.com/good-lly/s3mini/actions/workflows/codeql.yml)
26
+ [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=good-lly_s3mini&metric=bugs)](https://sonarcloud.io/summary/new_code?id=good-lly_s3mini)
27
+ [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=good-lly_s3mini&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=good-lly_s3mini)
28
+ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=good-lly_s3mini&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=good-lly_s3mini)
29
+ [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=good-lly_s3mini&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=good-lly_s3mini)
30
+ [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=good-lly_s3mini&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=good-lly_s3mini)
31
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=good-lly_s3mini&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=good-lly_s3mini)
26
32
  [![Test:e2e(all)](https://github.com/good-lly/s3mini/actions/workflows/test-e2e.yml/badge.svg?branch=dev)](https://github.com/good-lly/s3mini/actions/workflows/test-e2e.yml)
27
33
 
28
34
  ![GitHub Repo stars](https://img.shields.io/github/stars/good-lly/s3mini?style=social)
package/dist/s3mini.d.ts CHANGED
@@ -12,6 +12,9 @@ interface SSECHeaders {
12
12
  'x-amz-server-side-encryption-customer-key': string;
13
13
  'x-amz-server-side-encryption-customer-key-md5': string;
14
14
  }
15
+ interface AWSHeaders {
16
+ [k: `x-amz-${string}`]: string;
17
+ }
15
18
  interface Logger {
16
19
  info: (message: string, ...args: unknown[]) => void;
17
20
  warn: (message: string, ...args: unknown[]) => void;
@@ -57,7 +60,7 @@ interface ListMultipartUploadSuccess {
57
60
  key: string;
58
61
  uploadId: string;
59
62
  size?: number;
60
- mtime?: Date | undefined;
63
+ mtime?: Date;
61
64
  etag?: string;
62
65
  eTag?: string;
63
66
  parts: UploadPart[];
@@ -80,6 +83,55 @@ interface ErrorWithCode {
80
83
  type ListMultipartUploadResponse = ListMultipartUploadSuccess | MultipartUploadError;
81
84
  type HttpMethod = 'POST' | 'GET' | 'HEAD' | 'PUT' | 'DELETE';
82
85
  type ExistResponseCode = false | true | null;
86
+ interface CopyObjectOptions {
87
+ /**
88
+ * Specifies whether the metadata is copied from the source object or replaced with metadata provided in the request.
89
+ * Valid values: 'COPY' | 'REPLACE'
90
+ * Default: 'COPY'
91
+ */
92
+ metadataDirective?: 'COPY' | 'REPLACE';
93
+ /**
94
+ * Metadata to be set on the destination object when metadataDirective is 'REPLACE'.
95
+ * Keys can be provided with or without 'x-amz-meta-' prefix.
96
+ */
97
+ metadata?: Record<string, string>;
98
+ contentType?: string;
99
+ /**
100
+ * Storage class for the destination object.
101
+ * Valid values: 'STANDARD' | 'REDUCED_REDUNDANCY' | 'STANDARD_IA' | 'ONEZONE_IA' | 'INTELLIGENT_TIERING' | 'GLACIER' | 'DEEP_ARCHIVE' | 'GLACIER_IR'
102
+ */
103
+ storageClass?: string;
104
+ /**
105
+ * Specifies whether the object tag-set is copied from the source object or replaced with tag-set provided in the request.
106
+ * Valid values: 'COPY' | 'REPLACE'
107
+ */
108
+ taggingDirective?: 'COPY' | 'REPLACE';
109
+ /**
110
+ * If the bucket is configured as a website, redirects requests for this object to another object or URL.
111
+ */
112
+ websiteRedirectLocation?: string;
113
+ /**
114
+ * Server-Side Encryption with Customer-Provided Keys headers for the source object.
115
+ * Should include:
116
+ * - x-amz-copy-source-server-side-encryption-customer-algorithm
117
+ * - x-amz-copy-source-server-side-encryption-customer-key
118
+ * - x-amz-copy-source-server-side-encryption-customer-key-MD5
119
+ */
120
+ sourceSSECHeaders?: Record<string, string | number>;
121
+ destinationSSECHeaders?: SSECHeaders;
122
+ additionalHeaders?: Record<string, string | number>;
123
+ }
124
+ interface CopyObjectResult {
125
+ etag: string;
126
+ lastModified?: Date;
127
+ }
128
+ /**
129
+ * Where Buffer is available, e.g. when @types/node is loaded, we want to use it.
130
+ * But it should be excluded in other environments (e.g. Cloudflare).
131
+ */
132
+ type MaybeBuffer = typeof globalThis extends {
133
+ Buffer?: infer B;
134
+ } ? B extends new (...a: unknown[]) => unknown ? InstanceType<B> : ArrayBuffer | Uint8Array : ArrayBuffer | Uint8Array;
83
135
 
84
136
  /**
85
137
  * S3 class for interacting with S3-compatible object storage services.
@@ -91,8 +143,8 @@ type ExistResponseCode = false | true | null;
91
143
  * const s3 = new CoreS3({
92
144
  * accessKeyId: 'your-access-key',
93
145
  * secretAccessKey: 'your-secret-key',
94
- * endpoint: 'https://your-s3-endpoint.com',
95
- * region: 'us-east-1' // by default is auto
146
+ * endpoint: 'https://your-s3-endpoint.com/bucket-name',
147
+ * region: 'auto' // by default is auto
96
148
  * });
97
149
  *
98
150
  * // Upload a file
@@ -119,13 +171,14 @@ declare class S3mini {
119
171
  * @param {Object} [config.logger=null] - A logger object with methods like info, warn, error.
120
172
  * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
121
173
  */
122
- private accessKeyId;
123
- private secretAccessKey;
124
- private endpoint;
125
- private region;
126
- private requestSizeInBytes;
127
- private requestAbortTimeout?;
128
- private logger?;
174
+ readonly accessKeyId: string;
175
+ readonly secretAccessKey: string;
176
+ readonly endpoint: URL;
177
+ readonly region: string;
178
+ readonly bucketName: string;
179
+ readonly requestSizeInBytes: number;
180
+ readonly requestAbortTimeout?: number;
181
+ readonly logger?: Logger;
129
182
  private signingKeyDate?;
130
183
  private signingKey?;
131
184
  constructor({ accessKeyId, secretAccessKey, endpoint, region, requestSizeInBytes, requestAbortTimeout, logger, }: S3Config);
@@ -139,43 +192,10 @@ declare class S3mini {
139
192
  private _checkPrefix;
140
193
  private _checkOpts;
141
194
  private _filterIfHeaders;
195
+ private _validateData;
142
196
  private _validateUploadPartParams;
143
197
  private _sign;
144
- private _buildCanonicalHeaders;
145
- private _buildCanonicalRequest;
146
- private _buildCredentialScope;
147
- private _buildStringToSign;
148
- private _calculateSignature;
149
- private _buildAuthorizationHeader;
150
198
  private _signedRequest;
151
- /**
152
- * Gets the current configuration properties of the S3 instance.
153
- * @returns {IT.S3Config} The current S3 configuration object containing all settings.
154
- * @example
155
- * const config = s3.getProps();
156
- * console.log(config.endpoint); // 'https://s3.amazonaws.com/my-bucket'
157
- */
158
- getProps(): S3Config;
159
- /**
160
- * Updates the configuration properties of the S3 instance.
161
- * @param {IT.S3Config} props - The new configuration object.
162
- * @param {string} props.accessKeyId - The access key ID for authentication.
163
- * @param {string} props.secretAccessKey - The secret access key for authentication.
164
- * @param {string} props.endpoint - The endpoint URL of the S3-compatible service.
165
- * @param {string} [props.region='auto'] - The region of the S3 service.
166
- * @param {number} [props.requestSizeInBytes=8388608] - The request size of a single request in bytes.
167
- * @param {number} [props.requestAbortTimeout] - The timeout in milliseconds after which a request should be aborted.
168
- * @param {IT.Logger} [props.logger] - A logger object with methods like info, warn, error.
169
- * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
170
- * @example
171
- * s3.setProps({
172
- * accessKeyId: 'new-access-key',
173
- * secretAccessKey: 'new-secret-key',
174
- * endpoint: 'https://new-endpoint.com/my-bucket',
175
- * region: 'us-west-2' // by default is auto
176
- * });
177
- */
178
- setProps(props: S3Config): void;
179
199
  /**
180
200
  * Sanitizes an ETag value by removing surrounding quotes and whitespace.
181
201
  * Still returns RFC compliant ETag. https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3
@@ -191,6 +211,7 @@ declare class S3mini {
191
211
  * @returns A promise that resolves to true if the bucket was created successfully, false otherwise.
192
212
  */
193
213
  createBucket(): Promise<boolean>;
214
+ private _extractBucketName;
194
215
  /**
195
216
  * Checks if a bucket exists.
196
217
  * This method sends a request to check if the specified bucket exists in the S3-compatible service.
@@ -213,6 +234,12 @@ declare class S3mini {
213
234
  * const photos = await s3.listObjects('/', 'photos/', 100);
214
235
  */
215
236
  listObjects(delimiter?: string, prefix?: string, maxKeys?: number, opts?: Record<string, unknown>): Promise<ListObject[] | null>;
237
+ private _fetchObjectBatch;
238
+ private _buildListObjectsQuery;
239
+ private _handleListObjectsError;
240
+ private _parseListObjectsResponse;
241
+ private _extractObjectsFromResponse;
242
+ private _extractContinuationToken;
216
243
  /**
217
244
  * Lists multipart uploads in the bucket.
218
245
  * This method sends a request to list multipart uploads in the specified bucket.
@@ -319,6 +346,7 @@ declare class S3mini {
319
346
  * @param {string | Buffer} data - The data to upload (string or Buffer).
320
347
  * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
321
348
  * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
349
+ * @param {IT.AWSHeaders} [additionalHeaders] - Additional x-amz-* headers specific to this request, if any.
322
350
  * @returns {Promise<Response>} A promise that resolves to the Response object from the upload request.
323
351
  * @throws {TypeError} If data is not a string or Buffer.
324
352
  * @example
@@ -329,7 +357,7 @@ declare class S3mini {
329
357
  * const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
330
358
  * await s3.putObject('image.png', buffer, 'image/png');
331
359
  */
332
- putObject(key: string, data: string | Buffer, fileType?: string, ssecHeaders?: SSECHeaders): Promise<Response>;
360
+ putObject(key: string, data: string | MaybeBuffer, fileType?: string, ssecHeaders?: SSECHeaders, additionalHeaders?: AWSHeaders): Promise<Response>;
333
361
  /**
334
362
  * Initiates a multipart upload and returns the upload ID.
335
363
  * @param {string} key - The key/path where the object will be stored.
@@ -362,7 +390,7 @@ declare class S3mini {
362
390
  * );
363
391
  * console.log(`Part ${part.partNumber} uploaded with ETag: ${part.etag}`);
364
392
  */
365
- uploadPart(key: string, uploadId: string, data: Buffer | string, partNumber: number, opts?: Record<string, unknown>, ssecHeaders?: SSECHeaders): Promise<UploadPart>;
393
+ uploadPart(key: string, uploadId: string, data: MaybeBuffer | string, partNumber: number, opts?: Record<string, unknown>, ssecHeaders?: SSECHeaders): Promise<UploadPart>;
366
394
  /**
367
395
  * Completes a multipart upload by combining all uploaded parts.
368
396
  * @param {string} key - The key of the object being uploaded.
@@ -400,6 +428,109 @@ declare class S3mini {
400
428
  */
401
429
  abortMultipartUpload(key: string, uploadId: string, ssecHeaders?: SSECHeaders): Promise<object>;
402
430
  private _buildCompleteMultipartUploadXml;
431
+ /**
432
+ * Executes the copy operation for local copying (same bucket/endpoint).
433
+ * @private
434
+ */
435
+ private _executeCopyOperation;
436
+ /**
437
+ * Copies an object within the same bucket.
438
+ *
439
+ * @param {string} sourceKey - The key of the source object to copy
440
+ * @param {string} destinationKey - The key where the object will be copied to
441
+ * @param {IT.CopyObjectOptions} [options={}] - Copy operation options
442
+ * @param {string} [options.metadataDirective='COPY'] - How to handle metadata ('COPY' | 'REPLACE')
443
+ * @param {Record<string,string>} [options.metadata={}] - New metadata (only used if metadataDirective='REPLACE')
444
+ * @param {string} [options.contentType] - New content type for the destination object
445
+ * @param {string} [options.storageClass] - Storage class for the destination object
446
+ * @param {string} [options.taggingDirective] - How to handle object tags ('COPY' | 'REPLACE')
447
+ * @param {string} [options.websiteRedirectLocation] - Website redirect location for the destination
448
+ * @param {IT.SSECHeaders} [options.sourceSSECHeaders={}] - Encryption headers for reading source (if encrypted)
449
+ * @param {IT.SSECHeaders} [options.destinationSSECHeaders={}] - Encryption headers for destination
450
+ * @param {IT.AWSHeaders} [options.additionalHeaders={}] - Extra x-amz-* headers
451
+ *
452
+ * @returns {Promise<IT.CopyObjectResult>} Copy result with etag and lastModified date
453
+ * @throws {TypeError} If sourceKey or destinationKey is invalid
454
+ * @throws {Error} If copy operation fails or S3 returns an error
455
+ *
456
+ * @example
457
+ * // Simple copy
458
+ * const result = await s3.copyObject('report-2024.pdf', 'archive/report-2024.pdf');
459
+ * console.log(`Copied with ETag: ${result.etag}`);
460
+ *
461
+ * @example
462
+ * // Copy with new metadata and content type
463
+ * const result = await s3.copyObject('data.csv', 'processed/data.csv', {
464
+ * metadataDirective: 'REPLACE',
465
+ * metadata: {
466
+ * 'processed-date': new Date().toISOString(),
467
+ * 'original-name': 'data.csv'
468
+ * },
469
+ * contentType: 'text/csv; charset=utf-8'
470
+ * });
471
+ *
472
+ * @example
473
+ * // Copy encrypted object (Cloudflare R2 SSE-C)
474
+ * const ssecKey = 'n1TKiTaVHlYLMX9n0zHXyooMr026vOiTEFfT+719Hho=';
475
+ * await s3.copyObject('sensitive.json', 'backup/sensitive.json', {
476
+ * sourceSSECHeaders: {
477
+ * 'x-amz-copy-source-server-side-encryption-customer-algorithm': 'AES256',
478
+ * 'x-amz-copy-source-server-side-encryption-customer-key': ssecKey,
479
+ * 'x-amz-copy-source-server-side-encryption-customer-key-md5': 'gepZmzgR7Be/1+K1Aw+6ow=='
480
+ * },
481
+ * destinationSSECHeaders: {
482
+ * 'x-amz-server-side-encryption-customer-algorithm': 'AES256',
483
+ * 'x-amz-server-side-encryption-customer-key': ssecKey,
484
+ * 'x-amz-server-side-encryption-customer-key-md5': 'gepZmzgR7Be/1+K1Aw+6ow=='
485
+ * }
486
+ * });
487
+ */
488
+ copyObject(sourceKey: string, destinationKey: string, options?: CopyObjectOptions): Promise<CopyObjectResult>;
489
+ private _buildSSECHeaders;
490
+ /**
491
+ * Moves an object within the same bucket (copy + delete atomic-like operation).
492
+ *
493
+ * WARNING: Not truly atomic - if delete fails after successful copy, the object
494
+ * will exist in both locations. Consider your use case carefully.
495
+ *
496
+ * @param {string} sourceKey - The key of the source object to move
497
+ * @param {string} destinationKey - The key where the object will be moved to
498
+ * @param {IT.CopyObjectOptions} [options={}] - Options passed to the copy operation
499
+ *
500
+ * @returns {Promise<IT.CopyObjectResult>} Result from the copy operation
501
+ * @throws {TypeError} If sourceKey or destinationKey is invalid
502
+ * @throws {Error} If copy succeeds but delete fails (includes copy result in error)
503
+ *
504
+ * @example
505
+ * // Simple move
506
+ * await s3.moveObject('temp/upload.tmp', 'files/document.pdf');
507
+ *
508
+ * @example
509
+ * // Move with metadata update
510
+ * await s3.moveObject('unprocessed/image.jpg', 'processed/image.jpg', {
511
+ * metadataDirective: 'REPLACE',
512
+ * metadata: {
513
+ * 'status': 'processed',
514
+ * 'processed-at': Date.now().toString()
515
+ * },
516
+ * contentType: 'image/jpeg'
517
+ * });
518
+ *
519
+ * @example
520
+ * // Safe move with error handling
521
+ * try {
522
+ * const result = await s3.moveObject('inbox/file.dat', 'archive/file.dat');
523
+ * console.log(`Moved successfully: ${result.etag}`);
524
+ * } catch (error) {
525
+ * // Check if copy succeeded but delete failed
526
+ * if (error.message.includes('delete source object after successful copy')) {
527
+ * console.warn('File copied but not deleted from source - manual cleanup needed');
528
+ * }
529
+ * }
530
+ */
531
+ moveObject(sourceKey: string, destinationKey: string, options?: CopyObjectOptions): Promise<CopyObjectResult>;
532
+ private _buildMetadataHeaders;
533
+ private _parseCopyObjectResponse;
403
534
  /**
404
535
  * Deletes an object from the bucket.
405
536
  * This method sends a request to delete the specified object from the bucket.
@@ -415,14 +546,11 @@ declare class S3mini {
415
546
  */
416
547
  deleteObjects(keys: string[]): Promise<boolean[]>;
417
548
  private _sendRequest;
549
+ private _parseErrorXml;
418
550
  private _handleErrorResponse;
419
551
  private _buildCanonicalQueryString;
420
552
  private _getSignatureKey;
421
553
  }
422
- /**
423
- * @deprecated Use `S3mini` instead.
424
- */
425
- declare const s3mini: typeof S3mini;
426
554
 
427
555
  /**
428
556
  * Sanitize ETag value by removing quotes and XML entities
@@ -441,5 +569,5 @@ declare const sanitizeETag: (etag: string) => string;
441
569
  */
442
570
  declare const runInBatches: <T = unknown>(tasks: Iterable<() => Promise<T>>, batchSize?: number, minIntervalMs?: number) => Promise<Array<PromiseSettledResult<T>>>;
443
571
 
444
- export { S3mini, S3mini as default, runInBatches, s3mini, sanitizeETag };
572
+ export { S3mini, S3mini as default, runInBatches, sanitizeETag };
445
573
  export type { CompleteMultipartUploadResult, ErrorWithCode, ExistResponseCode, ListBucketResponse, ListMultipartUploadResponse, Logger, S3Config, UploadPart };