lean-s3 0.2.0 → 0.2.1
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 +13 -1
- package/dist/S3Client.d.ts +74 -29
- package/dist/S3Client.js +178 -53
- package/dist/S3File.d.ts +6 -5
- package/dist/S3File.js +6 -17
- package/dist/index.d.ts +15 -1
- package/dist/index.js +0 -8
- package/dist/sign.d.ts +1 -1
- package/dist/sign.js +2 -2
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ $ du -sh node_modules
|
|
|
77
77
|
`lean-s3` is _so_ lean that it is ~1.8MB just to do a couple of HTTP requests <img src="https://cdn.frankerfacez.com/emoticon/480839/1" width="20" height="20">
|
|
78
78
|
BUT...
|
|
79
79
|
|
|
80
|
-
Due to
|
|
80
|
+
Due to the scalability, portability and AWS integrations of @aws-sdk/client-s3, 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
82
|
lean-s3 is currently about 30x faster than AWS SDK when it comes to pre-signing URLs[^1]:
|
|
83
83
|
```
|
|
@@ -121,6 +121,18 @@ We try to keep this library small. If you happen to need something that is not s
|
|
|
121
121
|
|
|
122
122
|
See [DESIGN_DECISIONS.md](./DESIGN_DECISIONS.md) to read about why this library is the way it is.
|
|
123
123
|
|
|
124
|
+
## Supported Operations
|
|
125
|
+
|
|
126
|
+
### Bucket Operations
|
|
127
|
+
- ✅ [`CreateBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html) via `.createBucket`
|
|
128
|
+
|
|
129
|
+
### Object Operations
|
|
130
|
+
- ✅ [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) via `.list`/`.listIterating`
|
|
131
|
+
- ✅ [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) via `.deleteObjects`
|
|
132
|
+
- ✅ [`DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html) via `S3File.delete`
|
|
133
|
+
- ✅ [`PutObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) via `S3File.write`
|
|
134
|
+
- ✅ [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) via `S3File.exists`/`S3File.stat`
|
|
135
|
+
|
|
124
136
|
## Example Configurations
|
|
125
137
|
### Hetzner Object Storage
|
|
126
138
|
```js
|
package/dist/S3Client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import S3File from "./S3File.ts";
|
|
2
2
|
import S3BucketEntry from "./S3BucketEntry.ts";
|
|
3
3
|
import * as amzDate from "./AmzDate.ts";
|
|
4
|
-
import type { Acl, PresignableHttpMethod, StorageClass, UndiciBodyInit } from "./index.ts";
|
|
4
|
+
import type { Acl, BucketInfo, BucketLocationInfo, PresignableHttpMethod, StorageClass, UndiciBodyInit } from "./index.ts";
|
|
5
5
|
export declare const write: unique symbol;
|
|
6
6
|
export declare const stream: unique symbol;
|
|
7
7
|
export interface S3ClientOptions {
|
|
@@ -14,6 +14,9 @@ export interface S3ClientOptions {
|
|
|
14
14
|
}
|
|
15
15
|
export type OverridableS3ClientOptions = Pick<S3ClientOptions, "region" | "bucket" | "endpoint">;
|
|
16
16
|
export type CreateFileInstanceOptions = {};
|
|
17
|
+
export type DeleteObjectsOptions = {
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
};
|
|
17
20
|
export interface S3FilePresignOptions {
|
|
18
21
|
contentHash: Buffer;
|
|
19
22
|
/** Seconds. */
|
|
@@ -22,6 +25,19 @@ export interface S3FilePresignOptions {
|
|
|
22
25
|
storageClass: StorageClass;
|
|
23
26
|
acl: Acl;
|
|
24
27
|
}
|
|
28
|
+
export type ListObjectsOptions = {
|
|
29
|
+
prefix?: string;
|
|
30
|
+
maxKeys?: number;
|
|
31
|
+
startAfter?: string;
|
|
32
|
+
continuationToken?: string;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
};
|
|
35
|
+
export type ListObjectsIteratingOptions = {
|
|
36
|
+
prefix?: string;
|
|
37
|
+
startAfter?: string;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
internalPageSize?: number;
|
|
40
|
+
};
|
|
25
41
|
export type ListObjectsResponse = {
|
|
26
42
|
name: string;
|
|
27
43
|
prefix: string | undefined;
|
|
@@ -33,6 +49,12 @@ export type ListObjectsResponse = {
|
|
|
33
49
|
nextContinuationToken: string | undefined;
|
|
34
50
|
contents: readonly S3BucketEntry[];
|
|
35
51
|
};
|
|
52
|
+
export type BucketCreationOptions = {
|
|
53
|
+
locationConstraint?: string;
|
|
54
|
+
location?: BucketLocationInfo;
|
|
55
|
+
info?: BucketInfo;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
};
|
|
36
58
|
/**
|
|
37
59
|
* A configured S3 bucket instance for managing files.
|
|
38
60
|
*
|
|
@@ -60,9 +82,27 @@ export default class S3Client {
|
|
|
60
82
|
/**
|
|
61
83
|
* Creates an S3File instance for the given path.
|
|
62
84
|
*
|
|
63
|
-
* @param {string} path
|
|
85
|
+
* @param {string} path The path to the object in the bucket. ALso known as [object key](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html).
|
|
86
|
+
* We recommend not using the following characters in a key name because of significant special character handling, which isn't consistent across all applications (see [AWS docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html)):
|
|
87
|
+
* - Backslash (`\\`)
|
|
88
|
+
* - Left brace (`{`)
|
|
89
|
+
* - Non-printable ASCII characters (128–255 decimal characters)
|
|
90
|
+
* - Caret or circumflex (`^`)
|
|
91
|
+
* - Right brace (`}`)
|
|
92
|
+
* - Percent character (`%`)
|
|
93
|
+
* - Grave accent or backtick (`\``)
|
|
94
|
+
* - Right bracket (`]`)
|
|
95
|
+
* - Quotation mark (`"`)
|
|
96
|
+
* - Greater than sign (`>`)
|
|
97
|
+
* - Left bracket (`[`)
|
|
98
|
+
* - Tilde (`~`)
|
|
99
|
+
* - Less than sign (`<`)
|
|
100
|
+
* - Pound sign (`#`)
|
|
101
|
+
* - Vertical bar or pipe (`|`)
|
|
102
|
+
*
|
|
103
|
+
* lean-s3 does not enforce these restrictions.
|
|
104
|
+
*
|
|
64
105
|
* @param {Partial<CreateFileInstanceOptions>} [options] TODO
|
|
65
|
-
* @returns {S3File}
|
|
66
106
|
* @example
|
|
67
107
|
* ```js
|
|
68
108
|
* const file = client.file("image.jpg");
|
|
@@ -89,23 +129,39 @@ export default class S3Client {
|
|
|
89
129
|
*/
|
|
90
130
|
presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
|
|
91
131
|
storageClass, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
|
|
92
|
-
deleteObjects(objects: readonly S3BucketEntry[] | readonly string[], options: unknown): Promise<void>;
|
|
93
132
|
/**
|
|
94
|
-
* Uses `
|
|
133
|
+
* Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
|
|
134
|
+
*/
|
|
135
|
+
deleteObjects(objects: readonly S3BucketEntry[] | readonly string[], options?: DeleteObjectsOptions): Promise<{
|
|
136
|
+
errors: {
|
|
137
|
+
code: any;
|
|
138
|
+
key: any;
|
|
139
|
+
message: any;
|
|
140
|
+
versionId: any;
|
|
141
|
+
}[];
|
|
142
|
+
} | null>;
|
|
143
|
+
/**
|
|
144
|
+
* Creates a new bucket on the S3 server.
|
|
145
|
+
*
|
|
146
|
+
* @param name The name of the bucket to create. AWS the name according to [some rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html). The most important ones are:
|
|
147
|
+
* - Bucket names must be between `3` (min) and `63` (max) characters long.
|
|
148
|
+
* - Bucket names can consist only of lowercase letters, numbers, periods (`.`), and hyphens (`-`).
|
|
149
|
+
* - Bucket names must begin and end with a letter or number.
|
|
150
|
+
* - Bucket names must not contain two adjacent periods.
|
|
151
|
+
* - Bucket names must not be formatted as an IP address (for example, `192.168.5.4`).
|
|
152
|
+
*
|
|
153
|
+
* @throws {Error} If the bucket name is invalid.
|
|
154
|
+
* @remarks Uses [`CreateBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html)
|
|
155
|
+
*/
|
|
156
|
+
createBucket(name: string, options?: BucketCreationOptions): Promise<void>;
|
|
157
|
+
/**
|
|
158
|
+
* Uses [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys. Pagination and continuation is handled internally.
|
|
95
159
|
*/
|
|
96
|
-
listIterating(options:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}): AsyncGenerator<S3BucketEntry>;
|
|
102
|
-
list(options?: {
|
|
103
|
-
prefix?: string;
|
|
104
|
-
maxKeys?: number;
|
|
105
|
-
startAfter?: string;
|
|
106
|
-
continuationToken?: string;
|
|
107
|
-
signal?: AbortSignal;
|
|
108
|
-
}): Promise<ListObjectsResponse>;
|
|
160
|
+
listIterating(options: ListObjectsIteratingOptions): AsyncGenerator<S3BucketEntry>;
|
|
161
|
+
/**
|
|
162
|
+
* Implements [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys.
|
|
163
|
+
*/
|
|
164
|
+
list(options?: ListObjectsOptions): Promise<ListObjectsResponse>;
|
|
109
165
|
/**
|
|
110
166
|
* @internal
|
|
111
167
|
* @param {import("./index.d.ts").UndiciBodyInit} data TODO
|
|
@@ -116,15 +172,4 @@ export default class S3Client {
|
|
|
116
172
|
*/
|
|
117
173
|
[stream](path: string, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined): import("stream/web").ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
118
174
|
}
|
|
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
175
|
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;
|
package/dist/S3Client.js
CHANGED
|
@@ -10,7 +10,10 @@ import { buildRequestUrl, getRangeHeader, prepareHeadersForSigning, } from "./ur
|
|
|
10
10
|
export const write = Symbol("write");
|
|
11
11
|
export const stream = Symbol("stream");
|
|
12
12
|
const xmlParser = new XMLParser();
|
|
13
|
-
const xmlBuilder = new XMLBuilder(
|
|
13
|
+
const xmlBuilder = new XMLBuilder({
|
|
14
|
+
attributeNamePrefix: "$",
|
|
15
|
+
ignoreAttributes: false,
|
|
16
|
+
});
|
|
14
17
|
/**
|
|
15
18
|
* A configured S3 bucket instance for managing files.
|
|
16
19
|
*
|
|
@@ -28,11 +31,9 @@ const xmlBuilder = new XMLBuilder();
|
|
|
28
31
|
* ```
|
|
29
32
|
*/
|
|
30
33
|
export default class S3Client {
|
|
31
|
-
/** @type {Readonly<S3ClientOptions>} */
|
|
32
34
|
#options;
|
|
33
35
|
#keyCache = new KeyCache();
|
|
34
|
-
// TODO: pass options to this in client
|
|
35
|
-
/** @type {Dispatcher} */
|
|
36
|
+
// TODO: pass options to this in client? Do we want to expose tjhe internal use of undici?
|
|
36
37
|
#dispatcher = new Agent();
|
|
37
38
|
/**
|
|
38
39
|
* 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.
|
|
@@ -71,9 +72,27 @@ export default class S3Client {
|
|
|
71
72
|
/**
|
|
72
73
|
* Creates an S3File instance for the given path.
|
|
73
74
|
*
|
|
74
|
-
* @param {string} path
|
|
75
|
+
* @param {string} path The path to the object in the bucket. ALso known as [object key](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html).
|
|
76
|
+
* We recommend not using the following characters in a key name because of significant special character handling, which isn't consistent across all applications (see [AWS docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html)):
|
|
77
|
+
* - Backslash (`\\`)
|
|
78
|
+
* - Left brace (`{`)
|
|
79
|
+
* - Non-printable ASCII characters (128–255 decimal characters)
|
|
80
|
+
* - Caret or circumflex (`^`)
|
|
81
|
+
* - Right brace (`}`)
|
|
82
|
+
* - Percent character (`%`)
|
|
83
|
+
* - Grave accent or backtick (`\``)
|
|
84
|
+
* - Right bracket (`]`)
|
|
85
|
+
* - Quotation mark (`"`)
|
|
86
|
+
* - Greater than sign (`>`)
|
|
87
|
+
* - Left bracket (`[`)
|
|
88
|
+
* - Tilde (`~`)
|
|
89
|
+
* - Less than sign (`<`)
|
|
90
|
+
* - Pound sign (`#`)
|
|
91
|
+
* - Vertical bar or pipe (`|`)
|
|
92
|
+
*
|
|
93
|
+
* lean-s3 does not enforce these restrictions.
|
|
94
|
+
*
|
|
75
95
|
* @param {Partial<CreateFileInstanceOptions>} [options] TODO
|
|
76
|
-
* @returns {S3File}
|
|
77
96
|
* @example
|
|
78
97
|
* ```js
|
|
79
98
|
* const file = client.file("image.jpg");
|
|
@@ -86,6 +105,7 @@ export default class S3Client {
|
|
|
86
105
|
* ```
|
|
87
106
|
*/
|
|
88
107
|
file(path, options) {
|
|
108
|
+
// TODO: Check max path length in bytes
|
|
89
109
|
return new S3File(this, path, undefined, undefined, undefined);
|
|
90
110
|
}
|
|
91
111
|
/**
|
|
@@ -117,12 +137,115 @@ export default class S3Client {
|
|
|
117
137
|
res.search = `${query}&X-Amz-Signature=${signature}`;
|
|
118
138
|
return res.toString();
|
|
119
139
|
}
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
|
|
142
|
+
*/
|
|
143
|
+
async deleteObjects(objects, options = {}) {
|
|
144
|
+
const body = xmlBuilder.build({
|
|
145
|
+
Delete: {
|
|
146
|
+
Quiet: true,
|
|
147
|
+
Object: objects.map(o => ({
|
|
148
|
+
Key: typeof o === "string" ? o : o.key,
|
|
149
|
+
})),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const response = await this.#signedRequest("POST", "", "delete=", // "=" is needed by minio for some reason
|
|
153
|
+
body, {
|
|
154
|
+
"content-md5": sign.md5Base64(body),
|
|
155
|
+
}, undefined, undefined, this.#options.bucket, options.signal);
|
|
156
|
+
if (response.statusCode === 200) {
|
|
157
|
+
const text = await response.body.text();
|
|
158
|
+
let res = undefined;
|
|
159
|
+
try {
|
|
160
|
+
// Quite mode omits all deleted elements, so it will be parsed as "", wich we need to coalasce to null/undefined
|
|
161
|
+
res = (xmlParser.parse(text)?.DeleteResult || undefined)?.Error ?? [];
|
|
162
|
+
}
|
|
163
|
+
catch (cause) {
|
|
164
|
+
// Possible according to AWS docs
|
|
165
|
+
throw new S3Error("Unknown", "", {
|
|
166
|
+
message: "S3 service responded with invalid XML.",
|
|
167
|
+
cause,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!res || !Array.isArray(res)) {
|
|
171
|
+
throw new S3Error("Unknown", "", {
|
|
172
|
+
message: "Could not process response.",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const errors = res.map(e => ({
|
|
176
|
+
code: e.Code,
|
|
177
|
+
key: e.Key,
|
|
178
|
+
message: e.Message,
|
|
179
|
+
versionId: e.VersionId,
|
|
180
|
+
}));
|
|
181
|
+
return errors.length > 0 ? { errors } : null;
|
|
182
|
+
}
|
|
183
|
+
if (400 <= response.statusCode && response.statusCode < 500) {
|
|
184
|
+
throw await getResponseError(response, "");
|
|
185
|
+
}
|
|
186
|
+
response.body.dump(); // undici docs state that we should dump the body if not used
|
|
187
|
+
throw new Error(`Response code not implemented yet: ${response.statusCode}`);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Creates a new bucket on the S3 server.
|
|
191
|
+
*
|
|
192
|
+
* @param name The name of the bucket to create. AWS the name according to [some rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html). The most important ones are:
|
|
193
|
+
* - Bucket names must be between `3` (min) and `63` (max) characters long.
|
|
194
|
+
* - Bucket names can consist only of lowercase letters, numbers, periods (`.`), and hyphens (`-`).
|
|
195
|
+
* - Bucket names must begin and end with a letter or number.
|
|
196
|
+
* - Bucket names must not contain two adjacent periods.
|
|
197
|
+
* - Bucket names must not be formatted as an IP address (for example, `192.168.5.4`).
|
|
198
|
+
*
|
|
199
|
+
* @throws {Error} If the bucket name is invalid.
|
|
200
|
+
* @remarks Uses [`CreateBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html)
|
|
201
|
+
*/
|
|
202
|
+
async createBucket(name, options) {
|
|
203
|
+
if (name.length < 3 || name.length > 63) {
|
|
204
|
+
throw new Error("`name` must be between 3 and 63 characters long.");
|
|
205
|
+
}
|
|
206
|
+
let body = undefined;
|
|
207
|
+
if (options) {
|
|
208
|
+
const location = options.location && (options.location.name || options.location.type)
|
|
209
|
+
? {
|
|
210
|
+
Name: options.location.name ?? undefined,
|
|
211
|
+
Type: options.location.type ?? undefined,
|
|
212
|
+
}
|
|
213
|
+
: undefined;
|
|
214
|
+
const bucket = options.info && (options.info.dataRedundancy || options.info.type)
|
|
215
|
+
? {
|
|
216
|
+
DataRedundancy: options.info.dataRedundancy ?? undefined,
|
|
217
|
+
Type: options.info.type ?? undefined,
|
|
218
|
+
}
|
|
219
|
+
: undefined;
|
|
220
|
+
body =
|
|
221
|
+
location || bucket || options.locationConstraint
|
|
222
|
+
? xmlBuilder.build({
|
|
223
|
+
CreateBucketConfiguration: {
|
|
224
|
+
$xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
|
|
225
|
+
LocationConstraint: options.locationConstraint ?? undefined,
|
|
226
|
+
Location: location,
|
|
227
|
+
Bucket: bucket,
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
: undefined;
|
|
231
|
+
}
|
|
232
|
+
const additionalSignedHeaders = body
|
|
233
|
+
? { "content-md5": sign.md5Base64(body) }
|
|
234
|
+
: undefined;
|
|
235
|
+
const response = await this.#signedRequest("PUT", "", undefined, body, additionalSignedHeaders, undefined, undefined, name, options?.signal);
|
|
236
|
+
if (response.statusCode === 200) {
|
|
237
|
+
response.body.dump(); // undici docs state that we should dump the body if not used
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (400 <= response.statusCode && response.statusCode < 500) {
|
|
241
|
+
throw await getResponseError(response, "");
|
|
242
|
+
}
|
|
243
|
+
response.body.dump(); // undici docs state that we should dump the body if not used
|
|
244
|
+
throw new Error(`Response code not supported: ${response.statusCode}`);
|
|
122
245
|
}
|
|
123
246
|
//#region list
|
|
124
247
|
/**
|
|
125
|
-
* Uses `ListObjectsV2` to iterate over all keys. Pagination and continuation is handled internally.
|
|
248
|
+
* Uses [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys. Pagination and continuation is handled internally.
|
|
126
249
|
*/
|
|
127
250
|
async *listIterating(options) {
|
|
128
251
|
// only used to get smaller pages, so we can test this properly
|
|
@@ -142,6 +265,9 @@ export default class S3Client {
|
|
|
142
265
|
continuationToken = res.nextContinuationToken;
|
|
143
266
|
} while (continuationToken);
|
|
144
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Implements [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys.
|
|
270
|
+
*/
|
|
145
271
|
async list(options = {}) {
|
|
146
272
|
// See `benchmark-operations.js` on why we don't use URLSearchParams but string concat
|
|
147
273
|
// tldr: This is faster and we know the params exactly, so we can focus our encoding
|
|
@@ -174,7 +300,7 @@ export default class S3Client {
|
|
|
174
300
|
}
|
|
175
301
|
query += `&start-after=${encodeURIComponent(options.startAfter)}`;
|
|
176
302
|
}
|
|
177
|
-
const response = await this.#signedRequest("GET", "", query, undefined, undefined, undefined, undefined, options.signal);
|
|
303
|
+
const response = await this.#signedRequest("GET", "", query, undefined, undefined, undefined, undefined, this.#options.bucket, options.signal);
|
|
178
304
|
if (response.statusCode === 200) {
|
|
179
305
|
const text = await response.body.text();
|
|
180
306
|
let res = undefined;
|
|
@@ -211,13 +337,11 @@ export default class S3Client {
|
|
|
211
337
|
contents,
|
|
212
338
|
};
|
|
213
339
|
}
|
|
214
|
-
// undici docs state that we
|
|
215
|
-
response.body.dump();
|
|
340
|
+
response.body.dump(); // undici docs state that we should dump the body if not used
|
|
216
341
|
throw new Error(`Response code not implemented yet: ${response.statusCode}`);
|
|
217
342
|
}
|
|
218
343
|
//#endregion
|
|
219
|
-
async #signedRequest(method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, signal = undefined) {
|
|
220
|
-
const bucket = this.#options.bucket;
|
|
344
|
+
async #signedRequest(method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, bucket, signal = undefined) {
|
|
221
345
|
const endpoint = this.#options.endpoint;
|
|
222
346
|
const region = this.#options.region;
|
|
223
347
|
const url = buildRequestUrl(endpoint, bucket, region, pathWithoutBucket);
|
|
@@ -304,34 +428,7 @@ export default class S3Client {
|
|
|
304
428
|
// everything seemed to work, no need to process response body
|
|
305
429
|
return;
|
|
306
430
|
}
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
body = await response.body.text();
|
|
310
|
-
}
|
|
311
|
-
catch (cause) {
|
|
312
|
-
throw new S3Error("Unknown", path, {
|
|
313
|
-
message: "Could not read response body.",
|
|
314
|
-
cause,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
if (response.headers["content-type"] === "application/xml") {
|
|
318
|
-
let error = undefined;
|
|
319
|
-
try {
|
|
320
|
-
error = xmlParser.parse(body);
|
|
321
|
-
}
|
|
322
|
-
catch (cause) {
|
|
323
|
-
throw new S3Error("Unknown", path, {
|
|
324
|
-
message: "Could not parse XML error response.",
|
|
325
|
-
cause,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
throw new S3Error(error.Code || "Unknown", path, {
|
|
329
|
-
message: error.Message || undefined, // Message might be "",
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
throw new S3Error("Unknown", path, {
|
|
333
|
-
message: "Unknown error during S3 request.",
|
|
334
|
-
});
|
|
431
|
+
throw await getResponseError(response, path);
|
|
335
432
|
}
|
|
336
433
|
// TODO: Support abortSignal
|
|
337
434
|
/**
|
|
@@ -445,17 +542,6 @@ export default class S3Client {
|
|
|
445
542
|
return `AWS4-HMAC-SHA256 Credential=${credentialSpec}, SignedHeaders=${signedHeadersSpec}, Signature=${signature}`;
|
|
446
543
|
}
|
|
447
544
|
}
|
|
448
|
-
/**
|
|
449
|
-
* @param {string} amzCredential
|
|
450
|
-
* @param {import("./AmzDate.ts").AmzDate} date
|
|
451
|
-
* @param {number} expiresIn
|
|
452
|
-
* @param {string} headerList
|
|
453
|
-
* @param {StorageClass | null | undefined} storageClass
|
|
454
|
-
* @param {string | null | undefined} sessionToken
|
|
455
|
-
* @param {Acl | null | undefined} acl
|
|
456
|
-
* @param {string | null | undefined} contentHashStr
|
|
457
|
-
* @returns {string}
|
|
458
|
-
*/
|
|
459
545
|
export function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHashStr, storageClass, sessionToken, acl) {
|
|
460
546
|
// We tried to make these query params entirely lower-cased, just like the headers
|
|
461
547
|
// but Cloudflare R2 requires them to have this exact casing
|
|
@@ -483,3 +569,42 @@ export function buildSearchParams(amzCredential, date, expiresIn, headerList, co
|
|
|
483
569
|
}
|
|
484
570
|
return res;
|
|
485
571
|
}
|
|
572
|
+
async function getResponseError(response, path) {
|
|
573
|
+
let body = undefined;
|
|
574
|
+
try {
|
|
575
|
+
body = await response.body.text();
|
|
576
|
+
}
|
|
577
|
+
catch (cause) {
|
|
578
|
+
return new S3Error("Unknown", path, {
|
|
579
|
+
message: "Could not read response body.",
|
|
580
|
+
cause,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (response.headers["content-type"] === "application/xml") {
|
|
584
|
+
return parseAndGetXmlError(body, path);
|
|
585
|
+
}
|
|
586
|
+
return new S3Error("Unknown", path, {
|
|
587
|
+
message: "Unknown error during S3 request.",
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
function parseAndGetXmlError(body, path) {
|
|
591
|
+
let error = undefined;
|
|
592
|
+
try {
|
|
593
|
+
error = xmlParser.parse(body);
|
|
594
|
+
}
|
|
595
|
+
catch (cause) {
|
|
596
|
+
return new S3Error("Unknown", path, {
|
|
597
|
+
message: "Could not parse XML error response.",
|
|
598
|
+
cause,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
if (error.Error) {
|
|
602
|
+
const e = error.Error;
|
|
603
|
+
return new S3Error(e.Code || "Unknown", path, {
|
|
604
|
+
message: e.Message || undefined, // Message might be "",
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return new S3Error(error.Code || "Unknown", path, {
|
|
608
|
+
message: error.Message || undefined, // Message might be "",
|
|
609
|
+
});
|
|
610
|
+
}
|
package/dist/S3File.d.ts
CHANGED
|
@@ -12,21 +12,22 @@ export default class S3File {
|
|
|
12
12
|
/**
|
|
13
13
|
* Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
|
|
14
14
|
*
|
|
15
|
+
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
15
16
|
* @throws {Error} If the file does not exist.
|
|
16
|
-
* @param {Partial<S3StatOptions>} [options]
|
|
17
|
-
* @returns {Promise<S3Stat>}
|
|
18
17
|
*/
|
|
19
18
|
stat({ signal }?: Partial<S3StatOptions>): Promise<S3Stat>;
|
|
20
19
|
/**
|
|
21
20
|
* Check if a file exists in the bucket. Uses `HEAD` request to check existence.
|
|
22
|
-
*
|
|
23
|
-
* @
|
|
21
|
+
*
|
|
22
|
+
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
24
23
|
*/
|
|
25
24
|
exists({ signal, }?: Partial<S3FileExistsOptions>): Promise<boolean>;
|
|
26
25
|
/**
|
|
27
26
|
* Delete a file from the bucket.
|
|
27
|
+
*
|
|
28
|
+
* @remarks Uses [`DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).
|
|
29
|
+
*
|
|
28
30
|
* @param {Partial<S3FileDeleteOptions>} [options]
|
|
29
|
-
* @returns {Promise<void>}
|
|
30
31
|
*
|
|
31
32
|
* @example
|
|
32
33
|
* ```js
|
package/dist/S3File.js
CHANGED
|
@@ -40,9 +40,8 @@ export default class S3File {
|
|
|
40
40
|
/**
|
|
41
41
|
* Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
|
|
42
42
|
*
|
|
43
|
+
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
43
44
|
* @throws {Error} If the file does not exist.
|
|
44
|
-
* @param {Partial<S3StatOptions>} [options]
|
|
45
|
-
* @returns {Promise<S3Stat>}
|
|
46
45
|
*/
|
|
47
46
|
async stat({ signal } = {}) {
|
|
48
47
|
// TODO: Support all options
|
|
@@ -67,8 +66,8 @@ export default class S3File {
|
|
|
67
66
|
}
|
|
68
67
|
/**
|
|
69
68
|
* Check if a file exists in the bucket. Uses `HEAD` request to check existence.
|
|
70
|
-
*
|
|
71
|
-
* @
|
|
69
|
+
*
|
|
70
|
+
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
72
71
|
*/
|
|
73
72
|
async exists({ signal, } = {}) {
|
|
74
73
|
// TODO: Support all options
|
|
@@ -79,8 +78,10 @@ export default class S3File {
|
|
|
79
78
|
}
|
|
80
79
|
/**
|
|
81
80
|
* Delete a file from the bucket.
|
|
81
|
+
*
|
|
82
|
+
* @remarks Uses [`DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).
|
|
83
|
+
*
|
|
82
84
|
* @param {Partial<S3FileDeleteOptions>} [options]
|
|
83
|
-
* @returns {Promise<void>}
|
|
84
85
|
*
|
|
85
86
|
* @example
|
|
86
87
|
* ```js
|
|
@@ -143,14 +144,6 @@ export default class S3File {
|
|
|
143
144
|
// This function is called for every operation on the blob
|
|
144
145
|
return this.#client[stream](this.#path, undefined, this.#start, this.#end);
|
|
145
146
|
}
|
|
146
|
-
/**
|
|
147
|
-
* @param {ByteSource} data
|
|
148
|
-
* @returns {Promise<[
|
|
149
|
-
* buffer: import("./index.d.ts").UndiciBodyInit,
|
|
150
|
-
* size: number | undefined,
|
|
151
|
-
* hash: Buffer | undefined,
|
|
152
|
-
* ]>}
|
|
153
|
-
*/
|
|
154
147
|
async #transformData(data) {
|
|
155
148
|
if (typeof data === "string") {
|
|
156
149
|
const binary = new TextEncoder();
|
|
@@ -197,10 +190,6 @@ export default class S3File {
|
|
|
197
190
|
return await this.#client[write](this.#path, bytes, this.#contentType, length, hash, this.#start, this.#end, signal);
|
|
198
191
|
}
|
|
199
192
|
}
|
|
200
|
-
/**
|
|
201
|
-
* @param {never} v
|
|
202
|
-
* @returns {never}
|
|
203
|
-
*/
|
|
204
193
|
function assertNever(v) {
|
|
205
194
|
throw new TypeError(`Expected value not to have type ${typeof v}`);
|
|
206
195
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
2
|
export { default as S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3StatOptions, } from "./S3File.ts";
|
|
3
|
-
export { default as S3Client, type ListObjectsResponse, type CreateFileInstanceOptions, type OverridableS3ClientOptions, type S3ClientOptions, type S3FilePresignOptions, } from "./S3Client.ts";
|
|
3
|
+
export { default as S3Client, type ListObjectsOptions, type ListObjectsIteratingOptions, type ListObjectsResponse, type CreateFileInstanceOptions, type OverridableS3ClientOptions, type S3ClientOptions, type S3FilePresignOptions, type BucketCreationOptions, type DeleteObjectsOptions, } from "./S3Client.ts";
|
|
4
4
|
export { default as S3Error, type S3ErrorOptions } from "./S3Error.ts";
|
|
5
5
|
export { default as S3Stat } from "./S3Stat.ts";
|
|
6
6
|
export { default as S3BucketEntry } from "./S3BucketEntry.ts";
|
|
@@ -13,3 +13,17 @@ export type HttpMethod = PresignableHttpMethod | "POST";
|
|
|
13
13
|
/** Body values supported by undici. */
|
|
14
14
|
export type UndiciBodyInit = string | Buffer | Uint8Array | Readable;
|
|
15
15
|
export type ByteSource = UndiciBodyInit | Blob;
|
|
16
|
+
/**
|
|
17
|
+
* Implements [LocationInfo](https://docs.aws.amazon.com/AmazonS3/latest/API/API_LocationInfo.html)
|
|
18
|
+
*/
|
|
19
|
+
export type BucketLocationInfo = {
|
|
20
|
+
name?: string;
|
|
21
|
+
type?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Implements [BucketInfo](https://docs.aws.amazon.com/AmazonS3/latest/API/API_BucketInfo.html)
|
|
25
|
+
*/
|
|
26
|
+
export type BucketInfo = {
|
|
27
|
+
dataRedundancy?: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,3 @@ export { default as S3Client, } from "./S3Client.js";
|
|
|
3
3
|
export { default as S3Error } from "./S3Error.js";
|
|
4
4
|
export { default as S3Stat } from "./S3Stat.js";
|
|
5
5
|
export { default as S3BucketEntry } from "./S3BucketEntry.js";
|
|
6
|
-
// TODO
|
|
7
|
-
// | ArrayBufferView
|
|
8
|
-
// | ArrayBuffer
|
|
9
|
-
// | SharedArrayBuffer
|
|
10
|
-
// | Request
|
|
11
|
-
// | Response
|
|
12
|
-
// | S3File
|
|
13
|
-
// | ReadableStream<Uint8Array>
|
package/dist/sign.d.ts
CHANGED
|
@@ -13,4 +13,4 @@ export declare const unsignedPayload = "UNSIGNED-PAYLOAD";
|
|
|
13
13
|
export declare function createCanonicalDataDigestHostOnly(method: PresignableHttpMethod, path: string, query: string, host: string): string;
|
|
14
14
|
export declare function createCanonicalDataDigest(method: HttpMethod, path: string, query: string, sortedHeaders: Record<string, string>, contentHashStr: string): string;
|
|
15
15
|
export declare function sha256(data: BinaryLike): Buffer;
|
|
16
|
-
export declare function
|
|
16
|
+
export declare function md5Base64(data: BinaryLike): string;
|
package/dist/sign.js
CHANGED
|
@@ -72,6 +72,6 @@ export function createCanonicalDataDigest(method, path, query, sortedHeaders, co
|
|
|
72
72
|
export function sha256(data) {
|
|
73
73
|
return createHash("sha256").update(data).digest();
|
|
74
74
|
}
|
|
75
|
-
export function
|
|
76
|
-
return createHash("md5").update(data).digest("
|
|
75
|
+
export function md5Base64(data) {
|
|
76
|
+
return createHash("md5").update(data).digest("base64");
|
|
77
77
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "lean-s3",
|
|
3
3
|
"author": "Niklas Mollenhauer",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.1",
|
|
6
6
|
"description": "A server-side S3 API for the regular user.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"s3",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"prepublishOnly": "npm run clean && npm run build"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@aws-sdk/client-s3": "^3.828.0",
|
|
34
33
|
"@biomejs/biome": "^1.9.4",
|
|
35
34
|
"@testcontainers/localstack": "^11.0.3",
|
|
36
35
|
"@testcontainers/minio": "^11.0.3",
|