lean-s3 0.1.5 → 0.2.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 +2 -2
- package/dist/AmzDate.d.ts +7 -0
- package/dist/AmzDate.js +29 -0
- package/dist/KeyCache.d.ts +5 -0
- package/dist/KeyCache.js +21 -0
- package/dist/S3BucketEntry.d.ts +18 -0
- package/dist/S3BucketEntry.js +29 -0
- package/dist/S3Client.d.ts +130 -0
- package/dist/S3Client.js +485 -0
- package/dist/S3Error.d.ts +14 -0
- package/dist/S3Error.js +15 -0
- package/dist/S3File.d.ts +71 -0
- package/dist/S3File.js +206 -0
- package/dist/S3Stat.d.ts +9 -0
- package/dist/S3Stat.js +35 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +13 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/sign.d.ts +16 -0
- package/dist/sign.js +77 -0
- package/dist/sign.test.d.ts +1 -0
- package/dist/test-common.d.ts +4 -0
- package/dist/test.integration.d.ts +1 -0
- package/dist/test.integration.js +19 -0
- package/dist/url.d.ts +9 -0
- package/dist/url.js +52 -0
- package/dist/url.test.d.ts +1 -0
- package/package.json +19 -14
- package/src/AmzDate.js +0 -56
- package/src/KeyCache.js +0 -36
- package/src/S3BucketEntry.js +0 -91
- package/src/S3Client.js +0 -800
- package/src/S3Error.js +0 -55
- package/src/S3File.js +0 -291
- package/src/S3Stat.js +0 -68
- package/src/index.d.ts +0 -105
- package/src/index.js +0 -4
- package/src/sign.js +0 -145
- package/src/url.js +0 -87
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ BUT...
|
|
|
79
79
|
|
|
80
80
|
Due to its scalability, portability and AWS integrations, pre-signing URLs is `async` and performs poorly in high-performance scenarios. By taking different trade-offs, lean-s3 can presign URLs much faster. I promise! This is the reason you cannot use lean-s3 in the browser.
|
|
81
81
|
|
|
82
|
-
lean-s3 is currently about
|
|
82
|
+
lean-s3 is currently about 30x faster than AWS SDK when it comes to pre-signing URLs[^1]:
|
|
83
83
|
```
|
|
84
84
|
benchmark avg (min … max) p75 / p99
|
|
85
85
|
-------------------------------------------- ---------
|
|
@@ -159,4 +159,4 @@ const client = new S3Client({
|
|
|
159
159
|
Popular S3 provider missing? Open an issue or file a PR!
|
|
160
160
|
|
|
161
161
|
[^1]: Benchmark ran on a `13th Gen Intel(R) Core(TM) i7-1370P` using Node.js `23.11.0`. See `bench/` directory for the used benchmark.
|
|
162
|
-
[^2]: `git clone git@github.com:nikeee/lean-s3.git && cd lean-s3 && npm ci && cd bench && npm ci && npm start`
|
|
162
|
+
[^2]: `git clone git@github.com:nikeee/lean-s3.git && cd lean-s3 && npm ci && npm run build && cd bench && npm ci && npm start`
|
package/dist/AmzDate.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const ONE_DAY = 1000 * 60 * 60 * 24;
|
|
2
|
+
export function getAmzDate(dateTime) {
|
|
3
|
+
const date = pad4(dateTime.getUTCFullYear()) +
|
|
4
|
+
pad2(dateTime.getUTCMonth() + 1) +
|
|
5
|
+
pad2(dateTime.getUTCDate());
|
|
6
|
+
const time = pad2(dateTime.getUTCHours()) +
|
|
7
|
+
pad2(dateTime.getUTCMinutes()) +
|
|
8
|
+
pad2(dateTime.getUTCSeconds()); // it seems that we dont support milliseconds
|
|
9
|
+
return {
|
|
10
|
+
numericDayStart: (dateTime.getTime() / ONE_DAY) | 0,
|
|
11
|
+
date,
|
|
12
|
+
dateTime: `${date}T${time}Z`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function now() {
|
|
16
|
+
return getAmzDate(new Date());
|
|
17
|
+
}
|
|
18
|
+
function pad4(v) {
|
|
19
|
+
return v < 10
|
|
20
|
+
? `000${v}`
|
|
21
|
+
: v < 100
|
|
22
|
+
? `00${v}`
|
|
23
|
+
: v < 1000
|
|
24
|
+
? `0${v}`
|
|
25
|
+
: v.toString();
|
|
26
|
+
}
|
|
27
|
+
function pad2(v) {
|
|
28
|
+
return v < 10 ? `0${v}` : v.toString();
|
|
29
|
+
}
|
package/dist/KeyCache.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as sign from "./sign.js";
|
|
2
|
+
export default class KeyCache {
|
|
3
|
+
#lastNumericDay = -1;
|
|
4
|
+
#keys = new Map();
|
|
5
|
+
computeIfAbsent(date, region, accessKeyId, secretAccessKey) {
|
|
6
|
+
if (date.numericDayStart !== this.#lastNumericDay) {
|
|
7
|
+
this.#keys.clear();
|
|
8
|
+
this.#lastNumericDay = date.numericDayStart;
|
|
9
|
+
// TODO: Add mechanism to clear the cache after some time
|
|
10
|
+
}
|
|
11
|
+
// using accessKeyId to prevent keeping the secretAccessKey somewhere
|
|
12
|
+
const cacheKey = `${date.date}:${region}:${accessKeyId}`;
|
|
13
|
+
const key = this.#keys.get(cacheKey);
|
|
14
|
+
if (key) {
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
const newKey = sign.deriveSigningKey(date.date, region, secretAccessKey);
|
|
18
|
+
this.#keys.set(cacheKey, newKey);
|
|
19
|
+
return newKey;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChecksumType, ChecksumAlgorithm, StorageClass } from "./index.ts";
|
|
2
|
+
/**
|
|
3
|
+
* @internal Normally, we'd use an interface for that, but having a class with pre-defined fields makes it easier for V8 top optimize hidden classes.
|
|
4
|
+
*/
|
|
5
|
+
export default class S3BucketEntry {
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly size: number;
|
|
8
|
+
readonly lastModified: Date;
|
|
9
|
+
readonly etag: string;
|
|
10
|
+
readonly storageClass: StorageClass;
|
|
11
|
+
readonly checksumAlgorithm: ChecksumAlgorithm | undefined;
|
|
12
|
+
readonly checksumType: ChecksumType | undefined;
|
|
13
|
+
constructor(key: string, size: number, lastModified: Date, etag: string, storageClass: StorageClass, checksumAlgorithm: ChecksumAlgorithm | undefined, checksumType: ChecksumType | undefined);
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
static parse(source: any): S3BucketEntry;
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal Normally, we'd use an interface for that, but having a class with pre-defined fields makes it easier for V8 top optimize hidden classes.
|
|
3
|
+
*/
|
|
4
|
+
export default class S3BucketEntry {
|
|
5
|
+
key;
|
|
6
|
+
size;
|
|
7
|
+
lastModified;
|
|
8
|
+
etag;
|
|
9
|
+
storageClass;
|
|
10
|
+
checksumAlgorithm;
|
|
11
|
+
checksumType;
|
|
12
|
+
constructor(key, size, lastModified, etag, storageClass, checksumAlgorithm, checksumType) {
|
|
13
|
+
this.key = key;
|
|
14
|
+
this.size = size;
|
|
15
|
+
this.lastModified = lastModified;
|
|
16
|
+
this.etag = etag;
|
|
17
|
+
this.storageClass = storageClass;
|
|
18
|
+
this.checksumAlgorithm = checksumAlgorithm;
|
|
19
|
+
this.checksumType = checksumType;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: internal use only, any is ok here
|
|
25
|
+
static parse(source) {
|
|
26
|
+
// TODO: check values and throw exceptions
|
|
27
|
+
return new S3BucketEntry(source.Key, source.Size, new Date(source.LastModified), source.ETag, source.StorageClass, source.ChecksumAlgorithm, source.ChecksumType);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import S3File from "./S3File.ts";
|
|
2
|
+
import S3BucketEntry from "./S3BucketEntry.ts";
|
|
3
|
+
import * as amzDate from "./AmzDate.ts";
|
|
4
|
+
import type { Acl, PresignableHttpMethod, StorageClass, UndiciBodyInit } from "./index.ts";
|
|
5
|
+
export declare const write: unique symbol;
|
|
6
|
+
export declare const stream: unique symbol;
|
|
7
|
+
export interface S3ClientOptions {
|
|
8
|
+
bucket: string;
|
|
9
|
+
region: string;
|
|
10
|
+
endpoint: string;
|
|
11
|
+
accessKeyId: string;
|
|
12
|
+
secretAccessKey: string;
|
|
13
|
+
sessionToken?: string;
|
|
14
|
+
}
|
|
15
|
+
export type OverridableS3ClientOptions = Pick<S3ClientOptions, "region" | "bucket" | "endpoint">;
|
|
16
|
+
export type CreateFileInstanceOptions = {};
|
|
17
|
+
export interface S3FilePresignOptions {
|
|
18
|
+
contentHash: Buffer;
|
|
19
|
+
/** Seconds. */
|
|
20
|
+
expiresIn: number;
|
|
21
|
+
method: PresignableHttpMethod;
|
|
22
|
+
storageClass: StorageClass;
|
|
23
|
+
acl: Acl;
|
|
24
|
+
}
|
|
25
|
+
export type ListObjectsResponse = {
|
|
26
|
+
name: string;
|
|
27
|
+
prefix: string | undefined;
|
|
28
|
+
startAfter: string | undefined;
|
|
29
|
+
isTruncated: boolean;
|
|
30
|
+
continuationToken: string | undefined;
|
|
31
|
+
maxKeys: number;
|
|
32
|
+
keyCount: number;
|
|
33
|
+
nextContinuationToken: string | undefined;
|
|
34
|
+
contents: readonly S3BucketEntry[];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A configured S3 bucket instance for managing files.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```js
|
|
41
|
+
* // Basic bucket setup
|
|
42
|
+
* const bucket = new S3Client({
|
|
43
|
+
* bucket: "my-bucket",
|
|
44
|
+
* accessKeyId: "key",
|
|
45
|
+
* secretAccessKey: "secret"
|
|
46
|
+
* });
|
|
47
|
+
* // Get file instance
|
|
48
|
+
* const file = bucket.file("image.jpg");
|
|
49
|
+
* await file.delete();
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export default class S3Client {
|
|
53
|
+
#private;
|
|
54
|
+
/**
|
|
55
|
+
* Create a new instance of an S3 bucket so that credentials can be managed from a single instance instead of being passed to every method.
|
|
56
|
+
*
|
|
57
|
+
* @param options The default options to use for the S3 client.
|
|
58
|
+
*/
|
|
59
|
+
constructor(options: S3ClientOptions);
|
|
60
|
+
/**
|
|
61
|
+
* Creates an S3File instance for the given path.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} path
|
|
64
|
+
* @param {Partial<CreateFileInstanceOptions>} [options] TODO
|
|
65
|
+
* @returns {S3File}
|
|
66
|
+
* @example
|
|
67
|
+
* ```js
|
|
68
|
+
* const file = client.file("image.jpg");
|
|
69
|
+
* await file.write(imageData);
|
|
70
|
+
*
|
|
71
|
+
* const configFile = client.file("config.json", {
|
|
72
|
+
* type: "application/json",
|
|
73
|
+
* acl: "private"
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
file(path: string, options?: Partial<CreateFileInstanceOptions>): S3File;
|
|
78
|
+
/**
|
|
79
|
+
* Generate a presigned URL for temporary access to a file.
|
|
80
|
+
* Useful for generating upload/download URLs without exposing credentials.
|
|
81
|
+
* @returns The operation on {@link S3Client#presign.path} as a pre-signed URL.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```js
|
|
85
|
+
* const downloadUrl = client.presign("file.pdf", {
|
|
86
|
+
* expiresIn: 3600 // 1 hour
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
|
|
91
|
+
storageClass, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
|
|
92
|
+
deleteObjects(objects: readonly S3BucketEntry[] | readonly string[], options: unknown): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Uses `ListObjectsV2` to iterate over all keys. Pagination and continuation is handled internally.
|
|
95
|
+
*/
|
|
96
|
+
listIterating(options: {
|
|
97
|
+
prefix?: string;
|
|
98
|
+
startAfter?: string;
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
internalPageSize?: number;
|
|
101
|
+
}): AsyncGenerator<S3BucketEntry>;
|
|
102
|
+
list(options?: {
|
|
103
|
+
prefix?: string;
|
|
104
|
+
maxKeys?: number;
|
|
105
|
+
startAfter?: string;
|
|
106
|
+
continuationToken?: string;
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
}): Promise<ListObjectsResponse>;
|
|
109
|
+
/**
|
|
110
|
+
* @internal
|
|
111
|
+
* @param {import("./index.d.ts").UndiciBodyInit} data TODO
|
|
112
|
+
*/
|
|
113
|
+
[write](path: string, data: UndiciBodyInit, contentType: string, contentLength: number | undefined, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined, signal?: AbortSignal | undefined): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
117
|
+
[stream](path: string, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined): import("stream/web").ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} amzCredential
|
|
121
|
+
* @param {import("./AmzDate.ts").AmzDate} date
|
|
122
|
+
* @param {number} expiresIn
|
|
123
|
+
* @param {string} headerList
|
|
124
|
+
* @param {StorageClass | null | undefined} storageClass
|
|
125
|
+
* @param {string | null | undefined} sessionToken
|
|
126
|
+
* @param {Acl | null | undefined} acl
|
|
127
|
+
* @param {string | null | undefined} contentHashStr
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
export declare function buildSearchParams(amzCredential: string, date: amzDate.AmzDate, expiresIn: number, headerList: string, contentHashStr: string | null | undefined, storageClass: StorageClass | null | undefined, sessionToken: string | null | undefined, acl: Acl | null | undefined): string;
|