s3mini 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peter Jensen (good-lly)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # s3mini | Tiny & fast S3 client built for the edge.
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!)
4
+
5
+ ## Features
6
+
7
+ - πŸš€ Light and fast: averages β‰ˆ15 % more ops/s and only ~14 KB (minified, not gzipped).
8
+ - πŸ”§ Zero dependencies; supports AWS SigV4 (no pre-signed requests).
9
+ - 🟠 Works on Cloudflare Workers; ideal for edge computing, Node, and Bun (no browser support).
10
+ - πŸ”‘ Only the essential S3 APIsβ€”improved list, put, get, delete, and a few more.
11
+ - πŸ“¦ **BYOS3** β€” _Bring your own S3-compatible bucket_ (tested on Cloudflare R2, Backblaze B2, DigitalOcean Spaces, MinIO; Ceph and Garage are in the queue).
12
+
13
+ ![GitHub Repo stars](https://img.shields.io/github/stars/good-lly/s3mini?style=social)
14
+ ![GitHub License](https://img.shields.io/github/license/good-lly/s3mini)
15
+
16
+ Dev:
17
+ ![GitHub package.json version](https://img.shields.io/github/package-json/v/good-lly/s3mini?color=green)
18
+ ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/good-lly/s3mini)
19
+ [![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)
20
+ [![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)
21
+
22
+ ![performance-image](https://raw.githubusercontent.com/good-lly/s3mini/dev/performance-screenshot.png)
23
+
24
+ ## Table of Contents
25
+
26
+ - [Supported Ops](#supported-ops)
27
+ - [Installation](#installation)
28
+ - [Usage](#usage)
29
+ - [Security Notes](#security-notes)
30
+ - [πŸ’™ Contributions welcomed!](#contributions-welcomed)
31
+ - [License](#license)
32
+
33
+ ## Supported Ops
34
+
35
+ The library supports a subset of S3 operations, focusing on essential features, making it suitable for environments with limited resources.
36
+
37
+ #### Bucket ops
38
+
39
+ - βœ… HeadBucket (bucketExists)
40
+ - βœ… createBucket (createBucket)
41
+
42
+ #### Objects ops
43
+
44
+ - βœ… ListObjectsV2 (listObjects)
45
+ - βœ… GetObject (getObject, getObjectWithETag, getObjectRaw, getObjectArrayBuffer)
46
+ - βœ… PutObject (putObject)
47
+ - βœ… DeleteObject (deleteObject)
48
+ - βœ… HeadObject (objectExists, getEtag, getContentLength)
49
+ - βœ… listMultipartUploads
50
+ - βœ… CreateMultipartUpload (getMultipartUploadId)
51
+ - βœ… completeMultipartUpload
52
+ - βœ… abortMultipartUpload
53
+ - βœ… uploadPart
54
+ - ❌ CopyObject: Not implemented (tbd)
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ npm install s3mini
60
+ ```
61
+
62
+ ```bash
63
+ yarn add s3mini
64
+ ```
65
+
66
+ ```bash
67
+ pnpm add s3mini
68
+ ```
69
+
70
+ ## Usage
71
+
72
+ ```typescript
73
+ import { s3mini, sanitizeETag } from 's3mini';
74
+
75
+ const s3client = new s3mini({
76
+ accessKeyId: config.accessKeyId,
77
+ secretAccessKey: config.secretAccessKey,
78
+ endpoint: config.endpoint,
79
+ region: config.region,
80
+ });
81
+
82
+ // Basic bucket ops
83
+ let exists: boolean = false;
84
+ try {
85
+ // Check if the bucket exists
86
+ exists = await s3client.bucketExists();
87
+ } catch (err) {
88
+ throw new Error(`Failed bucketExists() call, wrong credentials maybe: ${err.message}`);
89
+ }
90
+ if (!exists) {
91
+ // Create the bucket based on the endpoint bucket name
92
+ await s3client.createBucket();
93
+ }
94
+
95
+ // Basic object ops
96
+ // key is the name of the object in the bucket
97
+ const smallObjectKey: string = 'small-object.txt';
98
+ // content is the data you want to store in the object
99
+ // it can be a string or Buffer (recommended for large objects)
100
+ const smallObjectContent: string = 'Hello, world!';
101
+
102
+ // check if the object exists
103
+ const objectExists: boolean = await s3client.objectExists(smallObjectKey);
104
+ let etag: string | null = null;
105
+ if (!objectExists) {
106
+ // put/upload the object, content can be a string or Buffer
107
+ // to add object into "folder", use "folder/filename.txt" as key
108
+ const resp: Response = await s3client.putObject(smallObjectKey, smallObjectContent);
109
+ // you can also get etag via getEtag method
110
+ // const etag: string = await s3client.getEtag(smallObjectKey);
111
+ etag = sanitizeETag(resp.headers.get('etag'));
112
+ }
113
+
114
+ // get the object, null if not found
115
+ const objectData: string | null = await s3client.getObject(smallObjectKey);
116
+ console.log('Object data:', objectData);
117
+
118
+ // get the object with ETag, null if not found
119
+ const response2: Response = await s3mini.getObject(smallObjectKey, { 'if-none-match': etag });
120
+ if (response2) {
121
+ // ETag changed so we can get the object data and new ETag
122
+ // Note: ETag is not guaranteed to be the same as the MD5 hash of the object
123
+ // ETag is sanitized to remove quotes
124
+ const etag2: string = sanitizeETag(response2.headers.get('etag'));
125
+ console.log('Object data with ETag:', response2.body, 'ETag:', etag2);
126
+ } else {
127
+ console.log('Object not found or ETag does match.');
128
+ }
129
+
130
+ // list objects in the bucket, null if bucket is empty
131
+ // Note: listObjects uses listObjectsV2 API and iterate over all pages
132
+ // so it will return all objects in the bucket which can take a while
133
+ // If you want to limit the number of objects returned, use the maxKeys option
134
+ // If you want to list objects in a specific "folder", use "folder/" as prefix
135
+ // Example s3client.listObjects({"/" "myfolder/"})
136
+ const list: object[] | null = await s3client.listObjects();
137
+ if (list) {
138
+ console.log('List of objects:', list);
139
+ } else {
140
+ console.log('No objects found in the bucket.');
141
+ }
142
+
143
+ // delete the object
144
+ const wasDeleted: boolean = await s3client.deleteObject(smallObjectKey);
145
+
146
+ // Multipart upload
147
+ const multipartKey = 'multipart-object.txt';
148
+ const large_buffer = new Uint8Array(1024 * 1024 * 15); // 15 MB buffer
149
+ const partSize = 8 * 1024 * 1024; // 8 MB
150
+ const totalParts = Math.ceil(large_buffer.length / partSize);
151
+ // Beware! This will return always a new uploadId
152
+ // if you want to use the same uploadId, you need to store it somewhere
153
+ const uploadId = await s3client.getMultipartUploadId(multipartKey);
154
+ const uploadPromises = [];
155
+ for (let i = 0; i < totalParts; i++) {
156
+ const partBuffer = large_buffer.subarray(i * partSize, (i + 1) * partSize);
157
+ // upload each part
158
+ // Note: uploadPart returns a promise, so you can use Promise.all to upload all parts in parallel
159
+ // but be careful with the number of parallel uploads, it can cause throttling
160
+ // or errors if you upload too many parts at once
161
+ // You can also use generator functions to upload parts in batches
162
+ uploadPromises.push(s3client.uploadPart(multipartKey, uploadId, partBuffer, i + 1));
163
+ }
164
+ const uploadResponses = await Promise.all(uploadPromises);
165
+ const parts = uploadResponses.map((response, index) => ({
166
+ partNumber: index + 1,
167
+ etag: response.etag,
168
+ }));
169
+ // Complete the multipart upload
170
+ const completeResponse = await s3client.completeMultipartUpload(multipartKey, uploadId, parts);
171
+ const completeEtag = completeResponse.etag;
172
+
173
+ // List multipart uploads
174
+ // returns object with uploadId and key
175
+ const multipartUploads: object = await s3client.listMultipartUploads();
176
+ // Abort the multipart upload
177
+ const abortResponse = await s3client.abortMultipartUpload(multipartUploads.key, multipartUploads.uploadId);
178
+
179
+ // Multipart download
180
+ // lets test getObjectRaw with range
181
+ const rangeStart = 2048 * 1024; // 2 MB
182
+ const rangeEnd = 8 * 1024 * 1024 * 2; // 16 MB
183
+ const rangeResponse = await s3client.getObjectRaw(multipartKey, false, rangeStart, rangeEnd);
184
+ const rangeData = await rangeResponse.arrayBuffer();
185
+ ```
186
+
187
+ For more check [USAGE.md](USAGE.md) file, examples and tests.
188
+
189
+ ## Security Notes
190
+
191
+ - The library masks sensitive information (access keys, session tokens, etc.) when logging.
192
+ - Always protect your AWS credentials and avoid hard-coding them in your application (!!!). Use environment variables. Use environment variables or a secure vault for storing credentials.
193
+ - Ensure you have the necessary permissions to access the S3 bucket and perform operations.
194
+ - Be cautious when using multipart uploads, as they can incur additional costs if not managed properly.
195
+ - Authors are not responsible for any data loss or security breaches resulting from improper usage of the library.
196
+ - 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.
197
+
198
+ ## Contributions welcomed!
199
+
200
+ Contributions are greatly appreciated! If you have an idea for a new feature or have found a bug, we encourage you to get involved:
201
+
202
+ - _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.
203
+
204
+ - _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.
205
+
206
+ - _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.
207
+
208
+ - _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.
209
+
210
+ 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!
211
+
212
+ ## License
213
+
214
+ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
@@ -0,0 +1,187 @@
1
+ interface S3Config {
2
+ accessKeyId: string;
3
+ secretAccessKey: string;
4
+ endpoint: string;
5
+ region?: string;
6
+ requestSizeInBytes?: number;
7
+ requestAbortTimeout?: number;
8
+ logger?: Logger;
9
+ }
10
+ interface Logger {
11
+ info: (message: string, ...args: unknown[]) => void;
12
+ warn: (message: string, ...args: unknown[]) => void;
13
+ error: (message: string, ...args: unknown[]) => void;
14
+ }
15
+ interface UploadPart {
16
+ partNumber: number;
17
+ etag: string;
18
+ }
19
+ interface CompleteMultipartUploadResult {
20
+ location: string;
21
+ bucket: string;
22
+ key: string;
23
+ etag: string;
24
+ eTag: string;
25
+ ETag: string;
26
+ }
27
+ interface ListBucketResult {
28
+ keyCount: string;
29
+ contents?: Array<Record<string, unknown>>;
30
+ }
31
+ interface ListBucketError {
32
+ error: {
33
+ code: string;
34
+ message: string;
35
+ };
36
+ }
37
+ type ListBucketResponse = {
38
+ listBucketResult: ListBucketResult;
39
+ } | {
40
+ error: ListBucketError;
41
+ };
42
+ interface ListMultipartUploadSuccess {
43
+ listMultipartUploadsResult: {
44
+ bucket: string;
45
+ key: string;
46
+ uploadId: string;
47
+ size?: number;
48
+ mtime?: Date | undefined;
49
+ etag?: string;
50
+ eTag?: string;
51
+ parts: UploadPart[];
52
+ isTruncated: boolean;
53
+ uploads: UploadPart[];
54
+ };
55
+ }
56
+ interface MultipartUploadError {
57
+ error: {
58
+ code: string;
59
+ message: string;
60
+ };
61
+ }
62
+ interface ErrorWithCode {
63
+ code?: string;
64
+ cause?: {
65
+ code?: string;
66
+ };
67
+ }
68
+ type ListMultipartUploadResponse = ListMultipartUploadSuccess | MultipartUploadError;
69
+ type HttpMethod = 'POST' | 'GET' | 'HEAD' | 'PUT' | 'DELETE';
70
+ type ExistResponseCode = false | true | null;
71
+
72
+ /**
73
+ * S3 class for interacting with S3-compatible object storage services.
74
+ * This class provides methods for common S3 operations such as uploading, downloading,
75
+ * and deleting objects, as well as multipart uploads.
76
+ *
77
+ * @class
78
+ * @example
79
+ * const s3 = new CoreS3({
80
+ * accessKeyId: 'your-access-key',
81
+ * secretAccessKey: 'your-secret-key',
82
+ * endpoint: 'https://your-s3-endpoint.com',
83
+ * region: 'us-east-1' // by default is auto
84
+ * });
85
+ *
86
+ * // Upload a file
87
+ * await s3.putObject('example.txt', 'Hello, World!');
88
+ *
89
+ * // Download a file
90
+ * const content = await s3.getObject('example.txt');
91
+ *
92
+ * // Delete a file
93
+ * await s3.deleteObject('example.txt');
94
+ */
95
+ declare class s3mini {
96
+ /**
97
+ * Creates an instance of the S3 class.
98
+ *
99
+ * @constructor
100
+ * @param {Object} config - Configuration options for the S3 instance.
101
+ * @param {string} config.accessKeyId - The access key ID for authentication.
102
+ * @param {string} config.secretAccessKey - The secret access key for authentication.
103
+ * @param {string} config.endpoint - The endpoint URL of the S3-compatible service.
104
+ * @param {string} [config.region='auto'] - The region of the S3 service.
105
+ * @param {number} [config.requestSizeInBytes=8388608] - The request size of a single request in bytes (AWS S3 is 8MB).
106
+ * @param {number} [config.requestAbortTimeout=undefined] - The timeout in milliseconds after which a request should be aborted (careful on streamed requests).
107
+ * @param {Object} [config.logger=null] - A logger object with methods like info, warn, error.
108
+ * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
109
+ */
110
+ private accessKeyId;
111
+ private secretAccessKey;
112
+ private endpoint;
113
+ private region;
114
+ private requestSizeInBytes;
115
+ private requestAbortTimeout?;
116
+ private logger?;
117
+ private fullDatetime;
118
+ private shortDatetime;
119
+ private signingKey;
120
+ private credentialScope;
121
+ constructor({ accessKeyId, secretAccessKey, endpoint, region, requestSizeInBytes, requestAbortTimeout, logger, }: S3Config);
122
+ private _sanitize;
123
+ private _log;
124
+ private _validateConstructorParams;
125
+ private _ensureValidUrl;
126
+ private _validateMethodIsGetOrHead;
127
+ private _checkKey;
128
+ private _checkDelimiter;
129
+ private _checkPrefix;
130
+ private _checkOpts;
131
+ private _filterIfHeaders;
132
+ private _validateUploadPartParams;
133
+ private _sign;
134
+ private _buildCanonicalHeaders;
135
+ private _buildCanonicalRequest;
136
+ private _buildStringToSign;
137
+ private _calculateSignature;
138
+ private _buildAuthorizationHeader;
139
+ private _signedRequest;
140
+ getProps(): S3Config;
141
+ setProps(props: S3Config): void;
142
+ sanitizeETag(etag: string): string;
143
+ createBucket(): Promise<boolean>;
144
+ bucketExists(): Promise<boolean>;
145
+ listObjects(delimiter?: string, prefix?: string, maxKeys?: number, opts?: Record<string, unknown>): Promise<object[] | null>;
146
+ listMultipartUploads(delimiter?: string, prefix?: string, method?: HttpMethod, opts?: Record<string, string | number | boolean | undefined>): Promise<ListMultipartUploadSuccess | MultipartUploadError>;
147
+ getObject(key: string, opts?: Record<string, unknown>): Promise<string | null>;
148
+ getObjectArrayBuffer(key: string, opts?: Record<string, unknown>): Promise<ArrayBuffer | null>;
149
+ getObjectWithETag(key: string, opts?: Record<string, unknown>): Promise<{
150
+ etag: string | null;
151
+ data: ArrayBuffer | null;
152
+ }>;
153
+ getObjectRaw(key: string, wholeFile?: boolean, rangeFrom?: number, rangeTo?: number, opts?: Record<string, unknown>): Promise<Response>;
154
+ getContentLength(key: string): Promise<number>;
155
+ objectExists(key: string, opts?: Record<string, unknown>): Promise<ExistResponseCode>;
156
+ getEtag(key: string, opts?: Record<string, unknown>): Promise<string | null>;
157
+ putObject(key: string, data: string | Buffer): Promise<Response>;
158
+ getMultipartUploadId(key: string, fileType?: string): Promise<string>;
159
+ uploadPart(key: string, uploadId: string, data: Buffer | string, partNumber: number, opts?: Record<string, unknown>): Promise<UploadPart>;
160
+ completeMultipartUpload(key: string, uploadId: string, parts: Array<UploadPart>): Promise<CompleteMultipartUploadResult>;
161
+ abortMultipartUpload(key: string, uploadId: string): Promise<object>;
162
+ private _buildCompleteMultipartUploadXml;
163
+ deleteObject(key: string): Promise<boolean>;
164
+ private _sendRequest;
165
+ private _handleErrorResponse;
166
+ private _buildCanonicalQueryString;
167
+ private _getSignatureKey;
168
+ }
169
+
170
+ /**
171
+ * Sanitize ETag value by removing quotes and XML entities
172
+ * @param etag ETag value to sanitize
173
+ * @returns Sanitized ETag
174
+ */
175
+ declare const sanitizeETag: (etag: string) => string;
176
+ /**
177
+ * Run async-returning tasks in batches with an *optional* minimum
178
+ * spacing (minIntervalMs) between the *start* times of successive batches.
179
+ *
180
+ * @param {Iterable<() => Promise<unknonw>>} tasks – functions returning Promises
181
+ * @param {number} [batchSize=30] – max concurrent requests
182
+ * @param {number} [minIntervalMs=0] – β‰₯0; 0 means β€œno pacing”
183
+ * @returns {Promise<Array<PromiseSettledResult<unknonw>>>}
184
+ */
185
+ declare const runInBatches: (tasks: Iterable<() => Promise<unknown>>, batchSize?: number, minIntervalMs?: number) => Promise<Array<PromiseSettledResult<unknown>>>;
186
+
187
+ export { type CompleteMultipartUploadResult, type ErrorWithCode, type ExistResponseCode, type ListBucketResponse, type ListMultipartUploadResponse, type Logger, type S3Config, type UploadPart, s3mini as default, runInBatches, s3mini, sanitizeETag };