s3mini 0.1.1 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # s3mini | Tiny & fast S3 client for node and edge platforms.
2
2
 
3
- `s3mini` is an ultra-lightweight Typescript client (~14 KB minified, ≈15 % more ops/s) for S3-compatible object storage. It runs on Node, Bun, Cloudflare Workers, and other edge platforms. It has been tested on Cloudflare R2, Backblaze B2, DigitalOcean Spaces, and MinIO. (No Browser support!)
3
+ `s3mini` is an ultra-lightweight Typescript client (~14 KB minified, ≈15 % more ops/s) for S3-compatible object storage. It runs on Node, Bun, Cloudflare Workers, and other edge platforms. It has been tested on Cloudflare R2, Backblaze B2, DigitalOcean Spaces, Ceph, Oracle, Garage and MinIO. (No Browser support!)
4
4
 
5
5
  [[github](https://github.com/good-lly/s3mini)]
6
6
  [[issues](https://github.com/good-lly/s3mini/issues)]
@@ -12,7 +12,7 @@
12
12
  - 🔧 Zero dependencies; supports AWS SigV4 (no pre-signed requests).
13
13
  - 🟠 Works on Cloudflare Workers; ideal for edge computing, Node, and Bun (no browser support).
14
14
  - 🔑 Only the essential S3 APIs—improved list, put, get, delete, and a few more.
15
- - 📦 **BYOS3** — _Bring your own S3-compatible bucket_ (tested on Cloudflare R2, Backblaze B2, DigitalOcean Spaces, MinIO and Garage! Ceph and AWS are in the queue).
15
+ - 📦 **BYOS3** — _Bring your own S3-compatible bucket_ (tested on Cloudflare R2, Backblaze B2, DigitalOcean Spaces, MinIO, Garage, Micro/Ceph and Oracle Object Storage).
16
16
 
17
17
  #### Tested On
18
18
 
@@ -31,6 +31,8 @@ Dev:
31
31
  ![npm package minimized gzipped size](https://img.shields.io/bundlejs/size/s3mini?color=green)
32
32
  ![GitHub License](https://img.shields.io/github/license/good-lly/s3mini)
33
33
 
34
+ <a href="https://github.com/good-lly/s3mini/issues/"> <img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg" alt="Contributions welcome" /></a>
35
+
34
36
  Performance tests was done on local Minio instance. Your results may vary depending on environment and network conditions, so take it with a grain of salt.
35
37
  ![performance-image](https://raw.githubusercontent.com/good-lly/s3mini/dev/performance-screenshot.png)
36
38
 
@@ -80,6 +82,15 @@ yarn add s3mini
80
82
  pnpm add s3mini
81
83
  ```
82
84
 
85
+ ### Environment Variables
86
+
87
+ To use `s3mini`, you need to set up your environment variables for provider credentials and S3 endpoint. Create a `.env` file in your project root directory. Checkout the [example.env](example.env) file for reference.
88
+
89
+ ```bash
90
+ # On Windows, Mac, or Linux
91
+ mv example.env .env
92
+ ```
93
+
83
94
  > **⚠️ Environment Support Notice**
84
95
  >
85
96
  > This library is designed to run in environments like **Node.js**, **Bun**, and **Cloudflare Workers**. It does **not support browser environments** due to the use of Node.js APIs and polyfills.
@@ -124,7 +135,10 @@ let etag: string | null = null;
124
135
  if (!objectExists) {
125
136
  // put/upload the object, content can be a string or Buffer
126
137
  // to add object into "folder", use "folder/filename.txt" as key
138
+ // Third argument is optional, it can be used to set content type ... default is 'application/octet-stream'
127
139
  const resp: Response = await s3client.putObject(smallObjectKey, smallObjectContent);
140
+ // example with content type:
141
+ // const resp: Response = await s3client.putObject(smallObjectKey, smallObjectContent, 'image/png');
128
142
  // you can also get etag via getEtag method
129
143
  // const etag: string = await s3client.getEtag(smallObjectKey);
130
144
  etag = sanitizeETag(resp.headers.get('etag'));
@@ -214,17 +228,17 @@ For more check [USAGE.md](USAGE.md) file, examples and tests.
214
228
  - Authors are not responsible for any data loss or security breaches resulting from improper usage of the library.
215
229
  - If you find a security vulnerability, please report it to us directly via email. For more details, please refer to the [SECURITY.md](SECURITY.md) file.
216
230
 
217
- ## Contributions welcomed!
231
+ ## Contributions welcomed! (in specific order)
218
232
 
219
- Contributions are greatly appreciated! If you have an idea for a new feature or have found a bug, we encourage you to get involved:
233
+ Contributions are greatly appreciated! If you have an idea for a new feature or have found a bug, we encourage you to get involved in this order:
220
234
 
221
- - _Report Issues_: If you encounter a problem or have a feature request, please open an issue on GitHub. Include as much detail as possible (environment, error messages, logs, steps to reproduce, etc.) so we can understand and address the issue.
235
+ 1. _Open/Report Issues or Ideas_: If you encounter a problem, have an idea or a feature request, please open an issue on GitHub (FIRST!) . Be concise but include as much detail as necessary (environment, error messages, logs, steps to reproduce, etc.) so we can understand and address the issue and have a dialog.
222
236
 
223
- - _Pull Requests_: We welcome PRs! If you want to implement a new feature or fix a bug, feel free to submit a pull request to the latest `dev branch`. For major changes, it's a good idea to discuss your plans in an issue first.
237
+ 2. _Create Pull Requests_: We welcome PRs! If you want to implement a new feature or fix a bug, feel free to submit a pull request to the latest `dev branch`. For major changes, it's a necessary to discuss your plans in an issue first!
224
238
 
225
- - _Lightweight Philosophy_: When contributing, keep in mind that s3mini aims to remain lightweight and dependency-free. Please avoid adding heavy dependencies. New features should provide significant value to justify any increase in size.
239
+ 3. _Lightweight Philosophy_: When contributing, keep in mind that s3mini aims to remain lightweight and dependency-free. Please avoid adding heavy dependencies. New features should provide significant value to justify any increase in size.
226
240
 
227
- - _Community Conduct_: Be respectful and constructive in communications. We want a welcoming environment for all contributors. For more details, please refer to our [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). No one reads it, but it's there for a reason.
241
+ 4. _Community Conduct_: Be respectful and constructive in communications. We want a welcoming environment for all contributors. For more details, please refer to our [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). No one reads it, but it's there for a reason.
228
242
 
229
243
  If you figure out a solution to your question or problem on your own, please consider posting the answer or closing the issue with an explanation. It could help the next person who runs into the same thing!
230
244
 
package/dist/s3mini.d.ts CHANGED
@@ -114,10 +114,8 @@ declare class s3mini {
114
114
  private requestSizeInBytes;
115
115
  private requestAbortTimeout?;
116
116
  private logger?;
117
- private fullDatetime;
118
- private shortDatetime;
119
- private signingKey;
120
- private credentialScope;
117
+ private signingKeyDate?;
118
+ private signingKey?;
121
119
  constructor({ accessKeyId, secretAccessKey, endpoint, region, requestSizeInBytes, requestAbortTimeout, logger, }: S3Config);
122
120
  private _sanitize;
123
121
  private _log;
@@ -133,36 +131,266 @@ declare class s3mini {
133
131
  private _sign;
134
132
  private _buildCanonicalHeaders;
135
133
  private _buildCanonicalRequest;
134
+ private _buildCredentialScope;
136
135
  private _buildStringToSign;
137
136
  private _calculateSignature;
138
137
  private _buildAuthorizationHeader;
139
138
  private _signedRequest;
139
+ /**
140
+ * Gets the current configuration properties of the S3 instance.
141
+ * @returns {IT.S3Config} The current S3 configuration object containing all settings.
142
+ * @example
143
+ * const config = s3.getProps();
144
+ * console.log(config.endpoint); // 'https://s3.amazonaws.com/my-bucket'
145
+ */
140
146
  getProps(): S3Config;
147
+ /**
148
+ * Updates the configuration properties of the S3 instance.
149
+ * @param {IT.S3Config} props - The new configuration object.
150
+ * @param {string} props.accessKeyId - The access key ID for authentication.
151
+ * @param {string} props.secretAccessKey - The secret access key for authentication.
152
+ * @param {string} props.endpoint - The endpoint URL of the S3-compatible service.
153
+ * @param {string} [props.region='auto'] - The region of the S3 service.
154
+ * @param {number} [props.requestSizeInBytes=8388608] - The request size of a single request in bytes.
155
+ * @param {number} [props.requestAbortTimeout] - The timeout in milliseconds after which a request should be aborted.
156
+ * @param {IT.Logger} [props.logger] - A logger object with methods like info, warn, error.
157
+ * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
158
+ * @example
159
+ * s3.setProps({
160
+ * accessKeyId: 'new-access-key',
161
+ * secretAccessKey: 'new-secret-key',
162
+ * endpoint: 'https://new-endpoint.com/my-bucket',
163
+ * region: 'us-west-2' // by default is auto
164
+ * });
165
+ */
141
166
  setProps(props: S3Config): void;
167
+ /**
168
+ * Sanitizes an ETag value by removing surrounding quotes and whitespace.
169
+ * Still returns RFC compliant ETag. https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3
170
+ * @param {string} etag - The ETag value to sanitize.
171
+ * @returns {string} The sanitized ETag value.
172
+ * @example
173
+ * const cleanEtag = s3.sanitizeETag('"abc123"'); // Returns: 'abc123'
174
+ */
142
175
  sanitizeETag(etag: string): string;
176
+ /**
177
+ * Creates a new bucket.
178
+ * This method sends a request to create a new bucket in the specified in endpoint.
179
+ * @returns A promise that resolves to true if the bucket was created successfully, false otherwise.
180
+ */
143
181
  createBucket(): Promise<boolean>;
182
+ /**
183
+ * Checks if a bucket exists.
184
+ * This method sends a request to check if the specified bucket exists in the S3-compatible service.
185
+ * @returns A promise that resolves to true if the bucket exists, false otherwise.
186
+ */
144
187
  bucketExists(): Promise<boolean>;
188
+ /**
189
+ * Lists objects in the bucket with optional filtering and no pagination.
190
+ * This method retrieves all objects matching the criteria (not paginated like listObjectsV2).
191
+ * @param {string} [delimiter='/'] - The delimiter to use for grouping objects.
192
+ * @param {string} [prefix=''] - The prefix to filter objects by.
193
+ * @param {number} [maxKeys] - The maximum number of keys to return. If not provided, all keys will be returned.
194
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
195
+ * @returns {Promise<object[] | null>} A promise that resolves to an array of objects or null if the bucket is empty.
196
+ * @example
197
+ * // List all objects
198
+ * const objects = await s3.listObjects();
199
+ *
200
+ * // List objects with prefix
201
+ * const photos = await s3.listObjects('/', 'photos/', 100);
202
+ */
145
203
  listObjects(delimiter?: string, prefix?: string, maxKeys?: number, opts?: Record<string, unknown>): Promise<object[] | null>;
204
+ /**
205
+ * Lists multipart uploads in the bucket.
206
+ * This method sends a request to list multipart uploads in the specified bucket.
207
+ * @param {string} [delimiter='/'] - The delimiter to use for grouping uploads.
208
+ * @param {string} [prefix=''] - The prefix to filter uploads by.
209
+ * @param {IT.HttpMethod} [method='GET'] - The HTTP method to use for the request (GET or HEAD).
210
+ * @param {Record<string, string | number | boolean | undefined>} [opts={}] - Additional options for the request.
211
+ * @returns A promise that resolves to a list of multipart uploads or an error.
212
+ */
146
213
  listMultipartUploads(delimiter?: string, prefix?: string, method?: HttpMethod, opts?: Record<string, string | number | boolean | undefined>): Promise<ListMultipartUploadSuccess | MultipartUploadError>;
214
+ /**
215
+ * Get an object from the S3-compatible service.
216
+ * This method sends a request to retrieve the specified object from the S3-compatible service.
217
+ * @param {string} key - The key of the object to retrieve.
218
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
219
+ * @returns A promise that resolves to the object data (string) or null if not found.
220
+ */
147
221
  getObject(key: string, opts?: Record<string, unknown>): Promise<string | null>;
222
+ /**
223
+ * Get an object response from the S3-compatible service.
224
+ * This method sends a request to retrieve the specified object and returns the full response.
225
+ * @param {string} key - The key of the object to retrieve.
226
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
227
+ * @returns A promise that resolves to the Response object or null if not found.
228
+ */
148
229
  getObjectResponse(key: string, opts?: Record<string, unknown>): Promise<Response | null>;
230
+ /**
231
+ * Get an object as an ArrayBuffer from the S3-compatible service.
232
+ * This method sends a request to retrieve the specified object and returns it as an ArrayBuffer.
233
+ * @param {string} key - The key of the object to retrieve.
234
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
235
+ * @returns A promise that resolves to the object data as an ArrayBuffer or null if not found.
236
+ */
149
237
  getObjectArrayBuffer(key: string, opts?: Record<string, unknown>): Promise<ArrayBuffer | null>;
238
+ /**
239
+ * Get an object as JSON from the S3-compatible service.
240
+ * This method sends a request to retrieve the specified object and returns it as JSON.
241
+ * @param {string} key - The key of the object to retrieve.
242
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
243
+ * @returns A promise that resolves to the object data as JSON or null if not found.
244
+ */
150
245
  getObjectJSON<T = unknown>(key: string, opts?: Record<string, unknown>): Promise<T | null>;
246
+ /**
247
+ * Get an object with its ETag from the S3-compatible service.
248
+ * This method sends a request to retrieve the specified object and its ETag.
249
+ * @param {string} key - The key of the object to retrieve.
250
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
251
+ * @returns A promise that resolves to an object containing the ETag and the object data as an ArrayBuffer or null if not found.
252
+ */
151
253
  getObjectWithETag(key: string, opts?: Record<string, unknown>): Promise<{
152
254
  etag: string | null;
153
255
  data: ArrayBuffer | null;
154
256
  }>;
257
+ /**
258
+ * Get an object as a raw response from the S3-compatible service.
259
+ * This method sends a request to retrieve the specified object and returns the raw response.
260
+ * @param {string} key - The key of the object to retrieve.
261
+ * @param {boolean} [wholeFile=true] - Whether to retrieve the whole file or a range.
262
+ * @param {number} [rangeFrom=0] - The starting byte for the range (if not whole file).
263
+ * @param {number} [rangeTo=this.requestSizeInBytes] - The ending byte for the range (if not whole file).
264
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
265
+ * @returns A promise that resolves to the Response object.
266
+ */
155
267
  getObjectRaw(key: string, wholeFile?: boolean, rangeFrom?: number, rangeTo?: number, opts?: Record<string, unknown>): Promise<Response>;
268
+ /**
269
+ * Get the content length of an object.
270
+ * This method sends a HEAD request to retrieve the content length of the specified object.
271
+ * @param {string} key - The key of the object to retrieve the content length for.
272
+ * @returns A promise that resolves to the content length of the object in bytes, or 0 if not found.
273
+ * @throws {Error} If the content length header is not found in the response.
274
+ */
156
275
  getContentLength(key: string): Promise<number>;
276
+ /**
277
+ * Checks if an object exists in the S3-compatible service.
278
+ * This method sends a HEAD request to check if the specified object exists.
279
+ * @param {string} key - The key of the object to check.
280
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
281
+ * @returns A promise that resolves to true if the object exists, false if not found, or null if ETag mismatch.
282
+ */
157
283
  objectExists(key: string, opts?: Record<string, unknown>): Promise<ExistResponseCode>;
284
+ /**
285
+ * Retrieves the ETag of an object without downloading its content.
286
+ * @param {string} key - The key of the object to retrieve the ETag for.
287
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
288
+ * @returns {Promise<string | null>} A promise that resolves to the ETag value or null if the object is not found.
289
+ * @throws {Error} If the ETag header is not found in the response.
290
+ * @example
291
+ * const etag = await s3.getEtag('path/to/file.txt');
292
+ * if (etag) {
293
+ * console.log(`File ETag: ${etag}`);
294
+ * }
295
+ */
158
296
  getEtag(key: string, opts?: Record<string, unknown>): Promise<string | null>;
159
- putObject(key: string, data: string | Buffer): Promise<Response>;
297
+ /**
298
+ * Uploads an object to the S3-compatible service.
299
+ * @param {string} key - The key/path where the object will be stored.
300
+ * @param {string | Buffer} data - The data to upload (string or Buffer).
301
+ * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
302
+ * @returns {Promise<Response>} A promise that resolves to the Response object from the upload request.
303
+ * @throws {TypeError} If data is not a string or Buffer.
304
+ * @example
305
+ * // Upload text file
306
+ * await s3.putObject('hello.txt', 'Hello, World!', 'text/plain');
307
+ *
308
+ * // Upload binary data
309
+ * const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
310
+ * await s3.putObject('image.png', buffer, 'image/png');
311
+ */
312
+ putObject(key: string, data: string | Buffer, fileType?: string): Promise<Response>;
313
+ /**
314
+ * Initiates a multipart upload and returns the upload ID.
315
+ * @param {string} key - The key/path where the object will be stored.
316
+ * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
317
+ * @returns {Promise<string>} A promise that resolves to the upload ID for the multipart upload.
318
+ * @throws {TypeError} If key is invalid or fileType is not a string.
319
+ * @throws {Error} If the multipart upload fails to initialize.
320
+ * @example
321
+ * const uploadId = await s3.getMultipartUploadId('large-file.zip', 'application/zip');
322
+ * console.log(`Started multipart upload: ${uploadId}`);
323
+ */
160
324
  getMultipartUploadId(key: string, fileType?: string): Promise<string>;
325
+ /**
326
+ * Uploads a part in a multipart upload.
327
+ * @param {string} key - The key of the object being uploaded.
328
+ * @param {string} uploadId - The upload ID from getMultipartUploadId.
329
+ * @param {Buffer | string} data - The data for this part.
330
+ * @param {number} partNumber - The part number (must be between 1 and 10,000).
331
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
332
+ * @returns {Promise<IT.UploadPart>} A promise that resolves to an object containing the partNumber and etag.
333
+ * @throws {TypeError} If any parameter is invalid.
334
+ * @example
335
+ * const part = await s3.uploadPart(
336
+ * 'large-file.zip',
337
+ * uploadId,
338
+ * partData,
339
+ * 1
340
+ * );
341
+ * console.log(`Part ${part.partNumber} uploaded with ETag: ${part.etag}`);
342
+ */
161
343
  uploadPart(key: string, uploadId: string, data: Buffer | string, partNumber: number, opts?: Record<string, unknown>): Promise<UploadPart>;
344
+ /**
345
+ * Completes a multipart upload by combining all uploaded parts.
346
+ * @param {string} key - The key of the object being uploaded.
347
+ * @param {string} uploadId - The upload ID from getMultipartUploadId.
348
+ * @param {Array<IT.UploadPart>} parts - Array of uploaded parts with partNumber and etag.
349
+ * @returns {Promise<IT.CompleteMultipartUploadResult>} A promise that resolves to the completion result containing the final ETag.
350
+ * @throws {Error} If the multipart upload fails to complete.
351
+ * @example
352
+ * const result = await s3.completeMultipartUpload(
353
+ * 'large-file.zip',
354
+ * uploadId,
355
+ * [
356
+ * { partNumber: 1, etag: 'abc123' },
357
+ * { partNumber: 2, etag: 'def456' }
358
+ * ]
359
+ * );
360
+ * console.log(`Upload completed with ETag: ${result.etag}`);
361
+ */
162
362
  completeMultipartUpload(key: string, uploadId: string, parts: Array<UploadPart>): Promise<CompleteMultipartUploadResult>;
363
+ /**
364
+ * Aborts a multipart upload and removes all uploaded parts.
365
+ * @param {string} key - The key of the object being uploaded.
366
+ * @param {string} uploadId - The upload ID to abort.
367
+ * @returns {Promise<object>} A promise that resolves to an object containing the abort status and details.
368
+ * @throws {TypeError} If key or uploadId is invalid.
369
+ * @throws {Error} If the abort operation fails.
370
+ * @example
371
+ * try {
372
+ * const result = await s3.abortMultipartUpload('large-file.zip', uploadId);
373
+ * console.log('Upload aborted:', result.status);
374
+ * } catch (error) {
375
+ * console.error('Failed to abort upload:', error);
376
+ * }
377
+ */
163
378
  abortMultipartUpload(key: string, uploadId: string): Promise<object>;
164
379
  private _buildCompleteMultipartUploadXml;
380
+ /**
381
+ * Deletes an object from the bucket.
382
+ * This method sends a request to delete the specified object from the bucket.
383
+ * @param {string} key - The key of the object to delete.
384
+ * @returns A promise that resolves to true if the object was deleted successfully, false otherwise.
385
+ */
165
386
  deleteObject(key: string): Promise<boolean>;
387
+ private _deleteObjectsProcess;
388
+ /**
389
+ * Deletes multiple objects from the bucket.
390
+ * @param {string[]} keys - An array of object keys to delete.
391
+ * @returns A promise that resolves to an array of booleans indicating success for each key in order.
392
+ */
393
+ deleteObjects(keys: string[]): Promise<boolean[]>;
166
394
  private _sendRequest;
167
395
  private _handleErrorResponse;
168
396
  private _buildCanonicalQueryString;
@@ -182,8 +410,8 @@ declare const sanitizeETag: (etag: string) => string;
182
410
  * @param {Iterable<() => Promise<unknonw>>} tasks – functions returning Promises
183
411
  * @param {number} [batchSize=30] – max concurrent requests
184
412
  * @param {number} [minIntervalMs=0] – ≥0; 0 means “no pacing”
185
- * @returns {Promise<Array<PromiseSettledResult<unknonw>>>}
413
+ * @returns {Promise<Array<PromiseSettledResult<T>>>}
186
414
  */
187
- declare const runInBatches: (tasks: Iterable<() => Promise<unknown>>, batchSize?: number, minIntervalMs?: number) => Promise<Array<PromiseSettledResult<unknown>>>;
415
+ declare const runInBatches: <T = unknown>(tasks: Iterable<() => Promise<T>>, batchSize?: number, minIntervalMs?: number) => Promise<Array<PromiseSettledResult<T>>>;
188
416
 
189
417
  export { type CompleteMultipartUploadResult, type ErrorWithCode, type ExistResponseCode, type ListBucketResponse, type ListMultipartUploadResponse, type Logger, type S3Config, type UploadPart, s3mini as default, runInBatches, s3mini, sanitizeETag };