@lunora/storage 1.0.0-alpha.13 → 1.0.0-alpha.130
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 +15 -7
- package/dist/index.d.mts +130 -169
- package/dist/index.d.ts +130 -169
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/DEFAULT_MAX_UPLOAD_BYTES-BOaXqwP5.mjs +1 -0
- package/dist/packem_shared/R2_PART_SIZE-Dw8vEttI.mjs +1 -0
- package/dist/packem_shared/buildPresignedUrl-U55ktL8x.mjs +4 -0
- package/dist/packem_shared/buildSignedUrl-czz1xjLU.mjs +6 -0
- package/dist/packem_shared/createBucketStorage-AqIMq050.mjs +1 -0
- package/dist/packem_shared/createR2BindingUploadStorage-Vjs0vyJJ.mjs +1 -0
- package/dist/packem_shared/createR2UploadStorage-DGB1xJiD.mjs +1 -0
- package/dist/packem_shared/createStorage-YPzdUgGl.mjs +1 -0
- package/dist/packem_shared/internal-B5yyxF10.mjs +1 -0
- package/dist/packem_shared/r2-upload-part-writer-SdQlWaAu.mjs +1 -0
- package/dist/upload.d.mts +242 -18
- package/dist/upload.d.ts +242 -18
- package/dist/upload.mjs +1 -1
- package/package.json +4 -3
- package/dist/packem_shared/buildPresignedUrl-TCv-5TAT.mjs +0 -4
- package/dist/packem_shared/buildSignedUrl-DocLLDGy.mjs +0 -5
- package/dist/packem_shared/createBucketStorage-DAip6cEw.mjs +0 -1
- package/dist/packem_shared/createR2UploadStorage-BSgOjRvv.mjs +0 -1
- package/dist/packem_shared/createStorage-CWKguFEG.mjs +0 -1
- package/dist/packem_shared/internal-DpjlEJPE.mjs +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,134 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* bound required, mirroring R2's own `R2Range`) or a `{ suffix }` tail. The
|
|
4
|
-
* subset of `R2Range` that {@link Storage.download} forwards so a caller can
|
|
5
|
-
* stream just the bytes it needs instead of the whole object.
|
|
6
|
-
*/
|
|
7
|
-
type R2RangeLike = {
|
|
8
|
-
length: number;
|
|
9
|
-
offset?: number;
|
|
10
|
-
} | {
|
|
11
|
-
length?: number;
|
|
12
|
-
offset: number;
|
|
13
|
-
} | {
|
|
14
|
-
suffix: number;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Minimal projection of `R2Bucket`. Declared structurally so unit tests can
|
|
18
|
-
* pass a plain object double; the real binding satisfies the same shape.
|
|
19
|
-
*/
|
|
20
|
-
interface R2BucketLike {
|
|
21
|
-
/**
|
|
22
|
-
* Begin a multipart upload (R2 `createMultipartUpload`). Optional so existing
|
|
23
|
-
* test doubles still satisfy the type; {@link Storage.createMultipartUpload}
|
|
24
|
-
* throws a clear error when the binding lacks it.
|
|
25
|
-
*/
|
|
26
|
-
createMultipartUpload?: (key: string, options?: {
|
|
27
|
-
customMetadata?: Record<string, string>;
|
|
28
|
-
httpMetadata?: {
|
|
29
|
-
contentType?: string;
|
|
30
|
-
};
|
|
31
|
-
}) => Promise<R2MultipartUploadLike>;
|
|
32
|
-
delete: (key: string) => Promise<void>;
|
|
33
|
-
get: (key: string, options?: {
|
|
34
|
-
range?: R2RangeLike;
|
|
35
|
-
}) => Promise<R2ObjectBodyLike | null>;
|
|
36
|
-
/**
|
|
37
|
-
* Fetch an object's metadata without its body (R2 HEAD). Returns `null` when
|
|
38
|
-
* the object is absent. Declared optional so existing test doubles that only
|
|
39
|
-
* implement `get`/`put`/`list`/`delete` still satisfy the type; callers that
|
|
40
|
-
* need metadata fall back to a 0-length ranged `get()` when `head` is absent.
|
|
41
|
-
*/
|
|
42
|
-
head?: (key: string) => Promise<R2ObjectLike | null>;
|
|
43
|
-
list: (options?: {
|
|
44
|
-
cursor?: string;
|
|
45
|
-
delimiter?: string;
|
|
46
|
-
limit?: number;
|
|
47
|
-
prefix?: string;
|
|
48
|
-
}) => Promise<{
|
|
49
|
-
cursor?: string;
|
|
50
|
-
objects: R2ObjectLike[];
|
|
51
|
-
truncated?: boolean;
|
|
52
|
-
}>;
|
|
53
|
-
put: (key: string, body: ReadableStream | ArrayBuffer | Blob | string | null, options?: {
|
|
54
|
-
customMetadata?: Record<string, string>;
|
|
55
|
-
httpMetadata?: {
|
|
56
|
-
contentType?: string;
|
|
57
|
-
};
|
|
58
|
-
}) => Promise<R2ObjectLike>;
|
|
59
|
-
/** Resume an in-progress multipart upload by id (R2 `resumeMultipartUpload`). Optional; see {@link Storage.resumeMultipartUpload}. */
|
|
60
|
-
resumeMultipartUpload?: (key: string, uploadId: string) => R2MultipartUploadLike;
|
|
61
|
-
}
|
|
62
|
-
/** One uploaded multipart part — returned by `uploadPart`, required to `complete`. Mirrors R2's `R2UploadedPart`. */
|
|
63
|
-
interface R2UploadedPartLike {
|
|
64
|
-
etag: string;
|
|
65
|
-
partNumber: number;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* An in-progress multipart upload, mirroring R2's `R2MultipartUpload`. Each part
|
|
69
|
-
* (except the last) must be uniform in size. The object does not guarantee the
|
|
70
|
-
* underlying upload still exists — a parallel `complete`/`abort` can invalidate
|
|
71
|
-
* it — so wrap each call in error handling.
|
|
72
|
-
*/
|
|
73
|
-
interface R2MultipartUploadLike {
|
|
74
|
-
/** Abort the upload, discarding any uploaded parts. */
|
|
75
|
-
abort: () => Promise<void>;
|
|
76
|
-
/** Finish the upload from the collected parts; resolves to the stored object. */
|
|
77
|
-
complete: (uploadedParts: R2UploadedPartLike[]) => Promise<R2ObjectLike>;
|
|
78
|
-
/** The object key being assembled. */
|
|
79
|
-
readonly key: string;
|
|
80
|
-
/** The R2 upload id (persist it to resume across requests). */
|
|
81
|
-
readonly uploadId: string;
|
|
82
|
-
/** Upload one part (1-indexed); returns the `{ partNumber, etag }` to pass to `complete`. */
|
|
83
|
-
uploadPart: (partNumber: number, value: ArrayBuffer | ArrayBufferView | Blob | ReadableStream | string) => Promise<R2UploadedPartLike>;
|
|
84
|
-
}
|
|
85
|
-
interface R2ObjectLike {
|
|
86
|
-
/**
|
|
87
|
-
* R2-computed checksums. The real binding exposes `sha256` as an
|
|
88
|
-
* `ArrayBuffer` (present only when R2 stored a SHA-256 for the object);
|
|
89
|
-
* declared optional so fakes and non-checksummed objects type-check.
|
|
90
|
-
*/
|
|
91
|
-
checksums?: {
|
|
92
|
-
sha256?: ArrayBuffer;
|
|
93
|
-
};
|
|
94
|
-
customMetadata?: Record<string, string>;
|
|
95
|
-
etag: string;
|
|
96
|
-
/**
|
|
97
|
-
* The quoted form of {@link R2ObjectLike.etag} (e.g. `"abc123"`), suitable
|
|
98
|
-
* for emitting directly as an HTTP `ETag` header. The real binding always
|
|
99
|
-
* provides it; declared optional so existing doubles that only set `etag`
|
|
100
|
-
* still type-check (callers fall back to quoting `etag`).
|
|
101
|
-
*/
|
|
102
|
-
httpEtag?: string;
|
|
103
|
-
httpMetadata?: {
|
|
104
|
-
contentType?: string;
|
|
105
|
-
};
|
|
106
|
-
key: string;
|
|
107
|
-
/**
|
|
108
|
-
* Hex-encoded SHA-256 of the object body, surfaced by `download()`/`list()`
|
|
109
|
-
* when R2 carries a checksum (derived from {@link R2ObjectLike.checksums}).
|
|
110
|
-
*/
|
|
111
|
-
sha256?: string;
|
|
112
|
-
/**
|
|
113
|
-
* Base64-encoded SHA-256 of the object body, surfaced alongside
|
|
114
|
-
* {@link R2ObjectLike.sha256} from the same checksum. Base64 is the encoding
|
|
115
|
-
* RFC 9530 digest headers (`Repr-Digest`/`Content-Digest`) require, so HTTP
|
|
116
|
-
* layers can emit a spec-compliant digest without re-deriving it.
|
|
117
|
-
*/
|
|
118
|
-
sha256Base64?: string;
|
|
119
|
-
size: number;
|
|
120
|
-
/**
|
|
121
|
-
* When the object was written. The real binding exposes this as a `Date`;
|
|
122
|
-
* declared optional so fakes that omit it still type-check.
|
|
123
|
-
* {@link Storage.getMetadata} normalises it to epoch ms.
|
|
124
|
-
*/
|
|
125
|
-
uploaded?: Date;
|
|
126
|
-
}
|
|
127
|
-
interface R2ObjectBodyLike extends R2ObjectLike {
|
|
128
|
-
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
129
|
-
body: ReadableStream | null;
|
|
130
|
-
text: () => Promise<string>;
|
|
131
|
-
}
|
|
1
|
+
import { R2MultipartUploadLike, R2RangeLike, R2ObjectBodyLike, R2ObjectLike, R2BucketLike } from '@lunora/platform';
|
|
2
|
+
export type { R2BucketLike, R2MultipartUploadLike, R2ObjectBodyLike, R2ObjectLike, R2RangeLike, R2UploadedPartLike } from '@lunora/platform';
|
|
132
3
|
/**
|
|
133
4
|
* R2 S3-API credentials for {@link Storage.getPresignedUrl}. These are an R2 API
|
|
134
5
|
* token's Access Key ID / Secret Access Key (NOT a Cloudflare API token), plus
|
|
@@ -149,13 +20,27 @@ interface R2S3Credentials {
|
|
|
149
20
|
}
|
|
150
21
|
/** Options for {@link Storage.getPresignedUrl}. */
|
|
151
22
|
interface PresignedUrlOptions {
|
|
152
|
-
/** Seconds the URL stays valid
|
|
23
|
+
/** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
|
|
153
24
|
expiresInSeconds?: number;
|
|
154
25
|
/** HTTP method the URL authorizes. Default `GET`. */
|
|
155
26
|
method?: "GET" | "PUT";
|
|
156
27
|
}
|
|
157
28
|
interface LunoraStorageOptions {
|
|
158
29
|
bucket: R2BucketLike;
|
|
30
|
+
/**
|
|
31
|
+
* The name this bucket is registered under — the same string a
|
|
32
|
+
* `defineStorageRule({ bucket })` rule and the generated `StorageBucketName`
|
|
33
|
+
* union use. Bound into every signed URL's HMAC and mirrored on it as
|
|
34
|
+
* `&bucket=`, so a URL minted for one bucket can't be replayed against
|
|
35
|
+
* another sharing the signing secret, and the serving route can resolve
|
|
36
|
+
* which bucket to read.
|
|
37
|
+
*
|
|
38
|
+
* Required, and deliberately without a default: a defaulted name is how
|
|
39
|
+
* every bucket ended up signing as `"default"` and cross-verifying against
|
|
40
|
+
* each other. Pass `"default"` for a single-bucket app's `ctx.storage`, and
|
|
41
|
+
* the registered name for any bucket reached through `createBucketStorage`.
|
|
42
|
+
*/
|
|
43
|
+
bucketName: string;
|
|
159
44
|
/** Public base URL used by `getSignedUrl()`. Required for signed URLs. */
|
|
160
45
|
publicBaseUrl?: string;
|
|
161
46
|
/**
|
|
@@ -174,26 +59,50 @@ interface UploadOptions {
|
|
|
174
59
|
customMetadata?: Record<string, string>;
|
|
175
60
|
/**
|
|
176
61
|
* Maximum body size in bytes. For `ArrayBuffer`/`Blob` sources the length is
|
|
177
|
-
* known up front and rejected before the upload starts.
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
62
|
+
* known up front and rejected before the upload starts.
|
|
63
|
+
*
|
|
64
|
+
* A `ReadableStream` has no length to check, and R2 refuses any stream whose
|
|
65
|
+
* length it cannot read — so a capped stream is READ INTO MEMORY under the
|
|
66
|
+
* cap and uploaded as a sized body. Nothing reaches the bucket if the body
|
|
67
|
+
* crosses the limit.
|
|
68
|
+
*
|
|
69
|
+
* `maxSize` is therefore also the memory ceiling for a streamed upload, and
|
|
70
|
+
* the isolate's ~128 MB is shared by every concurrent request — so on the
|
|
71
|
+
* stream path `maxSize` is itself capped at 16 MiB and rejected above it.
|
|
72
|
+
* For objects larger than that use `createMultipartUpload` /
|
|
73
|
+
* `createUploadHandler`, which upload without ever holding the whole object.
|
|
74
|
+
* Must be a finite, non-negative number; anything else (`Number(undefined)`
|
|
75
|
+
* is the usual source) is rejected as a `VALIDATION_ERROR`.
|
|
182
76
|
*/
|
|
183
77
|
maxSize?: number;
|
|
78
|
+
/**
|
|
79
|
+
* SHA-256 of the body (hex or a 32-byte buffer), recorded with the object.
|
|
80
|
+
*
|
|
81
|
+
* R2 only reports a checksum it was given: without this, `list()`/`head()`
|
|
82
|
+
* return no `sha256` and any later integrity check degrades to comparing
|
|
83
|
+
* sizes. R2 also verifies the digest itself on write, so supplying it turns
|
|
84
|
+
* the upload into a checked one.
|
|
85
|
+
*/
|
|
86
|
+
sha256?: ArrayBuffer | string;
|
|
184
87
|
}
|
|
185
88
|
interface ListOptions {
|
|
186
89
|
cursor?: string;
|
|
187
90
|
/** R2 list delimiter — when set, common prefixes group instead of listing. */
|
|
188
91
|
delimiter?: string;
|
|
189
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Defaults to 100, capped at 1000 (R2 limit). A ceiling, not a promise: R2
|
|
94
|
+
* may return fewer per page to fit the entry metadata.
|
|
95
|
+
*
|
|
96
|
+
* Must be a positive integer when given — `0`, `12.5` and `NaN` throw rather
|
|
97
|
+
* than being coerced into a page size the caller never asked for.
|
|
98
|
+
*/
|
|
190
99
|
limit?: number;
|
|
191
100
|
}
|
|
192
101
|
interface SignedUrlOptions {
|
|
193
102
|
/**
|
|
194
103
|
* Pin the `Content-Type` an uploader must send on a `method: "PUT"` URL.
|
|
195
104
|
* Baked into the HMAC canonical so the signature only authorizes a PUT with
|
|
196
|
-
* exactly this content-type; mirrored on the URL as `&
|
|
105
|
+
* exactly this content-type; mirrored on the URL as `&ct=...`. Ignored for
|
|
197
106
|
* `GET` URLs (a download has no request body content-type to pin).
|
|
198
107
|
*/
|
|
199
108
|
contentType?: string;
|
|
@@ -220,6 +129,17 @@ interface ObjectMetadata {
|
|
|
220
129
|
uploaded?: number;
|
|
221
130
|
}
|
|
222
131
|
interface Storage {
|
|
132
|
+
/**
|
|
133
|
+
* The bucket name this accessor operates under — the same value
|
|
134
|
+
* {@link Storage.getSignedUrl} puts into the HMAC canonical.
|
|
135
|
+
*
|
|
136
|
+
* Exposed so everything downstream agrees on one name: `asBucketStorage`
|
|
137
|
+
* tags a single-bucket storage with it, and `storageRules(...)` matches
|
|
138
|
+
* `(bucket, operation)` rules against that tag. Without it a
|
|
139
|
+
* `createStorage({ bucketName: "avatars" })` signed URLs as `avatars` while
|
|
140
|
+
* the rules engine only ever saw `"default"`.
|
|
141
|
+
*/
|
|
142
|
+
readonly bucketName: string;
|
|
223
143
|
/**
|
|
224
144
|
* Begin a native R2 **multipart upload** for very large objects — upload
|
|
225
145
|
* parts (each uniform in size except the last), then `complete` with the
|
|
@@ -251,10 +171,10 @@ interface Storage {
|
|
|
251
171
|
}) => Promise<string>;
|
|
252
172
|
/**
|
|
253
173
|
* Read a stored object's metadata (size, content-type, sha256, upload time,
|
|
254
|
-
* custom metadata) without fetching its body
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* `ctx.storage.getMetadata`.
|
|
174
|
+
* custom metadata) without fetching its body, as a flat serializable shape.
|
|
175
|
+
* Returns `null` when the object is absent. A projection of
|
|
176
|
+
* {@link Storage.head}, so it makes the same single body-free read. Mirrors
|
|
177
|
+
* Convex's `ctx.storage.getMetadata`.
|
|
258
178
|
*/
|
|
259
179
|
getMetadata: (key: string) => Promise<ObjectMetadata | null>;
|
|
260
180
|
/**
|
|
@@ -267,8 +187,32 @@ interface Storage {
|
|
|
267
187
|
getPresignedUrl: (key: string, options?: PresignedUrlOptions) => Promise<string>;
|
|
268
188
|
getSignedUrl: (key: string, options?: SignedUrlOptions) => Promise<string>;
|
|
269
189
|
getUrl: (key: string) => string;
|
|
190
|
+
/**
|
|
191
|
+
* Read an object's R2 metadata WITHOUT its body — `size` (the full object
|
|
192
|
+
* size), `etag`, `httpMetadata`, `checksums`, plus the `sha256`/`sha256Base64`
|
|
193
|
+
* projection `download()` adds. Returns `null` when the object is absent.
|
|
194
|
+
*
|
|
195
|
+
* Backed by an R2 HEAD when the binding exposes one, falling back to a
|
|
196
|
+
* 0-length ranged `get()` otherwise. Prefer this over `download()` whenever
|
|
197
|
+
* only the metadata is wanted — notably to resolve a `Range` header, where a
|
|
198
|
+
* plain `download()` starts a full-object body transfer that is then thrown
|
|
199
|
+
* away. {@link Storage.getMetadata} is the flat, serializable projection of
|
|
200
|
+
* the same read.
|
|
201
|
+
*/
|
|
202
|
+
head: (key: string) => Promise<R2ObjectLike | null>;
|
|
203
|
+
/**
|
|
204
|
+
* List objects under `prefix`. With `options.delimiter` set, keys sharing a
|
|
205
|
+
* segment are rolled up into `delimitedPrefixes` (the "folders") and are NOT
|
|
206
|
+
* in `objects` — a folder browser needs both, so a listing whose `objects` is
|
|
207
|
+
* empty is not an empty directory.
|
|
208
|
+
*
|
|
209
|
+
* A page may hold FEWER objects than `options.limit`: R2 shrinks a page to
|
|
210
|
+
* fit the per-entry metadata this call asks for. Paginate on `truncated` /
|
|
211
|
+
* `cursor`, never on `objects.length === limit`.
|
|
212
|
+
*/
|
|
270
213
|
list: (prefix?: string, options?: ListOptions) => Promise<{
|
|
271
214
|
cursor?: string;
|
|
215
|
+
delimitedPrefixes?: string[];
|
|
272
216
|
objects: R2ObjectLike[];
|
|
273
217
|
truncated?: boolean;
|
|
274
218
|
}>;
|
|
@@ -283,12 +227,20 @@ interface Storage {
|
|
|
283
227
|
* alias for {@link Storage.upload} — it accepts the same {@link UploadOptions}
|
|
284
228
|
* so the `maxSize` / `allowedContentTypes` guards aren't lost behind the alias.
|
|
285
229
|
*/
|
|
286
|
-
store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: UploadOptions) => Promise<{
|
|
230
|
+
store: (key: string, body: ReadableStream | ArrayBuffer | ArrayBufferView | Blob | string, options?: UploadOptions) => Promise<{
|
|
287
231
|
etag: string;
|
|
288
232
|
httpEtag: string;
|
|
289
233
|
key: string;
|
|
290
234
|
}>;
|
|
291
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Upload `body` to `key`, returning the stored key + etag.
|
|
237
|
+
*
|
|
238
|
+
* The accepted shapes mirror what R2's `put` stores bytes from — a view
|
|
239
|
+
* (`Uint8Array`) and a `string` included. They are listed here because
|
|
240
|
+
* `maxSize` measures every one of them; a shape the cap cannot measure is
|
|
241
|
+
* refused rather than uploaded uncapped.
|
|
242
|
+
*/
|
|
243
|
+
upload: (key: string, body: ReadableStream | ArrayBuffer | ArrayBufferView | Blob | string, options?: UploadOptions) => Promise<{
|
|
292
244
|
etag: string;
|
|
293
245
|
httpEtag: string;
|
|
294
246
|
key: string;
|
|
@@ -314,36 +266,32 @@ interface BucketStorage extends Storage {
|
|
|
314
266
|
*
|
|
315
267
|
* ```ts
|
|
316
268
|
* storage: (env) => createBucketStorage({
|
|
317
|
-
* default: createStorage({ bucket: env.FILES }),
|
|
318
|
-
* avatars: createStorage({ bucket: env.AVATARS }),
|
|
269
|
+
* default: createStorage({ bucket: env.FILES, bucketName: "default" }),
|
|
270
|
+
* avatars: createStorage({ bucket: env.AVATARS, bucketName: "avatars" }),
|
|
319
271
|
* }),
|
|
320
272
|
* // → ctx.storage.download(key) // default bucket
|
|
321
273
|
* // → ctx.storage.bucket("avatars").store() // the avatars bucket
|
|
322
274
|
* ```
|
|
323
275
|
*
|
|
324
|
-
* The bare accessor is tagged
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
276
|
+
* The bare accessor is tagged with the name of the binding it actually
|
|
277
|
+
* delegates to: `options.default` when given, else `"default"` when that key
|
|
278
|
+
* exists (the canonical name a `defineStorageRule({ bucket: "default" })` rule
|
|
279
|
+
* and the generated `StorageBucketName` union use), else the first registered
|
|
280
|
+
* bucket. Tag and binding must agree or a `{ bucket: "avatars" }` rule would
|
|
281
|
+
* gate `bucket("avatars").download()` and not the identical bare
|
|
282
|
+
* `ctx.storage.download()` reaching the same R2 bucket. Named buckets are
|
|
283
|
+
* reached with `bucket(name)`.
|
|
330
284
|
*/
|
|
331
285
|
declare const createBucketStorage: (buckets: Record<string, Storage>, options?: {
|
|
332
286
|
default?: string;
|
|
333
287
|
}) => BucketStorage;
|
|
334
|
-
/**
|
|
335
|
-
* Compose a per-tenant key from a scope prefix and a caller-supplied key.
|
|
336
|
-
* Both halves are validated — the prefix may not contain `..` or NUL either,
|
|
337
|
-
* and the resulting key must stay under R2's length ceiling. Recommended for
|
|
338
|
-
* any multi-tenant deployment so client-supplied keys can't address peer data.
|
|
339
|
-
*/
|
|
340
288
|
declare const scopeKey: (prefix: string, key: string) => string;
|
|
341
289
|
declare const createStorage: (options: LunoraStorageOptions) => Storage;
|
|
342
290
|
/** Parameters accepted by {@link buildPresignedUrl}. */
|
|
343
291
|
interface PresignedUrlParams {
|
|
344
292
|
/** R2 S3 API credentials + bucket/account. */
|
|
345
293
|
credentials: R2S3Credentials;
|
|
346
|
-
/** Seconds the URL stays valid
|
|
294
|
+
/** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
|
|
347
295
|
expiresInSeconds?: number;
|
|
348
296
|
/** Object key (path-style; not URL-encoded by the caller). */
|
|
349
297
|
key: string;
|
|
@@ -364,20 +312,33 @@ declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<str
|
|
|
364
312
|
* string carrying `exp` (unix seconds), `method` (`GET` or `PUT`) and `sig`
|
|
365
313
|
* (a base64url HMAC).
|
|
366
314
|
*
|
|
367
|
-
* The HMAC canonical includes the URL host so a signature
|
|
368
|
-
* cannot be replayed against another
|
|
369
|
-
*
|
|
370
|
-
*
|
|
315
|
+
* The HMAC canonical includes the URL host AND the `bucketName`, so a signature
|
|
316
|
+
* minted for one bucket cannot be replayed against another bucket (all buckets
|
|
317
|
+
* of one `.storage()` declaration share a base URL and signing secret) or
|
|
318
|
+
* another host. Even so, the signing secret MUST NOT be shared across
|
|
319
|
+
* tenants — this binding narrows replay surface but is not a substitute for
|
|
320
|
+
* per-tenant key isolation.
|
|
371
321
|
*
|
|
372
|
-
* The Worker handling
|
|
373
|
-
*
|
|
322
|
+
* The Worker route handling the signed download/upload (mounted at
|
|
323
|
+
* `publicBaseUrl`'s origin, e.g. `GET /:key`) should call
|
|
324
|
+
* {@link verifySignedUrl} to validate the signature + expiry before streaming
|
|
325
|
+
* the R2 body. `baseUrl` must be a bare origin (no path) — see the module
|
|
326
|
+
* docstring.
|
|
374
327
|
*/
|
|
375
|
-
declare const buildSignedUrl: (args:
|
|
328
|
+
declare const buildSignedUrl: (args: {
|
|
376
329
|
baseUrl: string;
|
|
330
|
+
/** The bucket the URL addresses — bound into the HMAC and mirrored as `&bucket=`. */
|
|
331
|
+
bucketName: string;
|
|
377
332
|
key: string;
|
|
378
333
|
secret: string;
|
|
379
|
-
}) => Promise<string>;
|
|
334
|
+
} & SignedUrlOptions) => Promise<string>;
|
|
380
335
|
interface VerifyResult {
|
|
336
|
+
/**
|
|
337
|
+
* The bucket the URL was minted for. The serving route MUST resolve its R2
|
|
338
|
+
* binding from this (never from a caller-supplied bucket), since one signing
|
|
339
|
+
* secret covers every bucket of a `.storage()` declaration.
|
|
340
|
+
*/
|
|
341
|
+
bucketName?: string;
|
|
381
342
|
/** The pinned upload `Content-Type` carried by a PUT URL, when present. */
|
|
382
343
|
contentType?: string;
|
|
383
344
|
key?: string;
|
|
@@ -403,4 +364,4 @@ interface VerifyResult {
|
|
|
403
364
|
declare const verifySignedUrl: (input: string | URL, secret: string, options?: {
|
|
404
365
|
expectedHost?: string;
|
|
405
366
|
}) => Promise<VerifyResult>;
|
|
406
|
-
export { type BucketStorage, type ListOptions, type LunoraStorageOptions, type ObjectMetadata, type PresignedUrlOptions, type PresignedUrlParams, type
|
|
367
|
+
export { type BucketStorage, type ListOptions, type LunoraStorageOptions, type ObjectMetadata, type PresignedUrlOptions, type PresignedUrlParams, type R2S3Credentials, type SignedUrlOptions, type Storage, type UploadOptions, type VerifyResult, buildPresignedUrl, buildSignedUrl, createBucketStorage, createStorage, scopeKey, verifySignedUrl };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createBucketStorage as o}from"./packem_shared/createBucketStorage-
|
|
1
|
+
import{createBucketStorage as o}from"./packem_shared/createBucketStorage-AqIMq050.mjs";import{createStorage as i,scopeKey as d}from"./packem_shared/createStorage-YPzdUgGl.mjs";import{buildPresignedUrl as g}from"./packem_shared/buildPresignedUrl-U55ktL8x.mjs";import{buildSignedUrl as p,verifySignedUrl as a}from"./packem_shared/buildSignedUrl-czz1xjLU.mjs";export{g as buildPresignedUrl,p as buildSignedUrl,o as createBucketStorage,i as createStorage,d as scopeKey,a as verifySignedUrl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AsyncLocalStorage as A}from"node:async_hooks";import{LunoraError as y}from"@lunora/errors";import{ERRORS as R,parseTusMetadata as I,File as L}from"@visulima/storage";import{Rest as F,Multipart as P,Tus as _}from"@visulima/storage/handler/http/fetch";const f="1.0.0",g=(t,e)=>Response.json({error:e},{headers:{"content-type":"application/json","Tus-Resumable":f},status:t}),M=t=>{try{return{metadata:Object.fromEntries(Object.entries(I(t)))}}catch(e){return{error:e instanceof Error?e.message:"Invalid Upload-Metadata header"}}},k=t=>{const e=new Headers(t.headers),r=e.get("Tus-Extension");return e.delete("Tus-Checksum-Algorithm"),r!==null&&e.set("Tus-Extension",r.split(",").filter(n=>n.trim()!=="checksum").join(",")),new Response(t.body,{headers:e,status:t.status,statusText:t.statusText})},C={declared:{contentType:void 0,metadata:{}}},D=t=>({check:e=>{if(e.method!=="POST"&&e.method!=="PATCH")return C;const r=M(e.headers.get("Upload-Metadata")??"");return"error"in r?{refusal:g(400,{code:"BadRequestError",message:r.error,name:"BadRequestError"})}:!t&&e.headers.has("Upload-Checksum")?{refusal:g(400,{code:R.UNSUPPORTED_CHECKSUM_ALGORITHM,message:"Upload-Checksum is not supported on this upload route: its storage verifies no checksums",name:"BadRequestError"})}:{declared:{contentType:void 0,metadata:r.metadata}}},finish:(e,r)=>e.method==="OPTIONS"&&!t?k(r):r}),z=100*1024*1024,H=16*1024*1024,S=new WeakMap,O=new A,ce=()=>{const t={get:()=>{const e=O.getStore();if(e?.issuer!==t)throw new y("INTERNAL","@lunora/storage: no upload grant from this UploadContext is active. Read it from a storage callback of a request `authorize` admitted with its grant()");return e.context},grant:e=>{const r=Object.freeze({});return S.set(r,{context:e,issuer:t}),r}};return t},m=(t,e,r)=>{const n={"content-type":"application/json"};return t==="tus"&&(n["Tus-Resumable"]=f),Response.json({error:r},{headers:n,status:e})},h=t=>m(t,403,{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}),w=t=>m(t,413,{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}),N={"chunked-rest":new Set(["HEAD","OPTIONS","PATCH","POST","PUT"]),multipart:new Set(["OPTIONS","POST"]),tus:new Set(["DELETE","HEAD","OPTIONS","PATCH","POST"])},E=(t,e)=>{const r=m(t,405,{code:"METHOD_NOT_ALLOWED",message:e,name:"MethodNotAllowedError"});return r.headers.set("Allow",[...N[t]].join(", ")),r},x=["X-HTTP-Method-Override","X-HTTP-Method","X-Method-Override"],v=/^\d+$/u,j=/\.[^.]+$/u,B=/^[\w-]{1,255}$/u,X=(t,e)=>{const r=(new URL(t.url).pathname.split("/").findLast(Boolean)??"").replace(j,""),n=t.headers.get("Content-Length")?.trim()??"",o=v.test(n)?Number(n):Number.NaN;return B.test(r)&&Number.isSafeInteger(o)&&o<=e.maxUploadSize?r:void 0},G=t=>typeof t=="object"&&t!==null&&t.UploadErrorCode===R.FILE_NOT_FOUND,$=async(t,e)=>{try{return await e.getMeta(t),!0}catch(r){return!G(r)}},b=(t,e)=>{if(e==="multipart")return;let r;for(const n of["Upload-Length","X-Total-Size","Content-Length"]){const o=t.headers.get(n);if(o===null)continue;const s=v.test(o.trim())?Number(o):Number.NaN,a=Number.isSafeInteger(s)?s:Number.POSITIVE_INFINITY;(r===void 0||a>r)&&(r=a)}return r},Y=(t,e,r,n)=>{const{method:o}=t;if(!N[e].has(o))return{refusal:E(e,`${o} is not allowed on this upload route: it is write-only. Serve stored files with ctx.storage.download() or a signed URL`)};const s=x.find(u=>t.headers.has(u));if(s!==void 0)return{refusal:E(e,`${s} is not allowed on this upload route: send the request with the method itself`)};const a=b(t,e);return a!==void 0&&a>r?{refusal:w(e)}:n.check(t)},V=t=>{try{const e=JSON.parse(t??"{}");return e!==null&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}},W={check:t=>{const e=t.headers.get("Content-Type");return{declared:{contentType:e===null||e===""?"application/octet-stream":e,metadata:V(t.headers.get("X-File-Metadata"))}}},finish:(t,e)=>e},J={check:()=>({declared:{contentType:void 0,metadata:{}}}),finish:(t,e)=>e},K=t=>{const e=new L({contentType:t.contentType,metadata:t.metadata}),r={};for(const[n,o]of Object.entries(t.metadata))r[n]=typeof o=="string"?o:JSON.stringify(o);return{contentType:e.contentType,metadata:r}},Q=(t,e)=>e!=="multipart"&&(t.method==="POST"||t.method==="PUT"),Z=async(t,e,r,n,o)=>{const s=b(e.request,e.protocol);let a;try{a=await t({...e,...K(n),declaredSize:s,granted:r})}catch{return h(e.protocol)}if(a!==void 0)return typeof a!="number"||!Number.isFinite(a)||a<0?h(e.protocol):s===void 0||s>Math.min(a,o)?w(e.protocol):void 0},q=(t,e)=>{if(t!=="tus"||e.headers.has("Tus-Resumable"))return e;const r=new Response(e.body,e);return r.headers.set("Tus-Resumable",f),r},ee=async(t,e)=>{let r;try{r=await t(e)}catch{return h(e.protocol)}if(r instanceof Response)return q(e.protocol,r);if(r===!0)return{granted:void 0};const n=typeof r=="object"&&r!==null?S.get(r):void 0;return n===void 0?h(e.protocol):{granted:n}},te=(t,e)=>t==="chunked-rest"?new F(e):t==="multipart"?new P(e):new _(e),re=(t,e)=>t==="tus"?D(e.checksumTypes.length>0):t==="chunked-rest"?W:J,de=t=>{const e=t.protocol??"tus",r=t.maxFileSize??z;if(!Number.isFinite(r)||r<0)throw new y("VALIDATION_ERROR",`@lunora/storage: maxFileSize must be a finite, non-negative number (received ${String(r)})`);const n={allowMethodOverride:!1,disableTerminationForFinishedUploads:!0,maxChecksumBufferSize:H,maxFileSize:r,storage:t.storage},o=te(e,n),s=re(e,t.storage),{authorize:a,maxFileSizeFor:u}=t;a===void 0&&!t.silent&&!t.public&&console.warn("@lunora/storage: createUploadHandler() has no `authorize` — this mounts an unauthenticated, unbounded-write endpoint. Pass an RLS `authorize` gate, or set `public: true` (or `silent: true`) to confirm this bucket is intentionally open.");const U=async(i,d,l,c)=>{if(u!==void 0&&Q(i,e)){const T=await Z(u,d,l?.context,c,r);if(T!==void 0)return T}const p=e==="chunked-rest"&&i.method==="PUT"?X(i,t.storage):void 0;return p!==void 0&&await $(p,t.storage)?m(e,409,{code:"FileConflict",message:"A file already exists under this name",name:"ConflictError"}):s.finish(i,await o.fetch(i))};return{fetch:async i=>{const d=Y(i,e,r,s);if("refusal"in d)return d.refusal;const l={method:i.method,protocol:e,request:i,url:new URL(i.url)},c=a===void 0?{granted:void 0}:await ee(a,l);return c instanceof Response?c:O.run(c.granted,async()=>U(i,l,c.granted,d.declared))},protocol:e}};export{z as DEFAULT_MAX_UPLOAD_BYTES,ce as createUploadContext,de as createUploadHandler};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{N as t,a as o,R as r,o as e,p}from"./r2-upload-part-writer-SdQlWaAu.mjs";export{t as NAME_TAKEN,o as R2UploadPartWriter,r as R2_PART_SIZE,e as objectExists,p as putObject};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{LunoraError as H}from"@lunora/errors";import{b as y,v as R}from"./internal-B5yyxF10.mjs";const $="auto",E="s3",g="AWS4-HMAC-SHA256",x=10080*60,K=900,d=new TextEncoder,z=(t,e)=>t[0]<e[0]?-1:t[0]>e[0]?1:0,i=t=>encodeURIComponent(t).replaceAll(/[!'()*]/gu,e=>`%${e.codePointAt(0)?.toString(16).toUpperCase()??""}`),C=t=>t.split("/").map(e=>i(e)).join("/"),X=async t=>y(await crypto.subtle.digest("SHA-256",d.encode(t))),c=async(t,e)=>{const n=await crypto.subtle.importKey("raw",t,{hash:"SHA-256",name:"HMAC"},!1,["sign"]);return crypto.subtle.sign("HMAC",n,d.encode(e))},O=async(t,e)=>{const n=await c(d.encode(`AWS4${t}`),e),r=await c(n,$),s=await c(r,E);return c(s,"aws4_request")},v=t=>{const e=t.jurisdiction===void 0?"":`${t.jurisdiction}.`;return`${t.accountId}.${e}r2.cloudflarestorage.com`},T=t=>{const e=`${t.toISOString().replaceAll(/[:-]/gu,"").slice(0,15)}Z`;return{amzDate:e,dateStamp:e.slice(0,8)}},M=async t=>{const{credentials:e,key:n}=t,r=t.method??"GET";let s=K;if(t.expiresInSeconds!==void 0){s=Math.floor(t.expiresInSeconds);const o=R(s,x);if(o!==void 0)throw new H("VALIDATION_ERROR",`@lunora/storage: ${o}`)}const l=v(e),w=new Date(t.now?.()??Date.now()),{amzDate:u,dateStamp:S}=T(w),A=`${S}/${$}/${E}/aws4_request`,m=`/${i(e.bucket)}/${C(n)}`,p=[["X-Amz-Algorithm",g],["X-Amz-Credential",`${e.accessKeyId}/${A}`],["X-Amz-Date",u],["X-Amz-Expires",s.toString()],["X-Amz-SignedHeaders","host"]].map(([o,a])=>[i(o),i(a)]).toSorted(z).map(([o,a])=>`${o}=${a}`).join("&"),h=[r,m,p,`host:${l}
|
|
2
|
+
`,"host","UNSIGNED-PAYLOAD"].join(`
|
|
3
|
+
`),I=[g,u,A,await X(h)].join(`
|
|
4
|
+
`),D=await O(e.secretAccessKey,S),f=y(await c(D,I));return`https://${l}${m}?${p}&X-Amz-Signature=${f}`};export{M as buildPresignedUrl};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{LunoraError as f}from"@lunora/errors";import{i as y,c as u,e as p,s as v,t as U,f as N,d as w,v as I,M as $}from"./internal-B5yyxF10.mjs";const k=/^\//,b=(e,s,r,t,o,a)=>{const n=`${e}
|
|
2
|
+
${s.toLowerCase()}
|
|
3
|
+
${r}
|
|
4
|
+
${t}
|
|
5
|
+
${String(o)}`;return a===void 0?n:`${n}
|
|
6
|
+
${a}`},x=async e=>{const s=e.method??"GET",r=e.expiresInSeconds??3600,t=I(r,$);if(t!==void 0)throw new f("VALIDATION_ERROR",`@lunora/storage: ${t}`);const o=s==="PUT"?e.contentType:void 0;let a="";try{a=new URL(e.baseUrl).pathname}catch{}if(a!==""&&!y(a))throw new f("VALIDATION_ERROR",`@lunora/storage: baseUrl must not carry a path ("${a}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`);if(u(e.key),u(e.bucketName),e.bucketName==="")throw new f("VALIDATION_ERROR","@lunora/storage: bucketName must not be empty");if(e.key==="")throw new f("VALIDATION_ERROR","@lunora/storage: key must not be empty");const n=Math.floor(Date.now()/1e3)+r,c=p(e.baseUrl),i=await v(e.secret,b(s,c,e.bucketName,e.key,n,o)),m=U(e.baseUrl),l=e.key.split("/").map(h=>encodeURIComponent(h)).join("/"),d=o===void 0?"":`&ct=${encodeURIComponent(o)}`;return`${m}/${l}?exp=${String(n)}&method=${s}&bucket=${encodeURIComponent(e.bucketName)}&sig=${i}${d}`},E=async(e,s,r)=>{let t;try{t=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const o=t.searchParams.get("exp"),a=o===null?Number.NaN:Number(o),n=t.searchParams.get("sig"),c=t.searchParams.get("method")??"GET",i=t.searchParams.get("bucket"),m=t.searchParams.get("ct")??void 0;if(!n||!i||!Number.isInteger(a))return{reason:"malformed",valid:!1};if(a<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};if(c!=="GET"&&c!=="PUT")return{reason:"malformed",valid:!1};let l,d;try{l=t.pathname.replace(k,"").split("/").map(R=>decodeURIComponent(R)).join("/"),u(l),u(i),d=N(n)}catch{return{reason:"malformed",valid:!1}}const h=r?.expectedHost===void 0?t.host:p(r.expectedHost);return await w(s,b(c,h,i,l,a,m),d)?{bucketName:i,contentType:m,key:l,method:c,valid:!0}:{reason:"bad_signature",valid:!1}};export{x as buildSignedUrl,E as verifySignedUrl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";const g=(e,a={})=>{const o=Object.keys(e),[u]=o;if(u===void 0)throw new n("INTERNAL","@lunora/storage: createBucketStorage requires at least one bucket");if(a.default!==void 0&&!Object.hasOwn(e,a.default))throw new n("INTERNAL",`@lunora/storage: default bucket "${a.default}" is not in the bucket map (have: ${o.join(", ")})`);const t=a.default??(e.default?"default":u),c=e[t];if(c===void 0)throw new n("INTERNAL",`@lunora/storage: default bucket "${t}" is not in the bucket map (have: ${o.join(", ")})`);const s=[...new Set([t,...o])],d=r=>{const f=Object.hasOwn(e,r)?e[r]:void 0,i=r===t?c:f;if(!i)throw new n("INTERNAL",`@lunora/storage: no bucket registered for "${r}". Known buckets: ${s.join(", ")}`);return{...i,bucket:l=>d(l),bucketName:r}};return d(t)};export{g as createBucketStorage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Readable as L}from"node:stream";import{MetaStorage as N,throwErrorCode as c,ERRORS as n,AbstractBaseStorage as g,File as y,UploadError as _}from"@visulima/storage";import{o as A,p as x,r as E,a as I,N as T}from"./r2-upload-part-writer-SdQlWaAu.mjs";import{R as G}from"./r2-upload-part-writer-SdQlWaAu.mjs";const k=5,S=["bytesWritten","ETag","id","name","size","status"],D=(d,t)=>JSON.stringify({...d,modifiedAt:void 0})===JSON.stringify({...t,modifiedAt:void 0});class U extends N{bucket;scope;constructor(t,e,a=()=>""){super({prefix:e,suffix:".json"}),this.bucket=t,this.scope=a}currentPrefix(){const t=this.scope();if(t!==""&&!t.endsWith("/"))throw new TypeError(`@lunora/storage: stateScope must end in "/" (got "${t}")`);return`${this.prefix}${t}`}getMetaName(t){return`${this.currentPrefix()}${t}${this.suffix}`}segmentPrefix(t){return`${this.currentPrefix()}${t}/`}async read(t){const e=await this.bucket.get(this.getMetaName(t));if(e===null)return;const a=await e.text();return{etag:e.etag,state:JSON.parse(a)}}async create(t,e){return await this.bucket.put(this.getMetaName(t),JSON.stringify(e),{onlyIf:{etagDoesNotMatch:"*"}})!==null}async write(t,e,a){return(await this.bucket.put(this.getMetaName(t),JSON.stringify(e),{onlyIf:{etagMatches:a}}))?.etag}async swap(t,e){for(let a=0;a<k;a+=1){const r=await this.read(t);if(r===void 0)return c(n.FILE_NOT_FOUND);const s=e(r.state);if(s===r.state||await this.write(t,s,r.etag)!==void 0)return s}return c(n.FILE_CONFLICT,"The upload kept changing while it was being written; resume from the current offset")}async get(t){const e=await this.read(t);return e===void 0?c(n.FILE_NOT_FOUND):e.state.file}async save(t,e){return(await this.swap(t,r=>{const i={...e};for(const o of S)i[o]=r.file[o];return D(i,r.file)?r:{...r,file:i}})).file}async delete(t){await this.bucket.delete(this.getMetaName(t))}}const F="_lunora/uploads/",m=6e4,R=new Set(["checksum","concatenation","creation-defer-length"]),M=new Set([...S,"expiredAt"]),p=d=>c(n.FILE_CONFLICT,d),P=d=>d!==void 0&&d.expiresAt>Date.now(),b=32*1024*1024;class j extends g{sequentialWrites=!0;meta;bucket;constructor(t,e={}){super(e),this.bucket=t,this.meta=new U(t,e.statePrefix??F,e.stateScope)}get tusExtension(){return super.tusExtension.filter(t=>!R.has(t))}async create(t){return this.instrumentOperation("create",async()=>{const e=t.size===void 0||t.size===""?Number.NaN:Number(t.size);if(!Number.isSafeInteger(e)||e<0)return c(n.INVALID_FILE_SIZE,"An upload must declare its size as a non-negative integer");const a=new y(t);if(a.size=e,a.name=this.namingFunction(a),g.assertSafeId(a.id),a.name.includes(this.meta.prefix))return c(n.INVALID_FILE_NAME,"An upload cannot be stored under the upload state prefix");await this.validate(a);const r=t.id!==void 0;if(r&&(await this.meta.read(a.id)!==void 0||await A(this.bucket,a.name)))return p(T);if(a.bytesWritten=0,a.status=e===0?"completed":"created",this.updateTimestamps(a),!await this.meta.create(a.id,{file:a,upload:{parts:[],segments:[],...r?{createOnly:r}:{}}}))return p(T);if(e===0)try{await x(this.bucket,a.name,new Uint8Array(0),a.contentType,r)}catch(s){throw await this.deleteMeta(a.id).catch(()=>{}),s}return await this.onCreate(a),a})}async write(t){return this.instrumentOperation("write",async()=>{const e=await this.meta.read(t.id);if(e===void 0)return c(n.FILE_NOT_FOUND);const a=await this.checkIfExpired(e.state.file);if(a.status==="completed"||!("body"in t)||!("start"in t))return a;this.checkPart(a,t);const r=await this.acquireLease(a.id,e.state,e.etag);try{return await this.writeLeased(a,e.state.upload,t.body,r)}catch(s){throw await this.releaseLease(a.id,r).catch(()=>{}),s}})}async get({id:t}){return this.instrumentOperation("get",async()=>{const{file:e,object:a}=await this.completedObject(t);if(a.size>b)return await a.body?.cancel(),c(n.REQUEST_ENTITY_TOO_LARGE,`get() buffers the whole file and refuses one over ${String(b)} bytes; use getStream(), ctx.storage.download() or a signed URL`);const r=Buffer.from(await a.arrayBuffer());return{content:r,contentType:e.contentType,ETag:a.etag,expiredAt:e.expiredAt,id:t,metadata:e.metadata,modifiedAt:e.modifiedAt,name:e.name,originalName:e.originalName,size:r.byteLength}})}async getStream({id:t}){return this.instrumentOperation("getStream",async()=>{const{file:e,object:a}=await this.completedObject(t),r=L.from(E(a.body));return{headers:{"Content-Length":String(a.size),"Content-Type":e.contentType,ETag:a.etag},size:a.size,stream:r}})}async delete({id:t}){return this.instrumentOperation("delete",async()=>{const e=await this.meta.read(t);if(e===void 0)return c(n.FILE_NOT_FOUND);const{file:a,upload:r}=e.state;a.status!=="completed"&&(await this.acquireLease(t,e.state,e.etag),r.uploadId!==void 0&&await this.bucket.resumeMultipartUpload(a.name,r.uploadId).abort().catch(()=>{}),await this.deleteSegments(t)),await this.deleteMeta(t);const s={...a,status:"deleted"};return await this.onDelete(s),s})}async update(t,e){const a=Object.fromEntries(Object.entries(e).filter(([r])=>!M.has(r)));return super.update(t,a)}async list(){return this.instrumentOperation("list",async()=>{const{prefix:t,suffix:e}=this.meta,a=[];let r;do{const s=await this.bucket.list({delimiter:"/",prefix:t,...r===void 0?{}:{cursor:r}});for(const i of s.objects)if(i.key.endsWith(e)){const o=new y({id:this.meta.getIdFromMetaName(i.key),metadata:{}});o.createdAt=(i.uploaded??new Date).toISOString(),o.modifiedAt=o.createdAt,a.push(o)}r=s.truncated===!0?s.cursor:void 0}while(r!==void 0);return a})}copy(){return this.unsupported("copy")}move(){return this.unsupported("move")}async finish(t,e,a){try{return await t.finish()}catch(r){throw a&&r.UploadErrorCode===n.FILE_CONFLICT&&(await this.deleteSegments(e).catch(()=>{}),await this.deleteMeta(e).catch(()=>{})),r}}async completedObject(t){const e=await this.checkIfExpired(await this.getMeta(t)),a=e.status==="completed"?await this.bucket.get(e.name):void 0;return a==null?c(n.FILE_NOT_FOUND):{file:e,object:a}}unsupported(t){return Promise.reject(new _(n.METHOD_NOT_ALLOWED,`${this.constructor.name} does not implement ${t}()`))}checkPart(t,e){e.start!==t.bytesWritten&&p(`Upload-Offset ${String(e.start)} does not match the upload's offset ${String(t.bytesWritten)}`),e.checksumAlgorithm!==void 0&&e.checksumAlgorithm!==""&&c(n.UNSUPPORTED_CHECKSUM_ALGORITHM),e.contentLength!==void 0&&e.start+e.contentLength>(t.size??0)&&c(n.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length")}async acquireLease(t,e,a){if(P(e.upload.lock))return p("Another request is writing to this upload");const r=crypto.randomUUID(),s={...e.upload,lock:{expiresAt:Date.now()+m,token:r}};return await this.meta.write(t,{...e,upload:s},a)===void 0?p("Another request is writing to this upload"):r}async changeLeased(t,e,a){return this.meta.swap(t,r=>r.upload.lock?.token!==e?p("This request's lease on the upload expired; resume from the current offset"):a(r))}async releaseLease(t,e){await this.changeLeased(t,e,a=>{const r={...a.upload};return delete r.lock,{...a,upload:r}})}async renewLease(t,e){await this.changeLeased(t,e,a=>({...a,upload:{...a.upload,lock:{expiresAt:Date.now()+m,token:e}}}))}async deleteSegments(t){const e=[];let a;do{const r=await this.bucket.list({prefix:this.meta.segmentPrefix(t),...a===void 0?{}:{cursor:a}});e.push(...r.objects.map(s=>s.key)),a=r.truncated===!0?r.cursor:void 0}while(a!==void 0);for(let r=0;r<e.length;r+=1e3)await this.bucket.delete(e.slice(r,r+1e3))}async pump(t,e,a,r,s){const i=E(e);let o=0,u=Date.now();try{for(;;){let h;try{h=await i.next()}catch(l){return l instanceof Error?l:new Error(String(l))}if(h.done===!0)return;if(o+=h.value.byteLength,o>a)return c(n.REQUEST_ENTITY_TOO_LARGE,"The chunk runs past the declared upload length");Date.now()-u>=m/3&&(u=Date.now(),await this.renewLease(r,s)),await t.push(h.value)}}finally{await i.return(void 0).catch(()=>{})}}async writeLeased(t,e,a,r){const s=t.size??0,i=new I(this.bucket,t,e,this.meta.segmentPrefix(t.id),{confirm:async()=>this.renewLease(t.id,r),save:async(f,v)=>{await this.changeLeased(t.id,r,w=>({file:{...w.file,bytesWritten:v,status:"part"},upload:{...f,lock:w.upload.lock}}))},token:r}),o=await this.pump(i,a,s-t.bytesWritten,t.id,r),u=i.offset===s,h=u?await this.finish(i,t.id,e.createOnly===!0):void 0;if(!u)try{await i.keepTail()}catch(f){if(o===void 0)throw f}const l={bytesWritten:u?s:i.stored,modifiedAt:new Date().toISOString(),status:u?"completed":"part",...h===void 0?{}:{ETag:h}},O=u?{parts:[],segments:[]}:i.progress();if(await this.changeLeased(t.id,r,f=>({file:{...f.file,...l},upload:O})),await(u?this.deleteSegments(t.id).catch(()=>{}):i.dropConsumed()),o!==void 0)throw o;return{...t,...l}}}const $=(d,t={})=>new j(d,t);export{G as R2_PART_SIZE,$ as createR2BindingUploadStorage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";import{AwsLightStorage as s}from"@visulima/storage/provider/aws-light";const n=new Set(["127.0.0.1","[::1]","localhost"]),h=t=>{let e;try{e=new URL(t)}catch{e=void 0}if(e?.protocol!=="https:"&&!(e?.protocol==="http:"&&n.has(e.hostname)))throw new c("VALIDATION_ERROR",`@lunora/storage: createR2UploadStorage's endpoint must be an absolute https:// URL such as https://<account>.r2.cloudflarestorage.com, or http:// to localhost (received ${JSON.stringify(t)})`);return e},u=t=>{const e=t.endpoint===void 0?new URL(`https://${t.accountId}.r2.cloudflarestorage.com`):h(t.endpoint),o=encodeURIComponent(t.bucket),r=e.pathname.split("/").filter(a=>a!=="");return!(t.endpoint!==void 0&&e.hostname.startsWith(`${t.bucket.toLowerCase()}.`))&&r.at(-1)!==o&&r.push(o),e.pathname=r.length===0?"/":`/${r.join("/")}/`,e.href},i=t=>new s({accessKeyId:t.accessKeyId,bucket:t.bucket,endpoint:u(t),path:t.path??"/",region:"auto",secretAccessKey:t.secretAccessKey,...t.partSize===void 0?{}:{partSize:t.partSize}});export{i as createR2UploadStorage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";import{t as U,h as M,a as I,b as f}from"./internal-B5yyxF10.mjs";import{buildPresignedUrl as S}from"./buildPresignedUrl-U55ktL8x.mjs";import{buildSignedUrl as _}from"./buildSignedUrl-czz1xjLU.mjs";const w=1024,v=1e3,k=100,A=16*1024*1024,D=e=>{const a=e.checksums?.sha256;if(a===void 0)return e;const c=f(a),l=I(new Uint8Array(a));return new Proxy(e,{get(u,s){if(s==="sha256")return c;if(s==="sha256Base64")return l;const d=Reflect.get(u,s,u);return typeof d=="function"?d.bind(u):d},has(u,s){return s==="sha256"||s==="sha256Base64"||Reflect.has(u,s)}})},B=e=>{const a=e.checksums?.sha256;return{contentType:e.httpMetadata?.contentType,customMetadata:e.customMetadata,key:e.key,sha256:a===void 0?void 0:f(a),size:e.size,uploaded:e.uploaded===void 0?void 0:e.uploaded.getTime()}},y=e=>{const a=e.checksums?.sha256;return{checksums:e.checksums,customMetadata:e.customMetadata,etag:e.etag,httpEtag:e.httpEtag,httpMetadata:e.httpMetadata,key:e.key,sha256:a===void 0?void 0:f(a),sha256Base64:a===void 0?void 0:I(new Uint8Array(a)),size:e.size,uploaded:e.uploaded}},O=async(e,a)=>{let c=0;const l=s=>s instanceof ArrayBuffer?new Uint8Array(s):ArrayBuffer.isView(s)?new Uint8Array(s.buffer,s.byteOffset,s.byteLength):void 0,u=new TransformStream({transform(s,d){const m=l(s);if(m===void 0){d.error(new n("VALIDATION_ERROR","@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(c+=m.byteLength,c>a){d.error(new n("PAYLOAD_TOO_LARGE",`@lunora/storage: stream body exceeds maxSize (> ${String(a)} bytes)`));return}d.enqueue(m)}});return await new Response(e.pipeThrough(u)).blob()},V=new TextEncoder,R=e=>V.encode(e).length,x=e=>{if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength;if(typeof e=="string")return R(e);if(typeof e!="object"||e===null)return;const{size:a}=e;return typeof a=="number"?a:void 0},$=async(e,a)=>{if(!Number.isFinite(a)||a<0)throw new n("VALIDATION_ERROR",`@lunora/storage: maxSize must be a finite, non-negative number (received ${String(a)})`);if(e instanceof ReadableStream){if(a>A)throw new n("VALIDATION_ERROR",`@lunora/storage: maxSize ${String(a)} exceeds the ${String(A)}-byte ceiling for a streamed body, which must be buffered to be capped — use createMultipartUpload() or createUploadHandler() for objects this large`);return await O(e,a)}const c=x(e);if(c===void 0)throw new n("VALIDATION_ERROR","@lunora/storage: cannot measure body length, so maxSize cannot be enforced");if(c>a)throw new n("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(c)} > ${String(a)})`);return e},i=e=>{if(typeof e!="string"||e.length===0)throw new n("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(R(e)>w)throw new n("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(w)}-byte limit`);if(e.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(M(e))throw new n("VALIDATION_ERROR","@lunora/storage: key contains a control character (including CR/LF)");if(e.startsWith("/"))throw new n("VALIDATION_ERROR","@lunora/storage: key must not start with `/`");if(e.split("/").includes(".."))throw new n("VALIDATION_ERROR","@lunora/storage: key contains a `..` path component")},C=e=>{if(e.allowedContentTypes!==void 0){if(e.contentType===void 0)throw new n("VALIDATION_ERROR","@lunora/storage: contentType is required when allowedContentTypes is set");if(!e.allowedContentTypes.includes(e.contentType))throw new n("VALIDATION_ERROR",`@lunora/storage: contentType "${e.contentType}" not in allowedContentTypes`)}},F=(e,a)=>{i(e),i(a);const l=`${e.endsWith("/")?e.slice(0,-1):e}/${a}`;if(R(l)>w)throw new n("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(w)}-byte limit`);return l},X=e=>{if(!e.bucket)throw new n("INTERNAL","@lunora/storage: `bucket` is required");if(typeof e.bucketName!="string"||e.bucketName==="")throw new n("INTERNAL",'@lunora/storage: `bucketName` is required — pass the name this bucket is registered under (`"default"` for a single-bucket app)');const a=async(r,t,o={})=>{i(r),C(o);const g=o.maxSize===void 0?t:await $(t,o.maxSize),h=await e.bucket.put(r,g,{customMetadata:o.customMetadata,httpMetadata:o.contentType?{contentType:o.contentType}:void 0,...o.sha256===void 0?{}:{sha256:o.sha256}});return{etag:h.etag,httpEtag:h.httpEtag??`"${h.etag}"`,key:h.key}},c=async(r,t={})=>{i(r);const o=await(t.range?e.bucket.get(r,{range:t.range}):e.bucket.get(r));return o&&D(o)},l=async r=>{i(r),await e.bucket.delete(r)},u=async r=>{i(r);let t;if(e.bucket.head)t=await e.bucket.head(r);else{const o=await e.bucket.get(r);await o?.body?.cancel(),t=o}return t&&y(t)},s=async r=>{const t=await u(r);return t&&B(t)},d=async(r,t={})=>{if(r?.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");if(t.limit!==void 0&&(!Number.isInteger(t.limit)||t.limit<=0))throw new n("VALIDATION_ERROR",`@lunora/storage: list limit must be a positive integer (received ${String(t.limit)})`);const o=Math.min(t.limit??k,v),g=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,include:["customMetadata","httpMetadata"],limit:o,prefix:r}),{delimitedPrefixes:h}=g;return{cursor:g.cursor,delimitedPrefixes:h,objects:g.objects.map(E=>y(E)),truncated:g.truncated}},m=r=>{if(!e.publicBaseUrl)throw new n("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");i(r);const t=r.split("/").map(o=>encodeURIComponent(o)).join("/");return`${U(e.publicBaseUrl)}/${t}`},T=async(r,t={})=>{if(!e.publicBaseUrl)throw new n("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");if(!e.signingSecret)throw new n("INTERNAL","@lunora/storage: `signingSecret` is required for getSignedUrl()");return i(r),_({baseUrl:e.publicBaseUrl,bucketName:e.bucketName,contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method,secret:e.signingSecret})},b=async(r,t={})=>{if(i(r),!e.bucket.createMultipartUpload)throw new n("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");return e.bucket.createMultipartUpload(r,{customMetadata:t.customMetadata,httpMetadata:t.contentType?{contentType:t.contentType}:void 0})},N=(r,t)=>{if(i(r),typeof t!="string"||t.length===0)throw new n("VALIDATION_ERROR","@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");if(!e.bucket.resumeMultipartUpload)throw new n("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");return e.bucket.resumeMultipartUpload(r,t)},L=async(r,t={})=>{if(!e.s3)throw new n("INTERNAL","@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }");return i(r),S({credentials:e.s3,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method})},p=async(r,t={})=>T(r,{contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,method:"PUT"});return{bucketName:e.bucketName,createMultipartUpload:b,delete:l,download:c,generateUploadUrl:p,getMetadata:s,getPresignedUrl:L,getSignedUrl:T,getUrl:m,head:u,list:d,resumeMultipartUpload:N,store:a,upload:a}};export{X as createStorage,F as scopeKey};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const p=t=>{let n="";for(let r=0;r<t.length;r+=32768)n+=String.fromCharCode(...t.subarray(r,r+32768));return btoa(n)},d=t=>{const n=atob(t),e=new Uint8Array(n.length);for(let r=0;r<n.length;r+=1)e[r]=n.codePointAt(r)??0;return e},s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",u=t=>{let n="",e=0;const r=t.length-2;for(;e<r;e+=3){const o=t[e]<<16|t[e+1]<<8|t[e+2];n+=s.charAt(o>>18&63)+s.charAt(o>>12&63)+s.charAt(o>>6&63)+s.charAt(o&63)}const c=t.length-e;if(c===1){const o=t[e]<<16;n+=s.charAt(o>>18&63)+s.charAt(o>>12&63)}else if(c===2){const o=t[e]<<16|t[e+1]<<8;n+=s.charAt(o>>18&63)+s.charAt(o>>12&63)+s.charAt(o>>6&63)}return n},_=t=>{const n=t.replaceAll("-","+").replaceAll("_","/"),e=n+"=".repeat((4-n.length%4)%4);return d(e)},A=(t,n)=>{if(t.size<n)return;const e=t.keys().next().value;e!==void 0&&t.delete(e)},f=(t,n,e,r)=>{const c=t.get(n);if(c!==void 0)return c;A(t,r);const o=e().catch(h=>{throw t.get(n)===o&&t.delete(n),h});return t.set(n,o),o},a=new TextEncoder,w=10080*60,g=/^[a-z][a-z0-9+\-.]*:\/\//i,y=Array.from({length:32},(t,n)=>n),i=new RegExp(`[${y.map(t=>String.fromCodePoint(t)).join("")}]`,"u"),C=64,S=new Map,l=async t=>f(S,t,async()=>crypto.subtle.importKey("raw",a.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]),C),m=t=>{try{return new URL(t).host}catch{return t.replace(g,"").split("/")[0]??""}},H=t=>{if(i.test(t))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},R=t=>i.test(t),E=/^\/+$/u,x=t=>E.test(t),L=(t,n)=>{if(!Number.isFinite(t)||t<=0)return"expiresInSeconds must be a positive finite number";if(t>n){const e=` (${String(n/86400)} days)`;return`expiresInSeconds must not exceed ${String(n)} seconds${e}`}},T=async(t,n)=>{const e=await l(t),r=await crypto.subtle.sign("HMAC",e,a.encode(n));return u(new Uint8Array(r))},O=async(t,n,e)=>{const r=await l(t);return crypto.subtle.verify("HMAC",r,e,a.encode(n))},v=t=>{const n=new Uint8Array(t);let e="";for(const r of n)e+=r.toString(16).padStart(2,"0");return e},M=t=>{let n=t.length;for(;n>0&&t[n-1]==="/";)n-=1;return t.slice(0,n)};export{w as M,p as a,v as b,H as c,O as d,m as e,_ as f,R as h,x as i,T as s,M as t,L as v};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{throwErrorCode as c,ERRORS as u}from"@visulima/storage";const d=i=>{if(i instanceof Uint8Array)return i;if(typeof i=="string")return new TextEncoder().encode(i);if(ArrayBuffer.isView(i))return new Uint8Array(i.buffer,i.byteOffset,i.byteLength);if(i instanceof ArrayBuffer)return new Uint8Array(i);throw new TypeError("Unsupported upload body chunk")},l=i=>typeof i.getReader=="function",b=async function*(t){if(t!=null){if(t instanceof Uint8Array){yield t;return}if(typeof t=="object"&&l(t)){const e=t.getReader();try{for(;;){const{done:s,value:n}=await e.read();if(s)return;yield d(n)}}finally{e.releaseLock()}}if(typeof t=="object"&&Symbol.asyncIterator in t){for await(const e of t)yield d(e);return}throw new TypeError("Unsupported upload body")}};class f{size=0;chunks=[];push(t){t.byteLength>0&&(this.chunks.push(t),this.size+=t.byteLength)}copyFront(t,e,s){let n=0;for(const a of this.chunks){if(n>=s)break;const r=Math.min(a.byteLength,s-n);t.set(a.subarray(0,r),e+n),n+=r}}drop(t){let e=t;for(;e>0;){const s=this.chunks[0];if(s===void 0)break;s.byteLength<=e?(this.chunks.shift(),e-=s.byteLength):(this.chunks[0]=s.subarray(e),e=0)}this.size-=t-e}}const o=5*1024*1024,g=8,h=i=>i.reduce((t,e)=>t+e.size,0),p="A file already exists under this name",m=async(i,t,e,s,n)=>n?await i.put(t,e,{httpMetadata:{contentType:s},onlyIf:{etagDoesNotMatch:"*"}})??c(u.FILE_CONFLICT,p):i.put(t,e,{httpMetadata:{contentType:s}}),y=async(i,t)=>{if(i.head!==void 0)return await i.head(t)!==null;const e=await i.get(t);return await e?.body?.cancel(),e!==null};class I{parts;segments;uploadId;bucket;createOnly;consumed=[];file;lease;pending=new f;segmentPrefix;constructor(t,e,s,n,a){this.bucket=t,this.file=e,this.parts=[...s.parts],this.segments=[...s.segments],this.uploadId=s.uploadId,this.createOnly=s.createOnly===!0,this.segmentPrefix=n,this.lease=a}get stored(){return this.parts.length*o+h(this.segments)}get offset(){return this.stored+this.pending.size}progress(){return{parts:[...this.parts],segments:[...this.segments],...this.uploadId===void 0?{}:{uploadId:this.uploadId},...this.createOnly?{createOnly:!0}:{}}}async push(t){for(this.pending.push(t);h(this.segments)+this.pending.size>=o;)await this.flushPart()}async keepTail(){if(this.pending.size===0)return;const t=this.segments.length+1>g,e=t?this.parts.length*o:this.stored,s=t?h(this.segments)+this.pending.size:this.pending.size,n=t?await this.assemble(s):this.front(s),a=`${this.segmentPrefix}${String(e).padStart(16,"0")}-${this.lease.token}`;await this.bucket.put(a,n),t&&this.consumeSegments(),this.segments=[...this.segments,{key:a,size:s}],this.pending.drop(this.pending.size)}async finish(){const t=await this.assemble(h(this.segments)+this.pending.size);await this.lease.confirm();let e;if(this.uploadId===void 0)e=(await m(this.bucket,this.file.name,t,this.file.contentType,this.createOnly)).etag;else{if(this.createOnly&&await y(this.bucket,this.file.name))return await this.multipart(this.uploadId).abort().catch(()=>{}),c(u.FILE_CONFLICT,p);const s=this.multipart(this.uploadId);if(t.byteLength>0){const a=this.parts.length+1,r=await s.uploadPart(a,t);this.parts.push({etag:r.etag,partNumber:a})}e=(await s.complete(this.parts)).etag}return this.consumeSegments(),this.pending.drop(this.pending.size),e}async dropConsumed(){const t=this.consumed;this.consumed=[],t.length>0&&await this.bucket.delete(t).catch(()=>{})}async assemble(t){const e=new Uint8Array(t);let s=0;for(const n of this.segments){const a=await this.bucket.get(n.key);if(a===null)return c(u.STORAGE_ERROR,`Buffered upload segment ${n.key} is missing`);const r=new Uint8Array(await a.arrayBuffer());e.set(r,s),s+=r.byteLength}return this.pending.copyFront(e,s,t-s),e}front(t){const e=new Uint8Array(t);return this.pending.copyFront(e,0,t),e}consumeSegments(){this.consumed.push(...this.segments.map(t=>t.key)),this.segments=[]}multipart(t){return this.bucket.resumeMultipartUpload(this.file.name,t)}async flushPart(){const t=h(this.segments),e=await this.assemble(o);if(await this.lease.confirm(),this.uploadId===void 0){const a=await this.bucket.createMultipartUpload(this.file.name,{httpMetadata:{contentType:this.file.contentType}});this.uploadId=a.uploadId}const s=this.parts.length+1,n=await this.multipart(this.uploadId).uploadPart(s,e);this.parts.push({etag:n.etag,partNumber:s}),this.pending.drop(o-t),this.consumeSegments(),await this.lease.save(this.progress(),this.stored),await this.dropConsumed()}}export{p as N,o as R,I as a,y as o,m as p,b as r};
|