@stowage/adapter-fs 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1268 @@
1
+ import { copyFile, lstat, mkdir, open, readdir, realpath, rename, rmdir, stat, unlink } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, sep } from "node:path";
3
+ import { StorageError, invalidKeyReason, isStorageError, lastByteOf, rangeBoundsRefusal, rangeStartRefusal } from "@stowage/core";
4
+ //#region src/body.ts
5
+ const utf8$2 = new TextEncoder();
6
+ /**
7
+ * Writes the body into the open file. A stream goes chunk by chunk, so that an upload of
8
+ * any size costs one chunk of memory rather than the object.
9
+ */
10
+ async function writeBody(handle, body, signal) {
11
+ if (isStream(body)) {
12
+ await body.pipeTo(new WritableStream({ async write(chunk) {
13
+ await writeAll(handle, chunk);
14
+ } }), { signal });
15
+ return;
16
+ }
17
+ signal?.throwIfAborted();
18
+ await writeAll(handle, typeof body === "string" ? utf8$2.encode(body) : body);
19
+ }
20
+ /** Spec 4.2 leaves a stream at its end or canceled once `put` settled, read or not. */
21
+ async function cancelBody(body, reason) {
22
+ if (!isStream(body) || body.locked) return;
23
+ await body.cancel(reason).catch(() => {});
24
+ }
25
+ function isStream(body) {
26
+ return typeof body !== "string" && !(body instanceof Uint8Array);
27
+ }
28
+ async function writeAll(handle, bytes) {
29
+ for (let written = 0; written < bytes.byteLength;) {
30
+ const { bytesWritten } = await handle.write(bytes, written, bytes.byteLength - written);
31
+ written += bytesWritten;
32
+ }
33
+ }
34
+ //#endregion
35
+ //#region src/content-type.ts
36
+ const defaultContentType = "application/octet-stream";
37
+ /**
38
+ * The table spec 6 has the adapter derive a content type from, kept to the types a file
39
+ * below a root is likely to carry: nothing here reads the bytes, and no dependency comes
40
+ * into a package that promises a runtime dependency outside `@stowage` for nothing.
41
+ */
42
+ const contentTypes = /* @__PURE__ */ new Map([
43
+ ["avif", "image/avif"],
44
+ ["css", "text/css"],
45
+ ["csv", "text/csv"],
46
+ ["gif", "image/gif"],
47
+ ["gz", "application/gzip"],
48
+ ["htm", "text/html"],
49
+ ["html", "text/html"],
50
+ ["ico", "image/vnd.microsoft.icon"],
51
+ ["jpeg", "image/jpeg"],
52
+ ["jpg", "image/jpeg"],
53
+ ["js", "text/javascript"],
54
+ ["json", "application/json"],
55
+ ["md", "text/markdown"],
56
+ ["mjs", "text/javascript"],
57
+ ["mp3", "audio/mpeg"],
58
+ ["mp4", "video/mp4"],
59
+ ["otf", "font/otf"],
60
+ ["pdf", "application/pdf"],
61
+ ["png", "image/png"],
62
+ ["svg", "image/svg+xml"],
63
+ ["tar", "application/x-tar"],
64
+ ["ttf", "font/ttf"],
65
+ ["txt", "text/plain"],
66
+ ["wasm", "application/wasm"],
67
+ ["wav", "audio/wav"],
68
+ ["webm", "video/webm"],
69
+ ["webp", "image/webp"],
70
+ ["woff", "font/woff"],
71
+ ["woff2", "font/woff2"],
72
+ ["xml", "application/xml"],
73
+ ["yaml", "application/yaml"],
74
+ ["yml", "application/yaml"],
75
+ ["zip", "application/zip"]
76
+ ]);
77
+ /** The type the key's extension names, and `application/octet-stream` for every other key. */
78
+ function contentTypeOf(key) {
79
+ const name = key.slice(key.lastIndexOf("/") + 1);
80
+ const dot = name.lastIndexOf(".");
81
+ if (dot < 1) return defaultContentType;
82
+ return contentTypes.get(name.slice(dot + 1).toLowerCase()) ?? "application/octet-stream";
83
+ }
84
+ //#endregion
85
+ //#region src/directory-lock.ts
86
+ const directoryMutationLocks = /* @__PURE__ */ new Map();
87
+ const doNothing = () => {};
88
+ async function holdDirectoryMutations(realRoot) {
89
+ const lock = directoryMutationLocks.get(realRoot) ?? {
90
+ tail: Promise.resolve(),
91
+ leases: 0
92
+ };
93
+ const previous = lock.tail;
94
+ let releaseNext = doNothing;
95
+ lock.tail = new Promise((resolve) => {
96
+ releaseNext = resolve;
97
+ });
98
+ lock.leases += 1;
99
+ directoryMutationLocks.set(realRoot, lock);
100
+ await previous;
101
+ let held = true;
102
+ return () => {
103
+ if (!held) return;
104
+ held = false;
105
+ lock.leases -= 1;
106
+ releaseNext();
107
+ if (lock.leases === 0 && directoryMutationLocks.get(realRoot) === lock) directoryMutationLocks.delete(realRoot);
108
+ };
109
+ }
110
+ /** Runs the step with no directory below the root created or removed by anyone else. */
111
+ async function withDirectoryMutations(realRoot, step) {
112
+ const release = await holdDirectoryMutations(realRoot);
113
+ try {
114
+ return await step();
115
+ } finally {
116
+ release();
117
+ }
118
+ }
119
+ //#endregion
120
+ //#region src/cursor.ts
121
+ const cursorTag = "stowage-fs-1:";
122
+ const unitDigits = 4;
123
+ const hexPosition = new RegExp(`^(?:[\\da-f]{${unitDigits}})*$`);
124
+ /** The key a page ended on, as the opaque string that continues from there. */
125
+ function encodeCursor(after) {
126
+ return btoa(`${cursorTag}${hexOf(after)}`);
127
+ }
128
+ /** The key the cursor continues from, or `undefined` for one this adapter did not produce. */
129
+ function decodeCursor(cursor) {
130
+ let decoded;
131
+ try {
132
+ decoded = atob(cursor);
133
+ } catch {
134
+ return;
135
+ }
136
+ if (!decoded.startsWith(cursorTag)) return void 0;
137
+ const position = decoded.slice(13);
138
+ return position.length > 0 && hexPosition.test(position) ? keyOf(position) : void 0;
139
+ }
140
+ function hexOf(key) {
141
+ const units = [];
142
+ for (let index = 0; index < key.length; index += 1) units.push(key.charCodeAt(index).toString(16).padStart(unitDigits, "0"));
143
+ return units.join("");
144
+ }
145
+ function keyOf(position) {
146
+ const units = [];
147
+ for (let index = 0; index < position.length; index += unitDigits) units.push(String.fromCharCode(Number.parseInt(position.slice(index, index + unitDigits), 16)));
148
+ return units.join("");
149
+ }
150
+ //#endregion
151
+ //#region src/storage-error.ts
152
+ /** `bucket` is the root as the storage was given it, which spec 6 names as the bucket. */
153
+ function fsError(root, fields) {
154
+ return new StorageError({
155
+ ...fields,
156
+ bucket: root,
157
+ provider: "fs"
158
+ });
159
+ }
160
+ //#endregion
161
+ //#region src/temporary.ts
162
+ const temporaryName = /^\.stowage-[\da-f-]{36}\.tmp$/;
163
+ /** Where the bytes of a write land before the rename that puts them under the key. */
164
+ function temporaryPathIn(directory) {
165
+ return join(directory, `.stowage-${crypto.randomUUID()}.tmp`);
166
+ }
167
+ function isTemporaryName(name) {
168
+ return temporaryName.test(name);
169
+ }
170
+ //#endregion
171
+ //#region src/key.ts
172
+ /** What a file system takes as one name, which spec 6 has the adapter refuse beyond. */
173
+ const segmentByteLimit = 255;
174
+ const utf8$1 = new TextEncoder();
175
+ function requireKey(root, key, rule, operation) {
176
+ const reason = invalidKeyReason(key, rule) ?? temporaryNameReason(key, rule) ?? longSegmentReason(key);
177
+ if (reason === void 0) return;
178
+ throw fsError(root, {
179
+ code: "InvalidKey",
180
+ message: `The key ${JSON.stringify(key)} ${reason}`,
181
+ operation,
182
+ key,
183
+ attempts: 0
184
+ });
185
+ }
186
+ function temporaryNameReason(key, rule) {
187
+ if (rule !== "writable" || !isTemporaryName(key.slice(key.lastIndexOf("/") + 1))) return;
188
+ return "uses the reserved .stowage temporary-file name";
189
+ }
190
+ function longSegmentReason(key) {
191
+ for (const segment of key.split("/")) {
192
+ const bytes = utf8$1.encode(segment).length;
193
+ if (bytes > segmentByteLimit) return `holds a segment of ${bytes} UTF-8 bytes, above the limit of ${segmentByteLimit}`;
194
+ }
195
+ }
196
+ //#endregion
197
+ //#region src/options.ts
198
+ const operationOptionKeys = ["signal"];
199
+ const putOptionKeys = [
200
+ ...operationOptionKeys,
201
+ "contentType",
202
+ "userMetadata"
203
+ ];
204
+ const getOptionKeys = [...operationOptionKeys, "range"];
205
+ const listOptionKeys = [
206
+ ...operationOptionKeys,
207
+ "prefix",
208
+ "delimiter",
209
+ "pageSize",
210
+ "cursor"
211
+ ];
212
+ const adapterOptionKeys = ["root"];
213
+ /**
214
+ * Refuses an option key the spec does not list (spec 4.3), in a call as well as in the
215
+ * configuration a storage is constructed from. TypeScript catches one at the call site;
216
+ * this catches the rest.
217
+ */
218
+ function requireKnownOptions(root, options, known, operation) {
219
+ if (options === void 0) return;
220
+ for (const key of Object.keys(options)) {
221
+ if (known.includes(key)) continue;
222
+ throw optionError(root, key, "is not one this storage takes", operation);
223
+ }
224
+ }
225
+ function optionError(root, option, expectation, operation) {
226
+ return fsError(root, {
227
+ code: "InvalidOption",
228
+ message: `The option \`${option}\` ${expectation}`,
229
+ operation,
230
+ attempts: 0
231
+ });
232
+ }
233
+ //#endregion
234
+ //#region src/listing.ts
235
+ const defaultPageSize = 1e3;
236
+ const maxPageSize = 1e3;
237
+ /** `objectsBelow` walks the tree below the prefix and hands it over sorted by key. */
238
+ function createListing(root, objectsBelow, options) {
239
+ return {
240
+ async page() {
241
+ const request = read(root, options);
242
+ options?.signal?.throwIfAborted();
243
+ const slice = take(await objectsBelow(request.prefix), request);
244
+ return {
245
+ objects: slice.objects,
246
+ prefixes: slice.prefixes,
247
+ cursor: slice.after === void 0 ? void 0 : encodeCursor(slice.after)
248
+ };
249
+ },
250
+ async *[Symbol.asyncIterator]() {
251
+ const request = read(root, options);
252
+ options?.signal?.throwIfAborted();
253
+ const entries = await objectsBelow(request.prefix);
254
+ for (let after = request.after;;) {
255
+ options?.signal?.throwIfAborted();
256
+ const slice = take(entries, {
257
+ ...request,
258
+ after
259
+ });
260
+ yield* slice.objects;
261
+ if (slice.after === void 0) return;
262
+ after = slice.after;
263
+ }
264
+ }
265
+ };
266
+ }
267
+ function read(root, options) {
268
+ requireKnownOptions(root, options, listOptionKeys, "list");
269
+ const prefix = options?.prefix ?? "";
270
+ requireKey(root, prefix, "prefix", "list");
271
+ const pageSize = options?.pageSize ?? defaultPageSize;
272
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > maxPageSize) throw optionError(root, "pageSize", `takes a whole number from 1 to ${maxPageSize}`, "list");
273
+ if (options?.delimiter === "") throw optionError(root, "delimiter", "takes at least one character", "list");
274
+ const after = options?.cursor === void 0 ? void 0 : decodeCursor(options.cursor);
275
+ if (options?.cursor !== void 0 && after === void 0) throw optionError(root, "cursor", "takes a cursor this storage handed out", "list");
276
+ return {
277
+ prefix,
278
+ delimiter: options?.delimiter,
279
+ pageSize,
280
+ after
281
+ };
282
+ }
283
+ function take(entries, request) {
284
+ const objects = [];
285
+ const prefixes = [];
286
+ let after;
287
+ for (const entry of entries) {
288
+ if (request.after !== void 0 && entry.key <= request.after) continue;
289
+ const pseudoDirectory = pseudoDirectoryOf(entry.key, request);
290
+ if (pseudoDirectory !== void 0 && prefixes.at(-1) === pseudoDirectory) {
291
+ after = entry.key;
292
+ continue;
293
+ }
294
+ if (objects.length + prefixes.length === request.pageSize) return {
295
+ objects,
296
+ prefixes,
297
+ after
298
+ };
299
+ if (pseudoDirectory === void 0) objects.push(entry);
300
+ else prefixes.push(pseudoDirectory);
301
+ after = entry.key;
302
+ }
303
+ return {
304
+ objects,
305
+ prefixes
306
+ };
307
+ }
308
+ function pseudoDirectoryOf(key, { prefix, delimiter }) {
309
+ if (delimiter === void 0) return void 0;
310
+ const end = key.indexOf(delimiter, prefix.length);
311
+ return end === -1 ? void 0 : key.slice(0, end + delimiter.length);
312
+ }
313
+ //#endregion
314
+ //#region src/errno.ts
315
+ const errnoMappings = /* @__PURE__ */ new Map([
316
+ ["ENOENT", { code: "NotFound" }],
317
+ ["EISDIR", {
318
+ code: "NotFound",
319
+ onWrite: "InvalidRequest"
320
+ }],
321
+ ["ENOTDIR", {
322
+ code: "NotFound",
323
+ onWrite: "InvalidRequest"
324
+ }],
325
+ ["EACCES", { code: "AccessDenied" }],
326
+ ["EPERM", { code: "AccessDenied" }],
327
+ ["ENAMETOOLONG", { code: "InvalidKey" }],
328
+ ["EILSEQ", {
329
+ code: "NotFound",
330
+ onWrite: "InvalidKey"
331
+ }],
332
+ ["EMFILE", {
333
+ code: "ProviderError",
334
+ retryable: true
335
+ }],
336
+ ["EBUSY", {
337
+ code: "ProviderError",
338
+ retryable: true
339
+ }],
340
+ ["EAGAIN", {
341
+ code: "ProviderError",
342
+ retryable: true
343
+ }]
344
+ ]);
345
+ /**
346
+ * What the file system threw, as the error spec 6 names for it. The adapter repeats
347
+ * nothing, so the count is the one attempt the syscall made, and `retryable` says that
348
+ * the condition may be gone rather than that anything was sent twice (spec 4.10).
349
+ */
350
+ function fsErrorFrom(thrown, failure) {
351
+ const errno = errnoOf(thrown);
352
+ const mapping = errno === void 0 ? void 0 : errnoMappings.get(errno);
353
+ const onWrite = failure.access === "write" ? mapping?.onWrite : void 0;
354
+ return fsError(failure.root, {
355
+ code: onWrite ?? mapping?.code ?? "ProviderError",
356
+ message: messageOf(thrown),
357
+ operation: failure.operation,
358
+ key: failure.key,
359
+ attempts: 1,
360
+ retryable: mapping?.retryable ?? false,
361
+ providerCode: errno,
362
+ cause: thrown
363
+ });
364
+ }
365
+ /** The `errno` string of a failed syscall, which travels on as the provider code. */
366
+ function errnoOf(thrown) {
367
+ if (typeof thrown !== "object" || thrown === null) return void 0;
368
+ const code = Reflect.get(thrown, "code");
369
+ return typeof code === "string" ? code : void 0;
370
+ }
371
+ /** The file system refuses to create the name, as APFS refuses a noncharacter (spec 6). */
372
+ function refusesName(thrown) {
373
+ return errnoOf(thrown) === "EILSEQ";
374
+ }
375
+ /**
376
+ * The key names nothing this storage holds, which is what an absent file amounts to, and
377
+ * so is a name a file system refuses on a lookup: no file can be held under it.
378
+ */
379
+ function isAbsence(thrown) {
380
+ const errno = errnoOf(thrown);
381
+ return errno === "ENOENT" || errno === "ENOTDIR" || errno === "EILSEQ";
382
+ }
383
+ function messageOf(thrown) {
384
+ return thrown instanceof Error ? thrown.message : String(thrown);
385
+ }
386
+ /**
387
+ * What a failed step reaches the caller as: the runtime's `AbortError` and a refusal this
388
+ * adapter already shaped travel as they are, and everything else is read off the table
389
+ * above (spec 4.10).
390
+ */
391
+ function asFailure(thrown, failure) {
392
+ if (isStorageError(thrown) || nameOf(thrown) === "AbortError") return thrown;
393
+ return fsErrorFrom(thrown, failure);
394
+ }
395
+ function nameOf(thrown) {
396
+ if (typeof thrown !== "object" || thrown === null) return void 0;
397
+ const name = Reflect.get(thrown, "name");
398
+ return typeof name === "string" ? name : void 0;
399
+ }
400
+ //#endregion
401
+ //#region src/paths.ts
402
+ /**
403
+ * The root as the file system resolves it, which every path an operation reaches is held
404
+ * against. It is read afresh per operation: construction performs no I/O, and a root
405
+ * that is gone by the time it is used is `NotFound` rather than a path of its own (spec 6).
406
+ */
407
+ async function resolveRoot(root, operation, access) {
408
+ try {
409
+ return await realpath(root);
410
+ } catch (thrown) {
411
+ throw fsErrorFrom(thrown, {
412
+ root,
413
+ operation,
414
+ access
415
+ });
416
+ }
417
+ }
418
+ /** The path the key names below the root, or `undefined` where no file can carry it. */
419
+ function pathOf(realRoot, key) {
420
+ const segments = key.split("/");
421
+ if (segments.at(-1) === "") return void 0;
422
+ return join(realRoot, ...segments);
423
+ }
424
+ function within(realRoot, path) {
425
+ return path === realRoot || path.startsWith(`${realRoot}${sep}`);
426
+ }
427
+ /**
428
+ * The key names nothing this storage holds, which is what a read of it answers. The count
429
+ * is what it cost to find that out: one lookup, or none where the key names no file at
430
+ * all (spec 4.10).
431
+ */
432
+ function absent(context, attempts) {
433
+ return fsError(context.root, {
434
+ code: "NotFound",
435
+ message: `No object under the key ${JSON.stringify(context.key)}`,
436
+ operation: context.operation,
437
+ key: context.key,
438
+ attempts
439
+ });
440
+ }
441
+ /**
442
+ * The real path of the file the key names. Spec 6 has every access resolve it and answer
443
+ * `NotFound` where it lies outside the root, so a symbolic link leaving the root behaves
444
+ * as an absent object rather than as a way out of the storage.
445
+ */
446
+ async function resolveObject(context) {
447
+ const path = pathOf(context.realRoot, context.key);
448
+ if (path === void 0) throw absent(context, 0);
449
+ let resolved;
450
+ try {
451
+ resolved = await realpath(path);
452
+ } catch (thrown) {
453
+ throw fsErrorFrom(thrown, {
454
+ ...context,
455
+ access: "read"
456
+ });
457
+ }
458
+ if (!within(context.realRoot, resolved)) throw absent(context, 1);
459
+ return resolved;
460
+ }
461
+ /**
462
+ * The path a write lands under, with the directories above it in place. The nearest
463
+ * existing directory is resolved before anything is created, so a link leaving the root
464
+ * cannot cause this storage to create directories outside it. The caller holds the
465
+ * directory lock until the write's file is in place, since a prune may otherwise remove a
466
+ * directory this found in place before anything lies in it.
467
+ */
468
+ async function prepareWrite(context) {
469
+ const path = pathOf(context.realRoot, context.key);
470
+ if (path === void 0) throw absent(context, 0);
471
+ const { directory, created } = await makeDirectory(dirname(path), context);
472
+ const abandon = async () => await removeCreated(created);
473
+ try {
474
+ const target = join(directory, basename(path));
475
+ if (!await resolvesWithin(context, target)) throw absent(context, 1);
476
+ return {
477
+ path: target,
478
+ abandon
479
+ };
480
+ } catch (thrown) {
481
+ await abandon();
482
+ throw thrown;
483
+ }
484
+ }
485
+ async function removeCreated(created) {
486
+ for (const directory of created.toReversed()) try {
487
+ await rmdir(directory);
488
+ } catch {
489
+ return;
490
+ }
491
+ }
492
+ async function resolvesWithin(context, path) {
493
+ try {
494
+ return within(context.realRoot, await realpath(path));
495
+ } catch (thrown) {
496
+ if (isAbsence(thrown)) return true;
497
+ throw fsErrorFrom(thrown, {
498
+ ...context,
499
+ access: "write"
500
+ });
501
+ }
502
+ }
503
+ async function makeDirectory(directory, context) {
504
+ const missing = [];
505
+ let existing = directory;
506
+ for (;;) try {
507
+ existing = await realpath(existing);
508
+ break;
509
+ } catch (thrown) {
510
+ if (!isAbsence(thrown)) throw fsErrorFrom(thrown, {
511
+ ...context,
512
+ access: "write"
513
+ });
514
+ missing.unshift(basename(existing));
515
+ existing = dirname(existing);
516
+ }
517
+ if (!within(context.realRoot, existing)) throw absent(context, 1);
518
+ const created = [];
519
+ try {
520
+ for (const segment of missing) {
521
+ const path = join(existing, segment);
522
+ try {
523
+ await mkdir(path);
524
+ created.push(path);
525
+ } catch (thrown) {
526
+ if (errnoOf(thrown) !== "EEXIST") throw fsErrorFrom(thrown, {
527
+ ...context,
528
+ access: "write"
529
+ });
530
+ }
531
+ try {
532
+ existing = await realpath(path);
533
+ } catch (thrown) {
534
+ throw fsErrorFrom(thrown, {
535
+ ...context,
536
+ access: "write"
537
+ });
538
+ }
539
+ if (!within(context.realRoot, existing)) throw absent(context, 1);
540
+ }
541
+ } catch (thrown) {
542
+ await removeCreated(created);
543
+ throw thrown;
544
+ }
545
+ return {
546
+ directory: existing,
547
+ created
548
+ };
549
+ }
550
+ //#endregion
551
+ //#region src/object-file.ts
552
+ /**
553
+ * The file below the key, or `undefined` where the key names no object of this storage:
554
+ * nothing at all, a directory, or a path that leaves the root (spec 6). What a listing
555
+ * passes over is what these operations find nothing under.
556
+ */
557
+ async function findObjectFile(context) {
558
+ const release = await holdDirectoryMutations(context.realRoot);
559
+ try {
560
+ const path = pathOf(context.realRoot, context.key);
561
+ if (path === void 0) {
562
+ release();
563
+ return;
564
+ }
565
+ const directory = await resolveWithin(context, dirname(path));
566
+ if (directory === void 0) {
567
+ release();
568
+ return;
569
+ }
570
+ const target = join(directory, basename(path));
571
+ const held = await describe$2(context, target);
572
+ if (held?.isFile() !== true) {
573
+ release();
574
+ return;
575
+ }
576
+ return {
577
+ path: target,
578
+ stats: held,
579
+ release
580
+ };
581
+ } catch (thrown) {
582
+ release();
583
+ throw thrown;
584
+ }
585
+ }
586
+ /** Releases the parent-directory lease after a read-only use of the found object. */
587
+ function releaseObjectFile(file) {
588
+ file.release();
589
+ }
590
+ /**
591
+ * Removes the file and the directories it leaves empty behind it, so that a listing with
592
+ * a delimiter names no pseudo-directory that holds nothing (spec 6).
593
+ */
594
+ async function removeObjectFile(context, file) {
595
+ try {
596
+ try {
597
+ await unlink(file.path);
598
+ } catch (thrown) {
599
+ if (!isAbsence(thrown)) throw fsErrorFrom(thrown, {
600
+ ...context,
601
+ access: "write"
602
+ });
603
+ }
604
+ await pruneEmptyDirectories(context.realRoot, dirname(file.path));
605
+ } finally {
606
+ file.release();
607
+ }
608
+ }
609
+ /**
610
+ * Moves the file to the path, which is the copy and the delete of spec 4.11 as one file
611
+ * system performs them, and removes what the source left empty behind it.
612
+ */
613
+ async function renameObjectFile(context, file, destination) {
614
+ try {
615
+ await rename(file.path, destination.path);
616
+ } catch (thrown) {
617
+ throw fsErrorFrom(thrown, {
618
+ ...refusesName(thrown) ? destination.context : context,
619
+ access: "write"
620
+ });
621
+ }
622
+ try {
623
+ await pruneEmptyDirectories(context.realRoot, dirname(file.path));
624
+ } finally {
625
+ file.release();
626
+ }
627
+ }
628
+ async function pruneEmptyDirectories(realRoot, directory) {
629
+ for (let current = directory; current !== realRoot && within(realRoot, current); current = dirname(current)) try {
630
+ await rmdir(current);
631
+ } catch {
632
+ return;
633
+ }
634
+ }
635
+ /** What the path holds, or `undefined` where nothing is there or a link leaves the root. */
636
+ async function describe$2(context, path) {
637
+ const held = await lookUp(context, async () => await lstat(path));
638
+ if (held === void 0 || !held.isSymbolicLink()) return held;
639
+ const resolved = await resolveWithin(context, path);
640
+ return resolved === void 0 ? void 0 : await lookUp(context, async () => await stat(resolved));
641
+ }
642
+ /** The path as the file system resolves it, where that lies below the root (spec 6). */
643
+ async function resolveWithin(context, path) {
644
+ const resolved = await lookUp(context, async () => await realpath(path));
645
+ return resolved !== void 0 && within(context.realRoot, resolved) ? resolved : void 0;
646
+ }
647
+ /** A lookup whose answer for a path that is not there is that nothing is there. */
648
+ async function lookUp(context, read) {
649
+ try {
650
+ return await read();
651
+ } catch (thrown) {
652
+ if (isAbsence(thrown)) return void 0;
653
+ throw fsErrorFrom(thrown, {
654
+ ...context,
655
+ access: "write"
656
+ });
657
+ }
658
+ }
659
+ //#endregion
660
+ //#region src/range.ts
661
+ /** Refuses bounds the spec does not allow, before the file is opened (spec 4.3). */
662
+ function requireRange(root, range) {
663
+ const refusal = rangeBoundsRefusal(range);
664
+ if (refusal !== void 0) throw fsError(root, {
665
+ ...refusal,
666
+ operation: "get",
667
+ attempts: 0
668
+ });
669
+ }
670
+ /** The last byte the range names, both ends inclusive, clipped to what the file holds. */
671
+ function lastByteToRead(root, range, size, key) {
672
+ if (range === void 0) return size - 1;
673
+ const refusal = rangeStartRefusal(range, size, key);
674
+ if (refusal !== void 0) throw fsError(root, {
675
+ ...refusal,
676
+ operation: "get",
677
+ key,
678
+ attempts: 1
679
+ });
680
+ return lastByteOf(range, size);
681
+ }
682
+ //#endregion
683
+ //#region src/stored-object.ts
684
+ const chunkSize = 65536;
685
+ /**
686
+ * The object `get` hands back. The file is opened where a reader is taken rather than
687
+ * where the object was described, so a caller reading the description alone holds no
688
+ * open file — the rename `put` lands through is what keeps that reader off a half-written
689
+ * object (spec 6).
690
+ */
691
+ function createStoredObject(context, stat, body) {
692
+ let read = false;
693
+ const take = () => {
694
+ if (read) throw fsError(context.root, {
695
+ code: "InvalidRequest",
696
+ message: "The body of this stored object has already been read",
697
+ operation: context.operation,
698
+ key: context.key,
699
+ attempts: 0
700
+ });
701
+ read = true;
702
+ return body;
703
+ };
704
+ const fail = (thrown) => asFailure(thrown, {
705
+ ...context,
706
+ access: "read"
707
+ });
708
+ const decoder = new TextDecoder();
709
+ return {
710
+ stat,
711
+ stream() {
712
+ let taken;
713
+ try {
714
+ taken = take();
715
+ } catch (error) {
716
+ return new ReadableStream({ start(controller) {
717
+ controller.error(error);
718
+ } });
719
+ }
720
+ return readStream(taken, fail);
721
+ },
722
+ async bytes() {
723
+ return await readAll(take(), fail);
724
+ },
725
+ async text() {
726
+ return decoder.decode(await readAll(take(), fail));
727
+ },
728
+ async json() {
729
+ return JSON.parse(decoder.decode(await readAll(take(), fail)));
730
+ }
731
+ };
732
+ }
733
+ function readStream(body, fail) {
734
+ let handle;
735
+ let position = body.start;
736
+ return new ReadableStream({
737
+ async pull(controller) {
738
+ const left = body.end - position + 1;
739
+ if (left <= 0) {
740
+ await close(handle);
741
+ controller.close();
742
+ return;
743
+ }
744
+ const buffer = new Uint8Array(Math.min(left, chunkSize));
745
+ try {
746
+ handle ??= await openFile(body.path, fail);
747
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
748
+ if (bytesRead === 0) {
749
+ await close(handle);
750
+ controller.close();
751
+ return;
752
+ }
753
+ position += bytesRead;
754
+ controller.enqueue(buffer.subarray(0, bytesRead));
755
+ } catch (thrown) {
756
+ await close(handle);
757
+ throw fail(thrown);
758
+ }
759
+ },
760
+ async cancel() {
761
+ await close(handle);
762
+ }
763
+ });
764
+ }
765
+ async function readAll(body, fail) {
766
+ const size = Math.max(body.end - body.start + 1, 0);
767
+ const bytes = new Uint8Array(size);
768
+ const handle = await openFile(body.path, fail);
769
+ let read = 0;
770
+ try {
771
+ while (read < size) {
772
+ const { bytesRead } = await handle.read(bytes, read, size - read, body.start + read);
773
+ if (bytesRead === 0) break;
774
+ read += bytesRead;
775
+ }
776
+ } catch (thrown) {
777
+ throw fail(thrown);
778
+ } finally {
779
+ await close(handle);
780
+ }
781
+ return read === size ? bytes : bytes.subarray(0, read);
782
+ }
783
+ async function openFile(path, fail) {
784
+ try {
785
+ return await open(path, "r");
786
+ } catch (thrown) {
787
+ throw fail(thrown);
788
+ }
789
+ }
790
+ async function close(handle) {
791
+ await handle?.close().catch(() => {});
792
+ }
793
+ //#endregion
794
+ //#region src/walk.ts
795
+ const utf8 = new TextEncoder();
796
+ const strictUtf8 = new TextDecoder("utf-8", { fatal: true });
797
+ /**
798
+ * Every object below the prefix, sorted by key. A listing pages through one order, so
799
+ * the snapshot it reads is sorted; the order itself is not promised (spec 4.6).
800
+ */
801
+ async function walkObjects(context, prefix) {
802
+ const entries = [];
803
+ const directory = prefix.slice(0, prefix.lastIndexOf("/") + 1);
804
+ await collect({
805
+ context,
806
+ entries,
807
+ prefix: utf8.encode(prefix)
808
+ }, join(context.realRoot, ...directory.split("/")), directory);
809
+ return entries.filter((entry) => entry.key.startsWith(prefix)).toSorted(byKey);
810
+ }
811
+ async function collect(walk, directory, keyPrefix) {
812
+ const { context, entries } = walk;
813
+ let resolved;
814
+ let held;
815
+ try {
816
+ resolved = await realpath(directory);
817
+ if (!within(context.realRoot, resolved)) return;
818
+ held = await entriesOf(resolved);
819
+ } catch (thrown) {
820
+ if (isAbsence(thrown)) return;
821
+ throw failure(context, thrown);
822
+ }
823
+ for (const entry of held) {
824
+ if ("bytes" in entry) {
825
+ refuseUndecodableName(walk, keyPrefix, entry.bytes);
826
+ continue;
827
+ }
828
+ const path = join(resolved, entry.name);
829
+ const key = `${keyPrefix}${entry.name}`;
830
+ if (entry.directory) {
831
+ await collect(walk, path, `${key}/`);
832
+ continue;
833
+ }
834
+ if (isTemporaryName(entry.name)) continue;
835
+ const described = await describe$1(context, path);
836
+ if (described !== void 0) entries.push({
837
+ key,
838
+ size: described.size,
839
+ lastModified: described.mtime
840
+ });
841
+ }
842
+ }
843
+ /**
844
+ * The names of one level. The runtime decodes a name that is no UTF-8 with U+FFFD in
845
+ * place of what it cannot read, so a level holding that character reads its names again
846
+ * as bytes to tell such a name from one that holds U+FFFD as written.
847
+ */
848
+ async function entriesOf(directory) {
849
+ const held = await readdir(directory, { withFileTypes: true });
850
+ if (!held.some((entry) => entry.name.includes("�"))) return held.map((entry) => ({
851
+ name: entry.name,
852
+ directory: entry.isDirectory()
853
+ }));
854
+ const names = await readdir(directory, { encoding: "buffer" });
855
+ const entries = [];
856
+ for (const bytes of names) {
857
+ const name = utf8NameOf(bytes);
858
+ if (name === void 0) {
859
+ entries.push({ bytes });
860
+ continue;
861
+ }
862
+ try {
863
+ entries.push({
864
+ name,
865
+ directory: (await lstat(join(directory, name))).isDirectory()
866
+ });
867
+ } catch (thrown) {
868
+ if (isAbsence(thrown)) continue;
869
+ throw thrown;
870
+ }
871
+ }
872
+ return entries;
873
+ }
874
+ function utf8NameOf(bytes) {
875
+ try {
876
+ return strictUtf8.decode(bytes);
877
+ } catch {
878
+ return;
879
+ }
880
+ }
881
+ /**
882
+ * A name that is no UTF-8 has no key, and a key with U+FFFD in its place would reach
883
+ * another object or none. Passing over it would let a move of the prefix lose the file
884
+ * in silence, so a listing it falls below fails as one whose entry arrived without a key
885
+ * (spec 4.6); a listing beside it goes on.
886
+ */
887
+ function refuseUndecodableName(walk, keyPrefix, bytes) {
888
+ const path = new Uint8Array([...utf8.encode(keyPrefix), ...bytes]);
889
+ if (!startsWith(path, walk.prefix)) return;
890
+ throw fsError(walk.context.root, {
891
+ code: "ProviderError",
892
+ message: `The name ${escapedBytesOf(path)} is no UTF-8, so no key names it`,
893
+ operation: walk.context.operation,
894
+ attempts: 1,
895
+ retryable: false
896
+ });
897
+ }
898
+ function startsWith(bytes, start) {
899
+ return start.length <= bytes.length && start.every((byte, index) => bytes[index] === byte);
900
+ }
901
+ function escapedBytesOf(bytes) {
902
+ return `"${Array.from(bytes, (byte) => standsAsItIs(byte) ? String.fromCharCode(byte) : `\\x${byte.toString(16).toUpperCase().padStart(2, "0")}`).join("")}"`;
903
+ }
904
+ /** Printable ASCII but for the quote and the backslash, which would read as an escape. */
905
+ function standsAsItIs(byte) {
906
+ return byte >= 32 && byte <= 126 && byte !== 34 && byte !== 92;
907
+ }
908
+ /**
909
+ * What the entry holds, or `undefined` for everything that is no object: a link leaving
910
+ * the root, a link to a directory, a socket, and a file another writer removed between
911
+ * the listing of the level and the reading of it.
912
+ */
913
+ async function describe$1(context, path) {
914
+ try {
915
+ const resolved = await realpath(path);
916
+ if (!within(context.realRoot, resolved)) return void 0;
917
+ const described = await stat(resolved);
918
+ return described.isFile() ? described : void 0;
919
+ } catch (thrown) {
920
+ if (isAbsence(thrown)) return void 0;
921
+ throw failure(context, thrown);
922
+ }
923
+ }
924
+ function failure(context, thrown) {
925
+ return fsErrorFrom(thrown, {
926
+ root: context.root,
927
+ operation: context.operation,
928
+ access: "read"
929
+ });
930
+ }
931
+ function byKey(one, other) {
932
+ return one.key < other.key ? -1 : one.key > other.key ? 1 : 0;
933
+ }
934
+ //#endregion
935
+ //#region src/index.ts
936
+ function fsStorage(options) {
937
+ return new FileSystemStorage(options);
938
+ }
939
+ const fsCapabilities = Object.freeze(["rangeReads"]);
940
+ /** A storage that does not declare `userMetadata` reads back none of it (spec 4.9). */
941
+ const noUserMetadata = Object.freeze({});
942
+ var FileSystemStorage = class {
943
+ provider = "fs";
944
+ bucket;
945
+ capabilities = fsCapabilities;
946
+ #root;
947
+ constructor(options) {
948
+ this.#root = readRoot(options);
949
+ this.bucket = this.#root;
950
+ }
951
+ async put(key, body, options) {
952
+ try {
953
+ return await this.#put(key, body, options);
954
+ } catch (failure) {
955
+ await cancelBody(body, failure);
956
+ throw failure;
957
+ }
958
+ }
959
+ async #put(key, body, options) {
960
+ requireKey(this.#root, key, "writable", "put");
961
+ requireKnownOptions(this.#root, options, putOptionKeys, "put");
962
+ this.#requireContentType(options?.contentType);
963
+ this.#requireNoUserMetadata(options?.userMetadata, key);
964
+ options?.signal?.throwIfAborted();
965
+ const context = await this.#context(key, "put", "write");
966
+ const reserved = await withDirectoryMutations(context.realRoot, async () => await this.#reserve(context));
967
+ try {
968
+ return await this.#write(context, reserved, body, options?.signal);
969
+ } catch (thrown) {
970
+ await withDirectoryMutations(context.realRoot, reserved.prepared.abandon);
971
+ throw thrown;
972
+ }
973
+ }
974
+ /**
975
+ * The directories a write needs and the file its bytes land in, created while the caller
976
+ * holds the directory lock: once that file is there, no prune removes the directory it
977
+ * lies in, so the body streams in without the lock.
978
+ */
979
+ async #reserve(context) {
980
+ const prepared = await prepareWrite(context);
981
+ const temporary = temporaryPathIn(dirname(prepared.path));
982
+ return {
983
+ prepared,
984
+ temporary,
985
+ handle: await abandoningOnFailure(prepared, async () => await this.#openTemporary(context, temporary))
986
+ };
987
+ }
988
+ async #write(context, { prepared, temporary, handle }, body, signal) {
989
+ return await this.#land(context, prepared.path, temporary, async () => {
990
+ try {
991
+ await writeBody(handle, body, signal);
992
+ signal?.throwIfAborted();
993
+ const written = await handle.stat();
994
+ await handle.close();
995
+ return written;
996
+ } catch (thrown) {
997
+ await handle.close().catch(() => {});
998
+ throw thrown;
999
+ }
1000
+ }, signal);
1001
+ }
1002
+ /**
1003
+ * Spec 6: the bytes land in a file beside the object and are renamed into place, so a
1004
+ * reader sees the object as it was or as it now is and never half of a write. A write
1005
+ * that broke takes its file with it, so that no listing names what never became one.
1006
+ */
1007
+ async #land(context, path, temporary, fill, signal) {
1008
+ try {
1009
+ const written = await fill();
1010
+ signal?.throwIfAborted();
1011
+ await rename(temporary, path);
1012
+ return describe(context.key, written.size, written.mtime);
1013
+ } catch (thrown) {
1014
+ await unlink(temporary).catch(() => {});
1015
+ throw asFailure(thrown, {
1016
+ ...context,
1017
+ access: "write"
1018
+ });
1019
+ }
1020
+ }
1021
+ async get(key, options) {
1022
+ requireKey(this.#root, key, "addressable", "get");
1023
+ requireKnownOptions(this.#root, options, getOptionKeys, "get");
1024
+ requireRange(this.#root, options?.range);
1025
+ options?.signal?.throwIfAborted();
1026
+ const context = await this.#context(key, "get", "read");
1027
+ const found = await this.#find(context);
1028
+ return createStoredObject(context, found.description, {
1029
+ path: found.path,
1030
+ start: options?.range?.start ?? 0,
1031
+ end: lastByteToRead(this.#root, options?.range, found.description.size, key)
1032
+ });
1033
+ }
1034
+ async stat(key, options) {
1035
+ requireKey(this.#root, key, "addressable", "stat");
1036
+ requireKnownOptions(this.#root, options, operationOptionKeys, "stat");
1037
+ options?.signal?.throwIfAborted();
1038
+ return (await this.#find(await this.#context(key, "stat", "read"))).description;
1039
+ }
1040
+ async exists(key, options) {
1041
+ requireKey(this.#root, key, "addressable", "exists");
1042
+ requireKnownOptions(this.#root, options, operationOptionKeys, "exists");
1043
+ options?.signal?.throwIfAborted();
1044
+ const context = await this.#context(key, "exists", "read");
1045
+ try {
1046
+ await this.#find(context);
1047
+ return true;
1048
+ } catch (thrown) {
1049
+ if (isStorageError(thrown) && thrown.code === "NotFound") return false;
1050
+ throw thrown;
1051
+ }
1052
+ }
1053
+ list(options) {
1054
+ return createListing(this.#root, async (prefix) => await walkObjects({
1055
+ root: this.#root,
1056
+ realRoot: await resolveRoot(this.#root, "list", "read"),
1057
+ operation: "list"
1058
+ }, prefix), options);
1059
+ }
1060
+ async delete(...keys) {
1061
+ const context = {
1062
+ root: this.#root,
1063
+ realRoot: await resolveRoot(this.#root, "delete", "write"),
1064
+ operation: "delete"
1065
+ };
1066
+ const failed = [];
1067
+ for (const key of keys) {
1068
+ const failure = await this.#remove({
1069
+ ...context,
1070
+ key
1071
+ });
1072
+ if (failure !== void 0) failed.push(failure);
1073
+ }
1074
+ return {
1075
+ requested: keys.length,
1076
+ failed
1077
+ };
1078
+ }
1079
+ async deleteAll(prefix, options) {
1080
+ requireKey(this.#root, prefix, "prefix", "deleteAll");
1081
+ requireKnownOptions(this.#root, options, operationOptionKeys, "deleteAll");
1082
+ options?.signal?.throwIfAborted();
1083
+ const context = {
1084
+ root: this.#root,
1085
+ realRoot: await resolveRoot(this.#root, "deleteAll", "write"),
1086
+ operation: "deleteAll"
1087
+ };
1088
+ const entries = await walkObjects(context, prefix);
1089
+ const failed = [];
1090
+ for (const entry of entries) {
1091
+ options?.signal?.throwIfAborted();
1092
+ const failure = await this.#remove({
1093
+ ...context,
1094
+ key: entry.key
1095
+ });
1096
+ if (failure !== void 0) failed.push(failure);
1097
+ }
1098
+ return {
1099
+ requested: entries.length,
1100
+ failed
1101
+ };
1102
+ }
1103
+ async #remove(context) {
1104
+ try {
1105
+ requireKey(this.#root, context.key, "addressable", context.operation);
1106
+ const file = await findObjectFile(context);
1107
+ if (file !== void 0) await removeObjectFile(context, file);
1108
+ return;
1109
+ } catch (thrown) {
1110
+ if (isStorageError(thrown)) return thrown;
1111
+ throw thrown;
1112
+ }
1113
+ }
1114
+ async copy(from, to, options) {
1115
+ const ends = await this.#endpoints(from, to, "copy", options);
1116
+ const source = await this.#requireFile(ends.from);
1117
+ try {
1118
+ const prepared = await prepareWrite(ends.to);
1119
+ const temporary = temporaryPathIn(dirname(prepared.path));
1120
+ return await abandoningOnFailure(prepared, async () => await this.#land(ends.to, prepared.path, temporary, async () => {
1121
+ await this.#read(ends.from, source.path, temporary);
1122
+ return await stat(temporary);
1123
+ }, options?.signal));
1124
+ } finally {
1125
+ releaseObjectFile(source);
1126
+ }
1127
+ }
1128
+ /** Reads the source into the file the copy lands through, and names it in a failure. */
1129
+ async #read(context, source, temporary) {
1130
+ try {
1131
+ await copyFile(source, temporary);
1132
+ } catch (thrown) {
1133
+ throw asFailure(thrown, {
1134
+ ...context,
1135
+ access: "read"
1136
+ });
1137
+ }
1138
+ }
1139
+ async move(from, to, options) {
1140
+ const ends = await this.#endpoints(from, to, "move", options);
1141
+ const source = await this.#requireFile(ends.from);
1142
+ try {
1143
+ const prepared = await prepareWrite(ends.to);
1144
+ await abandoningOnFailure(prepared, async () => {
1145
+ options?.signal?.throwIfAborted();
1146
+ await renameObjectFile(ends.from, source, {
1147
+ context: ends.to,
1148
+ path: prepared.path
1149
+ });
1150
+ });
1151
+ return describe(to, source.stats.size, source.stats.mtime);
1152
+ } finally {
1153
+ releaseObjectFile(source);
1154
+ }
1155
+ }
1156
+ /** Both ends of a `copy` or a `move`, once every rule of spec 4.11 has passed. */
1157
+ async #endpoints(from, to, operation, options) {
1158
+ requireKey(this.#root, from, "addressable", operation);
1159
+ requireKey(this.#root, to, "writable", operation);
1160
+ requireKnownOptions(this.#root, options, operationOptionKeys, operation);
1161
+ options?.signal?.throwIfAborted();
1162
+ if (from === to) throw fsError(this.#root, {
1163
+ code: "InvalidRequest",
1164
+ message: `The key ${JSON.stringify(from)} is both the source and the destination`,
1165
+ operation,
1166
+ key: from,
1167
+ attempts: 0
1168
+ });
1169
+ const realRoot = await resolveRoot(this.#root, operation, "write");
1170
+ return {
1171
+ from: {
1172
+ root: this.#root,
1173
+ realRoot,
1174
+ key: from,
1175
+ operation
1176
+ },
1177
+ to: {
1178
+ root: this.#root,
1179
+ realRoot,
1180
+ key: to,
1181
+ operation
1182
+ }
1183
+ };
1184
+ }
1185
+ /** The file the source key names, which `copy` and `move` owe a `NotFound` for. */
1186
+ async #requireFile(context) {
1187
+ const file = await findObjectFile(context);
1188
+ if (file !== void 0) return file;
1189
+ throw absent(context, context.key.endsWith("/") ? 0 : 1);
1190
+ }
1191
+ /** The file the key names, described as spec 4.4 has a read of one describe it. */
1192
+ async #find(context) {
1193
+ const path = await resolveObject(context);
1194
+ try {
1195
+ const described = await stat(path);
1196
+ if (!described.isFile()) throw absent(context, 1);
1197
+ return {
1198
+ path,
1199
+ description: describe(context.key, described.size, described.mtime)
1200
+ };
1201
+ } catch (thrown) {
1202
+ throw asFailure(thrown, {
1203
+ ...context,
1204
+ access: "read"
1205
+ });
1206
+ }
1207
+ }
1208
+ async #context(key, operation, access) {
1209
+ return {
1210
+ root: this.#root,
1211
+ realRoot: await resolveRoot(this.#root, operation, access),
1212
+ key,
1213
+ operation
1214
+ };
1215
+ }
1216
+ async #openTemporary(context, path) {
1217
+ try {
1218
+ return await open(path, "wx");
1219
+ } catch (thrown) {
1220
+ throw asFailure(thrown, {
1221
+ ...context,
1222
+ access: "write"
1223
+ });
1224
+ }
1225
+ }
1226
+ /** Spec 6 validates the type `put` was given and derives the stored one from the key. */
1227
+ #requireContentType(contentType) {
1228
+ if (contentType === void 0 || typeof contentType === "string") return;
1229
+ throw optionError(this.#root, "contentType", "takes a string", "put");
1230
+ }
1231
+ #requireNoUserMetadata(userMetadata, key) {
1232
+ if (userMetadata === void 0 || Object.keys(userMetadata).length === 0) return;
1233
+ throw fsError(this.#root, {
1234
+ code: "Unsupported",
1235
+ message: "This storage holds no user metadata",
1236
+ operation: "put",
1237
+ key,
1238
+ attempts: 0,
1239
+ capability: "userMetadata"
1240
+ });
1241
+ }
1242
+ };
1243
+ function readRoot(options) {
1244
+ const root = options.root;
1245
+ const named = typeof root === "string" ? root : "";
1246
+ requireKnownOptions(named, options, adapterOptionKeys, "fsStorage");
1247
+ if (typeof root !== "string" || !isAbsolute(root)) throw optionError(named, "root", "takes an absolute path to a directory", "fsStorage");
1248
+ return root;
1249
+ }
1250
+ function describe(key, size, lastModified) {
1251
+ return {
1252
+ key,
1253
+ size,
1254
+ lastModified,
1255
+ contentType: contentTypeOf(key),
1256
+ userMetadata: noUserMetadata
1257
+ };
1258
+ }
1259
+ async function abandoningOnFailure(prepared, write) {
1260
+ try {
1261
+ return await write();
1262
+ } catch (thrown) {
1263
+ await prepared.abandon();
1264
+ throw thrown;
1265
+ }
1266
+ }
1267
+ //#endregion
1268
+ export { fsStorage };