@nxgt/s3 0.1.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 +75 -6
- package/dist/bucket/bind-bucket.d.ts +10 -5
- package/dist/bucket/bind-bucket.d.ts.map +1 -1
- package/dist/bucket/operations/presign.d.ts.map +1 -1
- package/dist/bucket/operations/writes.d.ts +2 -4
- package/dist/bucket/operations/writes.d.ts.map +1 -1
- package/dist/bucket/types.d.ts +22 -1
- package/dist/bucket/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -6
- package/dist/index.js.map +5 -5
- package/docs/README.md +16 -0
- package/docs/guide/buckets.md +170 -0
- package/docs/guide/presigned-urls.md +153 -0
- package/docs/guide/reads.md +186 -0
- package/docs/guide/writes.md +260 -0
- package/docs/roadmap.md +58 -0
- package/docs/troubleshooting.md +276 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -34,12 +34,15 @@ everything this package does not wrap is still there.
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
bun add @nxgt/s3 typescript
|
|
37
|
+
bun add @nxgt/s3 typescript @types/bun
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
- **Bun 1.4 or later, and Bun only.** `S3Client` is built into Bun, which is
|
|
41
41
|
why there is no SDK to install — and why this package does not run on Node.
|
|
42
42
|
- `typescript` `^6.0.3`: required peer, the version every `@nxgt` package pins.
|
|
43
|
+
- `@types/bun`: required to typecheck. The shipped declarations name Bun's own
|
|
44
|
+
`S3Client`, `S3File` and `S3Options`, so without Bun's types the first `tsc`
|
|
45
|
+
fails with `Cannot find module 'bun'`.
|
|
43
46
|
- Tested against SeaweedFS 4.47's S3 gateway. Anything S3-compatible that Bun
|
|
44
47
|
can sign for will do; a service that is not AWS wants
|
|
45
48
|
`virtualHostedStyle: false`.
|
|
@@ -53,7 +56,8 @@ bun add @nxgt/s3 typescript
|
|
|
53
56
|
- **No copy or move.** `store.client` is where those live when Bun grows them.
|
|
54
57
|
- **It adds no retry and no cache of its own.** A failed request comes back
|
|
55
58
|
with S3's own error. Bun's client does retry — `retry`, three attempts by
|
|
56
|
-
default — and that option is passed through like the rest
|
|
59
|
+
default — and that option is passed through to `bindBucket` like the rest;
|
|
60
|
+
a `put` does not take it.
|
|
57
61
|
|
|
58
62
|
## API
|
|
59
63
|
|
|
@@ -84,7 +88,7 @@ stateless HTTP, so there is no connection to share and nothing to close.
|
|
|
84
88
|
| `client` | the `S3Client` this holds |
|
|
85
89
|
| `keyFor(params)` | the key it would use |
|
|
86
90
|
| `file(params)` | Bun's lazy `S3File`: `.stream()`, `.slice()`, `.writer()` |
|
|
87
|
-
| `put(params, body, options?)` | writes it, once the content type and size have accepted it.
|
|
91
|
+
| `put(params, body, options?)` | writes it, once the content type and size have accepted it. See `PutOptions` below |
|
|
88
92
|
| `bytes(params)` | the bytes, or `undefined` when there is no such object |
|
|
89
93
|
| `text(params)` | the body as text, or `undefined` |
|
|
90
94
|
| `exists(params)` | |
|
|
@@ -94,6 +98,33 @@ stateless HTTP, so there is no connection to share and nothing to close.
|
|
|
94
98
|
| `presignGet(params, { expiresIn, acl })` | a signed URL that reads it |
|
|
95
99
|
| `presignPut(params, { expiresIn, acl })` | a signed URL that writes it. It takes no `type` — see the Traps |
|
|
96
100
|
|
|
101
|
+
### What a write may say
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
await store.put({ userId: 'u1' }, Bun.file('ada.png')); // the type comes from the file
|
|
105
|
+
await store.put({ userId: 'u1' }, png, {
|
|
106
|
+
type: 'image/png',
|
|
107
|
+
contentDisposition: 'attachment; filename="ada.png"',
|
|
108
|
+
storageClass: 'STANDARD_IA',
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| `PutOptions` | |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `type` | the body's content type. A `Blob` — `Bun.file` included — is asked for its own when none is given, and *that* is the type the guard checks and the service receives |
|
|
115
|
+
| `contentDisposition` | how a reader should present it: measured, it comes back on a `GET` |
|
|
116
|
+
| `contentEncoding` | how the body is encoded, likewise returned on a read |
|
|
117
|
+
| `acl` | who may read it, where the service implements ACLs |
|
|
118
|
+
| `storageClass` | what it costs to keep, returned as `x-amz-storage-class` |
|
|
119
|
+
|
|
120
|
+
These are Bun's own names, picked out of its `S3Options`, so a Bun release
|
|
121
|
+
that changes one is a compile error here rather than a silent drift. Every
|
|
122
|
+
one of them describes the **object**; a write says nothing about where it
|
|
123
|
+
goes, or how it gets there — see the Traps.
|
|
124
|
+
|
|
125
|
+
Given both here and to `bindBucket`, the one on the write wins: it is the
|
|
126
|
+
last thing handed to the client.
|
|
127
|
+
|
|
97
128
|
### Listing
|
|
98
129
|
|
|
99
130
|
```ts
|
|
@@ -121,6 +152,7 @@ it carries.
|
|
|
121
152
|
| `PresignOptions` | `{ expiresIn?: number; acl?: … }` |
|
|
122
153
|
| `ParamsOf<D>` | what a definition's `key` takes, for a caller writing its own helper |
|
|
123
154
|
| `PutBody` | everything Bun's `write` takes |
|
|
155
|
+
| `PutOptions` | what one write may say about the object — see above |
|
|
124
156
|
|
|
125
157
|
`stat` gives back Bun's own `S3Stats`, which this package does not re-export;
|
|
126
158
|
import it from `bun` where you need to name it.
|
|
@@ -156,6 +188,10 @@ Each is a `@ts-expect-error` case in `test/types/s3.ts`.
|
|
|
156
188
|
|
|
157
189
|
- An object read, written, deleted or signed for with the wrong key
|
|
158
190
|
parameters, and a `put` given a misspelt option.
|
|
191
|
+
- A `put` or a presigned URL naming a `bucket`, an `endpoint`, a `region` or
|
|
192
|
+
a credential: a write says what the object is, never where it goes.
|
|
193
|
+
- A `put` given `partSize`, `queueSize` or `retry`: those are the client's,
|
|
194
|
+
and a `put` is one PUT.
|
|
159
195
|
- A `presignPut` given a `type`: a presigned PUT constrains no content type,
|
|
160
196
|
so it takes none.
|
|
161
197
|
- A definition with no `bucket`, no `key`, a `key` that gives something other
|
|
@@ -188,9 +224,33 @@ Each is a `@ts-expect-error` case in `test/types/s3.ts`.
|
|
|
188
224
|
memory first, or leave `maxSize` out and let the service refuse it.
|
|
189
225
|
- **A content type is compared on its essence.** `text/csv` accepts
|
|
190
226
|
`text/csv;charset=utf-8` and `TEXT/CSV`, because that is what real bodies
|
|
191
|
-
carry
|
|
192
|
-
|
|
193
|
-
|
|
227
|
+
carry. Measured on bun 1.4.2, `Bun.file` puts a charset on some types and
|
|
228
|
+
not others — `.txt` is `text/plain;charset=utf-8` and `.json` is
|
|
229
|
+
`application/json;charset=utf-8`, while `.csv` is the bare `text/csv` — so
|
|
230
|
+
a definition that named either form would refuse half the files it exists
|
|
231
|
+
for. Parameters and case are ignored; nothing else is.
|
|
232
|
+
- **A write names the object, never where it goes.** `PutOptions` and
|
|
233
|
+
`PresignOptions` carry no `bucket`, `endpoint`, `region` or credential, and
|
|
234
|
+
the run time forwards only the keys they list. The types are not enough on
|
|
235
|
+
their own: a bag that arrives in a request body never met them. Measured,
|
|
236
|
+
spreading it straight through let a `bucket` key store the object in
|
|
237
|
+
**another bucket** and report success, and sign a URL for that bucket —
|
|
238
|
+
a credential key signed one against another endpoint entirely.
|
|
239
|
+
- **A `put` is one PUT, so it takes no upload tuning.** `partSize`,
|
|
240
|
+
`queueSize` and `retry` belong to `bindBucket`, where they are the client's
|
|
241
|
+
own. Measured on bun 1.4.2 against a 12 MiB body: a `put` with `partSize`
|
|
242
|
+
set and one without come back with the **same** ETag, and neither carries
|
|
243
|
+
the `-<parts>` suffix a multipart upload leaves. Use `file(params).writer()`
|
|
244
|
+
for a body that wants parts.
|
|
245
|
+
- **An option's *value* is Bun's to check, and it throws Bun's error.** The
|
|
246
|
+
guards refuse a content type and a size before anything is sent, and raise
|
|
247
|
+
`S3Error`. An `acl` or a `storageClass` outside what Bun accepts raises
|
|
248
|
+
Bun's own `TypeError` instead — measured:
|
|
249
|
+
`storageClass: 'NOPE'` gives `TypeError: storageClass must be one of …`, and
|
|
250
|
+
`instanceof S3Error` is false. `contentDisposition` and `contentEncoding`
|
|
251
|
+
are plain strings to Bun and accept anything. Nothing is sent either way; it
|
|
252
|
+
is the class a handler catches that differs, so validate a bag from a
|
|
253
|
+
request body before passing it on.
|
|
194
254
|
- **A string body is measured in bytes, not in characters**, and `maxSize` is
|
|
195
255
|
inclusive: 1024 passes, 1025 does not.
|
|
196
256
|
- **`expiresIn` is seconds, and Bun's default is a day.** Always pass one.
|
|
@@ -208,6 +268,15 @@ Each is a `@ts-expect-error` case in `test/types/s3.ts`.
|
|
|
208
268
|
`undefined`; every other failure — a wrong secret, a refused request, a
|
|
209
269
|
service that is down — comes back as the error it is.
|
|
210
270
|
|
|
271
|
+
## Documentation
|
|
272
|
+
|
|
273
|
+
- [docs/README.md](docs/README.md) — the guide index: buckets, reading,
|
|
274
|
+
writing and presigned URLs, each with its options and a worked example.
|
|
275
|
+
- [docs/troubleshooting.md](docs/troubleshooting.md) — every error this
|
|
276
|
+
package can raise, by the message you will see.
|
|
277
|
+
- [docs/roadmap.md](docs/roadmap.md) — what is coming, and what has been
|
|
278
|
+
ruled out.
|
|
279
|
+
|
|
211
280
|
## License
|
|
212
281
|
|
|
213
282
|
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { S3Client, type S3File, type S3Options, type S3Stats } from 'bun';
|
|
2
2
|
import { type PresignOptions } from './operations/presign';
|
|
3
|
-
import type { BucketDefinition, ObjectPage, PutBody } from './types';
|
|
3
|
+
import type { BucketDefinition, ObjectPage, PutBody, PutOptions } from './types';
|
|
4
4
|
export type { PresignOptions };
|
|
5
5
|
/** A bucket bound to credentials: the definition, with somewhere to put it. */
|
|
6
6
|
export interface BoundBucket<P> {
|
|
@@ -10,10 +10,15 @@ export interface BoundBucket<P> {
|
|
|
10
10
|
keyFor(params: P): string;
|
|
11
11
|
/** Bun's own lazy handle: `.stream()`, `.slice()`, `.writer()` and the rest. */
|
|
12
12
|
file(params: P): S3File;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Writes it, once the bucket's content type and size have accepted it.
|
|
15
|
+
*
|
|
16
|
+
* Beyond `type`, an option here describes **this object** — how it is
|
|
17
|
+
* served back (`contentDisposition`, `contentEncoding`), who may read it
|
|
18
|
+
* (`acl`), what it costs to keep (`storageClass`) — or how a large body is
|
|
19
|
+
* uploaded (`partSize`, `queueSize`, `retry`).
|
|
20
|
+
*/
|
|
21
|
+
put(params: P, body: PutBody, options?: PutOptions): Promise<void>;
|
|
17
22
|
/** The bytes, or `undefined` when there is no such object. */
|
|
18
23
|
bytes(params: P): Promise<Uint8Array | undefined>;
|
|
19
24
|
/** The body as text, or `undefined` when there is no such object. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bind-bucket.d.ts","sourceRoot":"","sources":["../../src/bucket/bind-bucket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,KAAK,CAAC;AAG1E,OAAO,EACN,KAAK,cAAc,EAGnB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"bind-bucket.d.ts","sourceRoot":"","sources":["../../src/bucket/bind-bucket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,KAAK,CAAC;AAG1E,OAAO,EACN,KAAK,cAAc,EAGnB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,KAAK,EACX,gBAAgB,EAChB,UAAU,EACV,OAAO,EACP,UAAU,EACV,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B,+EAA+E;AAC/E,MAAM,WAAW,WAAW,CAAC,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,yEAAyE;IACzE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC;IAC1B,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,8DAA8D;IAC9D,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAClD,qEAAqE;IACrE,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,4EAA4E;IAC5E,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9C,iFAAiF;IACjF,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,iEAAiE;IACjE,IAAI,CAAC,OAAO,CAAC,EAAE;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACxB,4CAA4C;IAC5C,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC;IACxD;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC;CACxD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAC3B,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,OAAO,GAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAM,GACrC,WAAW,CAAC,CAAC,CAAC,CAoBhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presign.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/presign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAEvD,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC9B,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"presign.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/presign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAEvD,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC9B,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;CACvB;AA4BD,wBAAgB,aAAa,CAAC,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE,cAAc,GACtB,MAAM,CAKR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE,cAAc,GACtB,MAAM,CAKR"}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { type BucketContext } from '../context';
|
|
2
|
-
import type { PutBody } from '../types';
|
|
2
|
+
import type { PutBody, PutOptions } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Writes it, once the bucket's content type and size have accepted it. Both
|
|
5
5
|
* guards run before `write` is called, so a refused body is never sent.
|
|
6
6
|
*/
|
|
7
|
-
export declare function putObject<P>(context: BucketContext<P>, params: P, body: PutBody, options?:
|
|
8
|
-
type?: string;
|
|
9
|
-
}): Promise<void>;
|
|
7
|
+
export declare function putObject<P>(context: BucketContext<P>, params: P, body: PutBody, options?: PutOptions): Promise<void>;
|
|
10
8
|
/** Removes it. S3 does not say whether anything was there, and nor does this. */
|
|
11
9
|
export declare function deleteObject<P>(context: BucketContext<P>, params: P): Promise<void>;
|
|
12
10
|
//# sourceMappingURL=writes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writes.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/writes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAEvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"writes.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/writes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAEvD,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEpD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAChC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,UAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CAYf;AAyCD,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,CAAC,EAC7B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,IAAI,CAAC,CAEf"}
|
package/dist/bucket/types.d.ts
CHANGED
|
@@ -42,11 +42,32 @@ export interface ObjectPage {
|
|
|
42
42
|
}
|
|
43
43
|
/** What a definition's `key` takes, so a caller can write its own helper. */
|
|
44
44
|
export type ParamsOf<D> = D extends BucketDefinition<infer P> ? P : never;
|
|
45
|
-
import type { S3Client } from 'bun';
|
|
45
|
+
import type { S3Client, S3Options } from 'bun';
|
|
46
46
|
/**
|
|
47
47
|
* Everything Bun's own `write` takes — a string, bytes, a `Blob`, a stream, a
|
|
48
48
|
* `Response`. A bucket with a `maxSize` refuses the ones whose size cannot be
|
|
49
49
|
* known before sending; one without accepts them all.
|
|
50
50
|
*/
|
|
51
51
|
export type PutBody = Parameters<S3Client['write']>[1];
|
|
52
|
+
/**
|
|
53
|
+
* What a single write may say about **the object it stores**.
|
|
54
|
+
*
|
|
55
|
+
* Picked out of Bun's own `S3Options` rather than written again here, so a
|
|
56
|
+
* Bun release that changes one of these is a compile error instead of a
|
|
57
|
+
* silent drift.
|
|
58
|
+
*
|
|
59
|
+
* Only what describes the object is here. The credentials, the endpoint, the
|
|
60
|
+
* bucket and the region belong to the bound bucket — naming them per write
|
|
61
|
+
* would let one call store the object somewhere the definition never
|
|
62
|
+
* described. `partSize`, `queueSize` and `retry` are not here either:
|
|
63
|
+
* measured on bun 1.4.2 against a 12 MiB body, a `put` with `partSize` set
|
|
64
|
+
* and one without produce the **same** ETag, with no `-<parts>` suffix — so
|
|
65
|
+
* a single PUT either way, and offering them would promise a multipart
|
|
66
|
+
* upload this package does not do. `bindBucket` still takes them, where they
|
|
67
|
+
* are the client's own.
|
|
68
|
+
*
|
|
69
|
+
* `type` is the one the guards read — see `effectiveType`. The rest reach
|
|
70
|
+
* the service untouched.
|
|
71
|
+
*/
|
|
72
|
+
export type PutOptions = Pick<S3Options, 'type' | 'acl' | 'storageClass' | 'contentDisposition' | 'contentEncoding'>;
|
|
52
73
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bucket/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAClC,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,YAAY,EAAE,IAAI,GAAG,SAAS,CAAC;IAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,qEAAqE;IACrE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE1E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bucket/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAClC,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,YAAY,EAAE,IAAI,GAAG,SAAS,CAAC;IAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,qEAAqE;IACrE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE1E,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAE/C;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,CAC5B,SAAS,EACT,MAAM,GAAG,KAAK,GAAG,cAAc,GAAG,oBAAoB,GAAG,iBAAiB,CAC1E,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { BoundBucket, PresignOptions } from './bucket/bind-bucket';
|
|
2
2
|
export { bindBucket } from './bucket/bind-bucket';
|
|
3
3
|
export { defineBucket } from './bucket/define-bucket';
|
|
4
|
-
export type { BucketDefinition, ObjectPage, ParamsOf, PutBody, StoredObject, } from './bucket/types';
|
|
4
|
+
export type { BucketDefinition, ObjectPage, ParamsOf, PutBody, PutOptions, StoredObject, } from './bucket/types';
|
|
5
5
|
export type { S3ErrorCode } from './errors/s3-error';
|
|
6
6
|
export { S3Error } from './errors/s3-error';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,YAAY,EACX,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,YAAY,GACZ,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,YAAY,EACX,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,UAAU,EACV,YAAY,GACZ,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -37,15 +37,24 @@ async function listObjects(context, options = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// src/bucket/operations/presign.ts
|
|
40
|
+
var SIGNED = ["expiresIn", "acl"];
|
|
41
|
+
function signed(options) {
|
|
42
|
+
const forwarded = {};
|
|
43
|
+
for (const key of SIGNED) {
|
|
44
|
+
if (options?.[key] !== undefined)
|
|
45
|
+
forwarded[key] = options[key];
|
|
46
|
+
}
|
|
47
|
+
return forwarded;
|
|
48
|
+
}
|
|
40
49
|
function presignGetUrl(context, params, options) {
|
|
41
50
|
return context.client.presign(keyOf(context, params), {
|
|
42
|
-
...options,
|
|
51
|
+
...signed(options),
|
|
43
52
|
method: "GET"
|
|
44
53
|
});
|
|
45
54
|
}
|
|
46
55
|
function presignPutUrl(context, params, options) {
|
|
47
56
|
return context.client.presign(keyOf(context, params), {
|
|
48
|
-
...options,
|
|
57
|
+
...signed(options),
|
|
49
58
|
method: "PUT"
|
|
50
59
|
});
|
|
51
60
|
}
|
|
@@ -128,12 +137,29 @@ function checkSize(context, key, body) {
|
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
// src/bucket/operations/writes.ts
|
|
131
|
-
async function putObject(context, params, body, options) {
|
|
140
|
+
async function putObject(context, params, body, options = {}) {
|
|
132
141
|
const key = keyOf(context, params);
|
|
133
|
-
const type = effectiveType(body, options
|
|
142
|
+
const type = effectiveType(body, options.type);
|
|
134
143
|
checkType(context, key, type);
|
|
135
144
|
checkSize(context, key, body);
|
|
136
|
-
await context.client.write(key, body,
|
|
145
|
+
await context.client.write(key, body, {
|
|
146
|
+
...passed(options),
|
|
147
|
+
...type ? { type } : {}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
var PASSED = [
|
|
151
|
+
"acl",
|
|
152
|
+
"storageClass",
|
|
153
|
+
"contentDisposition",
|
|
154
|
+
"contentEncoding"
|
|
155
|
+
];
|
|
156
|
+
function passed(options) {
|
|
157
|
+
const forwarded = {};
|
|
158
|
+
for (const key of PASSED) {
|
|
159
|
+
if (options[key] !== undefined)
|
|
160
|
+
forwarded[key] = options[key];
|
|
161
|
+
}
|
|
162
|
+
return forwarded;
|
|
137
163
|
}
|
|
138
164
|
function deleteObject(context, params) {
|
|
139
165
|
return context.client.delete(keyOf(context, params));
|
|
@@ -181,5 +207,5 @@ export {
|
|
|
181
207
|
defineBucket
|
|
182
208
|
};
|
|
183
209
|
|
|
184
|
-
//# debugId=
|
|
210
|
+
//# debugId=F9842873CB32A8B764756E2164756E21
|
|
185
211
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/bucket/bind-bucket.ts", "../src/bucket/context.ts", "../src/bucket/operations/list.ts", "../src/bucket/operations/presign.ts", "../src/bucket/operations/reads.ts", "../src/errors/s3-error.ts", "../src/bucket/guards.ts", "../src/bucket/operations/writes.ts", "../src/bucket/define-bucket.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { S3Client, type S3File, type S3Options, type S3Stats } from 'bun';\nimport { bucketContext, keyOf } from './context';\nimport { listObjects } from './operations/list';\nimport {\n\ttype PresignOptions,\n\tpresignGetUrl,\n\tpresignPutUrl,\n} from './operations/presign';\nimport {\n\tobjectExists,\n\treadBytes,\n\treadText,\n\tstatObject,\n} from './operations/reads';\nimport { deleteObject, putObject } from './operations/writes';\nimport type {
|
|
5
|
+
"import { S3Client, type S3File, type S3Options, type S3Stats } from 'bun';\nimport { bucketContext, keyOf } from './context';\nimport { listObjects } from './operations/list';\nimport {\n\ttype PresignOptions,\n\tpresignGetUrl,\n\tpresignPutUrl,\n} from './operations/presign';\nimport {\n\tobjectExists,\n\treadBytes,\n\treadText,\n\tstatObject,\n} from './operations/reads';\nimport { deleteObject, putObject } from './operations/writes';\nimport type {\n\tBucketDefinition,\n\tObjectPage,\n\tPutBody,\n\tPutOptions,\n} from './types';\n\nexport type { PresignOptions };\n\n/** A bucket bound to credentials: the definition, with somewhere to put it. */\nexport interface BoundBucket<P> {\n\t/** The client this holds, for anything this package does not wrap. */\n\treadonly client: S3Client;\n\t/** The key this would use, for a caller that needs the string itself. */\n\tkeyFor(params: P): string;\n\t/** Bun's own lazy handle: `.stream()`, `.slice()`, `.writer()` and the rest. */\n\tfile(params: P): S3File;\n\t/**\n\t * Writes it, once the bucket's content type and size have accepted it.\n\t *\n\t * Beyond `type`, an option here describes **this object** — how it is\n\t * served back (`contentDisposition`, `contentEncoding`), who may read it\n\t * (`acl`), what it costs to keep (`storageClass`) — or how a large body is\n\t * uploaded (`partSize`, `queueSize`, `retry`).\n\t */\n\tput(params: P, body: PutBody, options?: PutOptions): Promise<void>;\n\t/** The bytes, or `undefined` when there is no such object. */\n\tbytes(params: P): Promise<Uint8Array | undefined>;\n\t/** The body as text, or `undefined` when there is no such object. */\n\ttext(params: P): Promise<string | undefined>;\n\texists(params: P): Promise<boolean>;\n\t/** What the service knows about it, or `undefined` when it is not there. */\n\tstat(params: P): Promise<S3Stats | undefined>;\n\t/** Removes it. S3 does not say whether anything was there, and nor does this. */\n\tdelete(params: P): Promise<void>;\n\t/** One page of the bucket, in this repository's cursor shape. */\n\tlist(options?: {\n\t\tprefix?: string;\n\t\tlimit?: number;\n\t\tcursor?: string | null;\n\t}): Promise<ObjectPage>;\n\t/** A URL that reads this object, signed. */\n\tpresignGet(params: P, options?: PresignOptions): string;\n\t/**\n\t * A URL that writes this object, signed. It constrains the key and the\n\t * deadline, and **nothing else** — not the size, not the content type.\n\t */\n\tpresignPut(params: P, options?: PresignOptions): string;\n}\n\n/**\n * Binds a bucket definition to credentials.\n *\n * ```ts\n * const store = bindBucket(avatars, {\n * \tendpoint: process.env.S3_ENDPOINT,\n * \taccessKeyId: process.env.S3_KEY,\n * \tsecretAccessKey: process.env.S3_SECRET,\n * });\n * await store.put({ userId: 'u1' }, png, { type: 'image/png' });\n * ```\n *\n * Given no options, Bun reads its own `S3_*` / `AWS_*` environment variables.\n *\n * Each bound bucket holds an `S3Client` of its own. S3 is stateless HTTP —\n * there is no connection to share, and nothing to close.\n */\nexport function bindBucket<P>(\n\tdefinition: BucketDefinition<P>,\n\toptions: Omit<S3Options, 'bucket'> = {},\n): BoundBucket<P> {\n\tconst client = new S3Client({ ...options, bucket: definition.bucket });\n\tconst context = bucketContext(client, definition);\n\treturn {\n\t\tclient,\n\t\tkeyFor: (params) => keyOf(context, params),\n\t\tfile: (params) => client.file(keyOf(context, params)),\n\t\tput: (params, body, putOptions) =>\n\t\t\tputObject(context, params, body, putOptions),\n\t\tbytes: (params) => readBytes(context, params),\n\t\ttext: (params) => readText(context, params),\n\t\texists: (params) => objectExists(context, params),\n\t\tstat: (params) => statObject(context, params),\n\t\tdelete: (params) => deleteObject(context, params),\n\t\tlist: (listOptions) => listObjects(context, listOptions),\n\t\tpresignGet: (params, presignOptions) =>\n\t\t\tpresignGetUrl(context, params, presignOptions),\n\t\tpresignPut: (params, presignOptions) =>\n\t\t\tpresignPutUrl(context, params, presignOptions),\n\t};\n}\n",
|
|
6
6
|
"import type { S3Client } from 'bun';\nimport type { BucketDefinition } from './types';\n\n/**\n * What every operation of a bound bucket works from, resolved once: the\n * client, the definition, and the accepted content types as a list.\n *\n * It holds **data only**. The operations are plain functions that take it as\n * their first argument, in `guards.ts` and `operations/` — a context of\n * closures would only be the factory this package split up, one size down.\n */\nexport interface BucketContext<P> {\n\treadonly client: S3Client;\n\treadonly definition: BucketDefinition<P>;\n\t/**\n\t * The types the definition accepts, always as a list, **as written** —\n\t * an error message quotes these, and the comparison normalises them.\n\t * `undefined` when the bucket accepts anything.\n\t */\n\treadonly accepted: readonly string[] | undefined;\n}\n\n/** The types a definition accepts, as a list. */\nfunction acceptedTypes(\n\tcontentType: string | readonly string[] | undefined,\n): readonly string[] | undefined {\n\tif (contentType === undefined) return undefined;\n\treturn typeof contentType === 'string' ? [contentType] : contentType;\n}\n\nexport function bucketContext<P>(\n\tclient: S3Client,\n\tdefinition: BucketDefinition<P>,\n): BucketContext<P> {\n\treturn {\n\t\tclient,\n\t\tdefinition,\n\t\taccepted: acceptedTypes(definition.contentType),\n\t};\n}\n\n/** The key this definition builds for these parameters. */\nexport function keyOf<P>(context: BucketContext<P>, params: P): string {\n\treturn context.definition.key(params);\n}\n",
|
|
7
7
|
"import type { S3ListObjectsResponse } from 'bun';\nimport type { BucketContext } from '../context';\nimport type { ObjectPage, StoredObject } from '../types';\n\n/** One page of the bucket, in this repository's cursor shape. */\nexport async function listObjects<P>(\n\tcontext: BucketContext<P>,\n\toptions: { prefix?: string; limit?: number; cursor?: string | null } = {},\n): Promise<ObjectPage> {\n\tconst answer = await context.client.list({\n\t\tprefix: options.prefix,\n\t\tmaxKeys: options.limit,\n\t\tcontinuationToken: options.cursor ?? undefined,\n\t});\n\tconst contents: NonNullable<S3ListObjectsResponse['contents']> =\n\t\tanswer.contents ?? [];\n\tconst items: StoredObject[] = contents.map((found) => ({\n\t\tkey: found.key,\n\t\tsize: found.size,\n\t\tlastModified: found.lastModified ? new Date(found.lastModified) : undefined,\n\t\teTag: found.eTag,\n\t}));\n\t// `isTruncated` is what says there is more, and it is the only thing that\n\t// does: a service that sends a token on the last page anyway would page\n\t// for ever if the token alone decided. `null` is this repository's\n\t// \"no next page\".\n\tconst next = answer.isTruncated\n\t\t? (answer.nextContinuationToken ?? null)\n\t\t: null;\n\treturn { items, nextCursor: next };\n}\n",
|
|
8
|
-
"import type { S3Options } from 'bun';\nimport { type BucketContext, keyOf } from '../context';\n\n/** How a presigned URL is asked for. `expiresIn` is **seconds**, as S3's is. */\nexport interface PresignOptions {\n\t/** Seconds until it expires. Bun's default is a day; give one. */\n\texpiresIn?: number;\n\t/** `public-read` and the rest, when the service honours it. */\n\tacl?: S3Options['acl'];\n}\n\nexport function presignGetUrl<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\toptions?: PresignOptions,\n): string {\n\treturn context.client.presign(keyOf(context, params), {\n\t\t...options,\n\t\tmethod: 'GET',\n\t});\n}\n\n/**\n * A URL that writes this object, signed.\n *\n * It carries **no content type**, and takes none. Measured on bun 1.4.2:\n * `presign`'s `type` only adds `response-content-type`, S3's override for\n * what a *download* is labelled; `X-Amz-SignedHeaders` stays `host`, so the\n * `Content-Type` the uploader sends is not signed and not constrained. A PUT\n * signed for a `text/csv` bucket stores an `application/zip` body happily —\n * measured against this package's own test service, status 200. Naming a type\n * here would only look like a guarantee.\n */\nexport function presignPutUrl<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\toptions?: PresignOptions,\n): string {\n\treturn context.client.presign(keyOf(context, params), {\n\t\t...options,\n\t\tmethod: 'PUT',\n\t});\n}\n",
|
|
8
|
+
"import type { S3Options } from 'bun';\nimport { type BucketContext, keyOf } from '../context';\n\n/** How a presigned URL is asked for. `expiresIn` is **seconds**, as S3's is. */\nexport interface PresignOptions {\n\t/** Seconds until it expires. Bun's default is a day; give one. */\n\texpiresIn?: number;\n\t/** `public-read` and the rest, when the service honours it. */\n\tacl?: S3Options['acl'];\n}\n\n/**\n * The options this package signs with, and only those.\n *\n * The same rule as a write's, for the same measured reason: Bun's second\n * parameter extends `S3Options`, so a value carrying extra keys at run time —\n * an options bag off a request body, anything that is not a fresh object\n * literal — **redirects the signed URL**. Measured: a `bucket` key signs a\n * URL for another bucket, and a credential key signs it against another\n * endpoint entirely. The types refuse both; a value that never met the types\n * does not.\n */\nconst SIGNED = ['expiresIn', 'acl'] as const satisfies readonly Signable[];\n\ntype Signable = keyof PresignOptions;\ntype Unsigned = Exclude<Signable, (typeof SIGNED)[number]>;\nconst _nothingForgotten: [Unsigned] extends [never] ? true : Unsigned = true;\nvoid _nothingForgotten;\n\nfunction signed(options: PresignOptions | undefined): PresignOptions {\n\tconst forwarded: Record<string, unknown> = {};\n\tfor (const key of SIGNED) {\n\t\tif (options?.[key] !== undefined) forwarded[key] = options[key];\n\t}\n\treturn forwarded as PresignOptions;\n}\n\nexport function presignGetUrl<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\toptions?: PresignOptions,\n): string {\n\treturn context.client.presign(keyOf(context, params), {\n\t\t...signed(options),\n\t\tmethod: 'GET',\n\t});\n}\n\n/**\n * A URL that writes this object, signed.\n *\n * It carries **no content type**, and takes none. Measured on bun 1.4.2:\n * `presign`'s `type` only adds `response-content-type`, S3's override for\n * what a *download* is labelled; `X-Amz-SignedHeaders` stays `host`, so the\n * `Content-Type` the uploader sends is not signed and not constrained. A PUT\n * signed for a `text/csv` bucket stores an `application/zip` body happily —\n * measured against this package's own test service, status 200. Naming a type\n * here would only look like a guarantee.\n */\nexport function presignPutUrl<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\toptions?: PresignOptions,\n): string {\n\treturn context.client.presign(keyOf(context, params), {\n\t\t...signed(options),\n\t\tmethod: 'PUT',\n\t});\n}\n",
|
|
9
9
|
"import type { S3File, S3Stats } from 'bun';\nimport { type BucketContext, keyOf } from '../context';\n\n/**\n * `undefined` rather than a throw when the object is simply not there.\n *\n * It reads straight away and catches the service's own `NoSuchKey`, rather\n * than asking `exists` first: one round trip instead of two, and no window in\n * which an object deleted between the two turns a promised `undefined` into a\n * throw. `stat` is itself a HEAD, so asking first bought nothing at all.\n */\nasync function whenPresent<P, T>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\tread: (file: S3File) => Promise<T>,\n): Promise<T | undefined> {\n\ttry {\n\t\treturn await read(context.client.file(keyOf(context, params)));\n\t} catch (reason) {\n\t\t// Bun names its own S3 failures `S3Error` too, and carries S3's code.\n\t\tif ((reason as { code?: unknown }).code === 'NoSuchKey') return undefined;\n\t\tthrow reason;\n\t}\n}\n\nexport function readBytes<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<Uint8Array | undefined> {\n\treturn whenPresent(context, params, (file) => file.bytes());\n}\n\nexport function readText<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<string | undefined> {\n\treturn whenPresent(context, params, (file) => file.text());\n}\n\nexport function statObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<S3Stats | undefined> {\n\treturn whenPresent(context, params, (file) => file.stat());\n}\n\nexport function objectExists<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<boolean> {\n\treturn context.client.exists(keyOf(context, params));\n}\n",
|
|
10
10
|
"/** What went wrong. Each one is documented in the README's Traps. */\nexport type S3ErrorCode =\n\t/** The body's content type is not one this bucket accepts. */\n\t| 'WRONG_TYPE'\n\t/** The body is bigger than this bucket's `maxSize`. */\n\t| 'TOO_LARGE'\n\t/** The body's size cannot be known before sending, and `maxSize` is set. */\n\t| 'UNMEASURABLE';\n\n/**\n * This package's only error, and every one of them is thrown **before**\n * anything is sent. S3's own failures come back as they are, from Bun's\n * client.\n */\nexport class S3Error extends Error {\n\treadonly code: S3ErrorCode;\n\t/** The object key it was about — the bucket and the key, never the body. */\n\treadonly key: string;\n\n\tconstructor(code: S3ErrorCode, key: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'S3Error';\n\t\tthis.code = code;\n\t\tthis.key = key;\n\t}\n}\n",
|
|
11
11
|
"import { S3Error } from '../errors/s3-error';\nimport type { BucketContext } from './context';\nimport type { PutBody } from './types';\n\n/**\n * A content type without its parameters, lower-cased: `text/plain` from\n * `text/plain;charset=utf-8`.\n *\n * Both sides of the comparison go through this, because the type a body\n * carries is rarely the bare one a definition names — `Bun.file('a.json')`\n * reports `application/json;charset=utf-8` (measured on bun 1.4.2), and an\n * explicit `IMAGE/PNG` is the same type as `image/png`.\n */\nexport function essenceOf(type: string): string {\n\treturn (type.split(';')[0] ?? '').trim().toLowerCase();\n}\n\n/**\n * The type a write would carry: the one the caller named, or the one the\n * body knows about itself. One function, so the type `check` approves and\n * the type `put` sends can never be two different answers.\n */\nexport function effectiveType(\n\tbody: PutBody,\n\tnamed: string | undefined,\n): string | undefined {\n\t// A Blob carries its own type; anything else has to be told.\n\treturn named ?? (body instanceof Blob ? body.type || undefined : undefined);\n}\n\n/** The body's size, or `undefined` when it cannot be known before sending. */\nexport function sizeOf(body: PutBody): number | undefined {\n\tif (typeof body === 'string') return Buffer.byteLength(body, 'utf8');\n\t// An `S3File` **is** a `Blob` — measured on bun 1.4.2 — and its `size` is\n\t// `NaN`, because nothing has asked the service yet. Returning that would\n\t// pass the guard silently: `NaN > maxSize` is false, whatever `maxSize` is.\n\tif (body instanceof Blob) {\n\t\treturn Number.isFinite(body.size) ? body.size : undefined;\n\t}\n\tif (body instanceof ArrayBuffer) return body.byteLength;\n\tif (ArrayBuffer.isView(body)) return body.byteLength;\n\t// A stream, a `Response`: nothing says how long it is until it has been\n\t// read, which is what `UNMEASURABLE` is about.\n\treturn undefined;\n}\n\n/** Refuses a type the bucket does not accept. Shared by `put` and `presign`. */\nexport function checkType<P>(\n\tcontext: BucketContext<P>,\n\tkey: string,\n\ttype: string | undefined,\n): void {\n\tconst { accepted } = context;\n\tif (!accepted) return;\n\tconst list = accepted.join(', ');\n\tif (!type) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_TYPE',\n\t\t\tkey,\n\t\t\t`\"${context.definition.bucket}\" accepts ${list}, and this write ` +\n\t\t\t\t'names no content type. Pass `type`',\n\t\t);\n\t}\n\tif (!accepted.some((one) => essenceOf(one) === essenceOf(type))) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_TYPE',\n\t\t\tkey,\n\t\t\t`\"${context.definition.bucket}\" accepts ${list}, not ${type}`,\n\t\t);\n\t}\n}\n\n/** Refuses a body the bucket does not accept, before anything is sent. */\nexport function checkSize<P>(\n\tcontext: BucketContext<P>,\n\tkey: string,\n\tbody: PutBody,\n): void {\n\tconst { maxSize, bucket } = context.definition;\n\tif (maxSize === undefined) return;\n\tconst size = sizeOf(body);\n\tif (size === undefined) {\n\t\tthrow new S3Error(\n\t\t\t'UNMEASURABLE',\n\t\t\tkey,\n\t\t\t`\"${bucket}\" has a maxSize, and this body's size cannot be known ` +\n\t\t\t\t'before sending it. Read it into memory first, or drop `maxSize` ' +\n\t\t\t\t'and let the service refuse it',\n\t\t);\n\t}\n\tif (size > maxSize) {\n\t\tthrow new S3Error(\n\t\t\t'TOO_LARGE',\n\t\t\tkey,\n\t\t\t`\"${bucket}\" accepts ${maxSize} bytes at most, and this body is ${size}`,\n\t\t);\n\t}\n}\n",
|
|
12
|
-
"import { type BucketContext, keyOf } from '../context';\nimport { checkSize, checkType, effectiveType } from '../guards';\nimport type { PutBody } from '../types';\n\n/**\n * Writes it, once the bucket's content type and size have accepted it. Both\n * guards run before `write` is called, so a refused body is never sent.\n */\nexport async function putObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\tbody: PutBody,\n\toptions
|
|
12
|
+
"import { type BucketContext, keyOf } from '../context';\nimport { checkSize, checkType, effectiveType } from '../guards';\nimport type { PutBody, PutOptions } from '../types';\n\n/**\n * Writes it, once the bucket's content type and size have accepted it. Both\n * guards run before `write` is called, so a refused body is never sent.\n */\nexport async function putObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\tbody: PutBody,\n\toptions: PutOptions = {},\n): Promise<void> {\n\tconst key = keyOf(context, params);\n\tconst type = effectiveType(body, options.type);\n\tcheckType(context, key, type);\n\tcheckSize(context, key, body);\n\t// The very type `checkType` approved, and nothing else: `type` is not one\n\t// of the keys `passed` forwards, so no option can carry a second one in\n\t// beside it.\n\tawait context.client.write(key, body, {\n\t\t...passed(options),\n\t\t...(type ? { type } : {}),\n\t});\n}\n\n/**\n * The options this package forwards, and only those.\n *\n * `PutOptions` refuses the rest at compile time, and that is not enough:\n * measured, spreading the caller's object straight through let a `bucket`\n * key **redirect the write to another bucket** — the object was stored\n * somewhere the definition never described, and the call reported success.\n * Options that arrive from outside a handler are not typed, so the list is\n * applied at run time as well. A key that is not here is dropped, never sent.\n */\ntype Forwardable = Exclude<keyof PutOptions, 'type'>;\n\nconst PASSED = [\n\t'acl',\n\t'storageClass',\n\t'contentDisposition',\n\t'contentEncoding',\n] as const satisfies readonly Forwardable[];\n\n/**\n * `satisfies` proves every key listed is real; it proves nothing about one\n * that is **missing**. Widen `PutOptions` and forget to list the new key\n * here, and the type would advertise an option the run time silently drops —\n * which is the failure this allowlist exists to prevent, in the other\n * direction. This line is what fails the build instead.\n */\ntype Unforwarded = Exclude<Forwardable, (typeof PASSED)[number]>;\nconst _nothingForgotten: [Unforwarded] extends [never] ? true : Unforwarded =\n\ttrue;\nvoid _nothingForgotten;\n\nfunction passed(options: PutOptions): PutOptions {\n\tconst forwarded: Record<string, unknown> = {};\n\tfor (const key of PASSED) {\n\t\tif (options[key] !== undefined) forwarded[key] = options[key];\n\t}\n\treturn forwarded as PutOptions;\n}\n\n/** Removes it. S3 does not say whether anything was there, and nor does this. */\nexport function deleteObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<void> {\n\treturn context.client.delete(keyOf(context, params));\n}\n",
|
|
13
13
|
"import type { BucketDefinition } from './types';\n\n/**\n * Describes a bucket. It talks to nothing: `bindBucket` is what needs\n * credentials.\n *\n * ```ts\n * export const avatars = defineBucket({\n * \tbucket: 'avatars',\n * \tkey: (p: { userId: string }) => `${p.userId}.png`,\n * \tcontentType: ['image/png', 'image/jpeg'],\n * \tmaxSize: 2 * 1024 * 1024,\n * });\n * ```\n */\nexport function defineBucket<P>(\n\tdefinition: BucketDefinition<P>,\n): BucketDefinition<P> {\n\tif (definition.bucket.length === 0) {\n\t\tthrow new TypeError('defineBucket: a bucket definition needs a bucket');\n\t}\n\tif (definition.maxSize !== undefined) {\n\t\tconst { maxSize } = definition;\n\t\tif (!Number.isFinite(maxSize) || maxSize <= 0) {\n\t\t\tthrow new TypeError(\n\t\t\t\t`defineBucket: \"${definition.bucket}\" has a maxSize of ${maxSize}; ` +\n\t\t\t\t\t'it is a number of bytes, and must be above zero',\n\t\t\t);\n\t\t}\n\t}\n\tconst types = definition.contentType;\n\tif (Array.isArray(types) && types.length === 0) {\n\t\tthrow new TypeError(\n\t\t\t`defineBucket: \"${definition.bucket}\" accepts an empty list of ` +\n\t\t\t\t'content types, so nothing could ever be written. Leave ' +\n\t\t\t\t'`contentType` out to accept anything',\n\t\t);\n\t}\n\treturn Object.freeze({ ...definition });\n}\n"
|
|
14
14
|
],
|
|
15
|
-
"mappings": ";AAAA;;;ACuBA,SAAS,aAAa,CACrB,aACgC;AAAA,EAChC,IAAI,gBAAgB;AAAA,IAAW;AAAA,EAC/B,OAAO,OAAO,gBAAgB,WAAW,CAAC,WAAW,IAAI;AAAA;AAGnD,SAAS,aAAgB,CAC/B,QACA,YACmB;AAAA,EACnB,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA,UAAU,cAAc,WAAW,WAAW;AAAA,EAC/C;AAAA;AAIM,SAAS,KAAQ,CAAC,SAA2B,QAAmB;AAAA,EACtE,OAAO,QAAQ,WAAW,IAAI,MAAM;AAAA;;;ACtCrC,eAAsB,WAAc,CACnC,SACA,UAAuE,CAAC,GAClD;AAAA,EACtB,MAAM,SAAS,MAAM,QAAQ,OAAO,KAAK;AAAA,IACxC,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,mBAAmB,QAAQ,UAAU;AAAA,EACtC,CAAC;AAAA,EACD,MAAM,WACL,OAAO,YAAY,CAAC;AAAA,EACrB,MAAM,QAAwB,SAAS,IAAI,CAAC,WAAW;AAAA,IACtD,KAAK,MAAM;AAAA,IACX,MAAM,MAAM;AAAA,IACZ,cAAc,MAAM,eAAe,IAAI,KAAK,MAAM,YAAY,IAAI;AAAA,IAClE,MAAM,MAAM;AAAA,EACb,EAAE;AAAA,EAKF,MAAM,OAAO,OAAO,cAChB,OAAO,yBAAyB,OACjC;AAAA,EACH,OAAO,EAAE,OAAO,YAAY,KAAK;AAAA;;;
|
|
16
|
-
"debugId": "
|
|
15
|
+
"mappings": "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",
|
|
16
|
+
"debugId": "F9842873CB32A8B764756E2164756E21",
|
|
17
17
|
"names": []
|
|
18
18
|
}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# `@nxgt/s3` documentation
|
|
2
|
+
|
|
3
|
+
S3 on Bun's own `S3Client`. `S3Client` is built into Bun, so this package has
|
|
4
|
+
**no dependency at all** — no AWS SDK — and does not run on Node.
|
|
5
|
+
|
|
6
|
+
| Page | Read it when |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| [Buckets](guide/buckets.md) | you are describing a bucket, building its keys, or binding it to credentials |
|
|
9
|
+
| [Reading](guide/reads.md) | you want an object's bytes, its text, whether it is there, what the service knows about it, or a page of the bucket |
|
|
10
|
+
| [Writing](guide/writes.md) | you are storing an object, choosing what a write says about it, or handling a refusal |
|
|
11
|
+
| [Presigned URLs](guide/presigned-urls.md) | a browser or another service should read or write an object directly, without your credentials |
|
|
12
|
+
| [Troubleshooting](troubleshooting.md) | a call threw, an upload was refused, or a signed URL did not do what you expected |
|
|
13
|
+
| [Roadmap](roadmap.md) | you want to know what is coming, and what has been ruled out |
|
|
14
|
+
|
|
15
|
+
The [README](../README.md) is the short version: install, one example per
|
|
16
|
+
area, and the traps in one line each.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Buckets
|
|
2
|
+
|
|
3
|
+
Describing a bucket once — which bucket, how a key is built, and what this
|
|
4
|
+
application is willing to put there — then binding that description to
|
|
5
|
+
credentials.
|
|
6
|
+
|
|
7
|
+
## The smallest thing that works
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { bindBucket, defineBucket } from '@nxgt/s3';
|
|
11
|
+
|
|
12
|
+
export const avatars = defineBucket({
|
|
13
|
+
bucket: 'avatars',
|
|
14
|
+
key: (p: { userId: string }) => `${p.userId}.png`,
|
|
15
|
+
contentType: ['image/png', 'image/jpeg'],
|
|
16
|
+
maxSize: 2 * 1024 * 1024, // bytes
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const store = bindBucket(avatars, {
|
|
20
|
+
endpoint: process.env.S3_ENDPOINT,
|
|
21
|
+
accessKeyId: process.env.S3_KEY,
|
|
22
|
+
secretAccessKey: process.env.S3_SECRET,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
store.keyFor({ userId: 'u1' }); // 'u1.png'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`defineBucket` talks to nothing, so the definition is what a server, a worker
|
|
29
|
+
and a test import; `bindBucket` is the half that needs credentials.
|
|
30
|
+
`S3Client` is built into Bun, which is why there is no SDK in `bun add` — and
|
|
31
|
+
why this package does not run on Node.
|
|
32
|
+
|
|
33
|
+
## The signatures
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import type { S3Client, S3File, S3Options, S3Stats } from 'bun';
|
|
37
|
+
|
|
38
|
+
interface BucketDefinition<P> {
|
|
39
|
+
readonly bucket: string;
|
|
40
|
+
readonly key: (params: P) => string;
|
|
41
|
+
readonly contentType?: string | readonly string[];
|
|
42
|
+
readonly maxSize?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defineBucket<P>(definition: BucketDefinition<P>): BucketDefinition<P>;
|
|
46
|
+
|
|
47
|
+
function bindBucket<P>(
|
|
48
|
+
definition: BucketDefinition<P>,
|
|
49
|
+
options?: Omit<S3Options, 'bucket'>,
|
|
50
|
+
): BoundBucket<P>;
|
|
51
|
+
|
|
52
|
+
interface BoundBucket<P> {
|
|
53
|
+
readonly client: S3Client;
|
|
54
|
+
keyFor(params: P): string;
|
|
55
|
+
file(params: P): S3File;
|
|
56
|
+
put(params: P, body: PutBody, options?: PutOptions): Promise<void>;
|
|
57
|
+
bytes(params: P): Promise<Uint8Array | undefined>;
|
|
58
|
+
text(params: P): Promise<string | undefined>;
|
|
59
|
+
exists(params: P): Promise<boolean>;
|
|
60
|
+
stat(params: P): Promise<S3Stats | undefined>;
|
|
61
|
+
delete(params: P): Promise<void>;
|
|
62
|
+
list(options?: {
|
|
63
|
+
prefix?: string;
|
|
64
|
+
limit?: number;
|
|
65
|
+
cursor?: string | null;
|
|
66
|
+
}): Promise<ObjectPage>;
|
|
67
|
+
presignGet(params: P, options?: PresignOptions): string;
|
|
68
|
+
presignPut(params: P, options?: PresignOptions): string;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## The definition
|
|
73
|
+
|
|
74
|
+
| Option | Type | Default | Effect |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `bucket` | `string` | required | the bucket's name on the service |
|
|
77
|
+
| `key` | `(params: P) => string` | required | the object's key, from whatever identifies it |
|
|
78
|
+
| `contentType` | `string \| readonly string[]` | anything goes | the content types this bucket accepts; a write of anything else is refused **before it is sent** |
|
|
79
|
+
| `maxSize` | `number` | anything goes | the biggest body, in **bytes**, refused before it is sent |
|
|
80
|
+
|
|
81
|
+
The key is a **function**, not a template, so nothing is spelled by hand at a
|
|
82
|
+
call site and a renamed parameter is a compile error. `P` is whatever that
|
|
83
|
+
function takes:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const uploads = defineBucket({
|
|
87
|
+
bucket: 'uploads',
|
|
88
|
+
key: (p: { folder: string; name: string }) => `${p.folder}/${p.name}`,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
bindBucket(uploads).keyFor({ folder: 'a', name: 'b.txt' }); // 'a/b.txt'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`keyFor` exists so a caller that needs the string gets *the* string. Building
|
|
95
|
+
one by hand somewhere else is how a bucket ends up with two spellings of the
|
|
96
|
+
same object.
|
|
97
|
+
|
|
98
|
+
What `contentType` and `maxSize` do is [Writing](writes.md); they are guards
|
|
99
|
+
on `put`, and nothing else on the object goes through them.
|
|
100
|
+
|
|
101
|
+
`defineBucket` refuses a definition that could never work, at import time:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
defineBucket({ bucket: '', key: () => 'k' });
|
|
105
|
+
// TypeError: defineBucket: a bucket definition needs a bucket
|
|
106
|
+
|
|
107
|
+
defineBucket({ bucket: 'b', key: () => 'k', maxSize: 0 });
|
|
108
|
+
// TypeError: defineBucket: "b" has a maxSize of 0; it is a number of bytes…
|
|
109
|
+
|
|
110
|
+
defineBucket({ bucket: 'b', key: () => 'k', contentType: [] });
|
|
111
|
+
// TypeError: … accepts an empty list of content types, so nothing could ever
|
|
112
|
+
// be written. Leave `contentType` out to accept anything
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The definition it gives back is frozen, so it cannot drift once it is shared.
|
|
116
|
+
|
|
117
|
+
## Binding it
|
|
118
|
+
|
|
119
|
+
`options` is Bun's own `S3Options` without `bucket` — the bucket comes from
|
|
120
|
+
the definition, and a call may never change it. It is optional: given none,
|
|
121
|
+
Bun reads its own `S3_*` / `AWS_*` environment variables.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const store = bindBucket(avatars); // credentials from the environment
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| Option | Type | Default | Effect |
|
|
128
|
+
| --- | --- | --- | --- |
|
|
129
|
+
| `endpoint` | `string` | AWS | the service's URL; anything S3-compatible Bun can sign for |
|
|
130
|
+
| `accessKeyId` / `secretAccessKey` | `string` | `$S3_*` / `$AWS_*` | the credentials |
|
|
131
|
+
| `sessionToken` | `string` | `$AWS_SESSION_TOKEN` | for temporary credentials |
|
|
132
|
+
| `region` | `string` | `$S3_REGION` / `$AWS_REGION` | the region to sign for |
|
|
133
|
+
| `virtualHostedStyle` | `boolean` | `false` | `bucket.host` URLs instead of `host/bucket`; a service that is not AWS usually wants it left off |
|
|
134
|
+
| `acl`, `storageClass` | see [Writing](writes.md) | — | a default for every object this client writes |
|
|
135
|
+
| `retry`, `partSize`, `queueSize` | `number` | Bun's | the client's own transfer tuning. A [`put`](writes.md) takes none of these: it is one PUT |
|
|
136
|
+
|
|
137
|
+
Each bound bucket holds an `S3Client` of its own. S3 is stateless HTTP —
|
|
138
|
+
there is no connection to share, and **nothing to close**.
|
|
139
|
+
|
|
140
|
+
## Everything this package does not wrap
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
store.client; // Bun's S3Client
|
|
144
|
+
store.file({ userId: 'u1' }); // Bun's lazy S3File
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`file(params)` is the handle for `.stream()`, `.slice()`, `.writer()` and the
|
|
148
|
+
rest, keyed by the definition rather than by a string you typed. It is also
|
|
149
|
+
the way to write a body in parts, which `put` does not do.
|
|
150
|
+
|
|
151
|
+
## Types a caller names
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import type { BoundBucket, ParamsOf } from '@nxgt/s3';
|
|
155
|
+
|
|
156
|
+
type AvatarParams = ParamsOf<typeof avatars>; // { userId: string }
|
|
157
|
+
|
|
158
|
+
async function purge(bucket: BoundBucket<AvatarParams>, userId: string) {
|
|
159
|
+
await bucket.delete({ userId });
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`stat` gives back Bun's own `S3Stats`, which this package does not re-export:
|
|
164
|
+
import it from `bun` where you need to name it.
|
|
165
|
+
|
|
166
|
+
## Next
|
|
167
|
+
|
|
168
|
+
- [Writing](writes.md) — `put`, its options, and the guards.
|
|
169
|
+
- [Reading](reads.md) — `bytes`, `text`, `stat`, `exists` and `list`.
|
|
170
|
+
- [Presigned URLs](presigned-urls.md) — the same definition, signed.
|