@ultimat3/storage 23.0.0 → 24.0.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.
@@ -0,0 +1,185 @@
1
+ // Single responsibility: how the local disk WRITES one object — the bytes, the sidecar and a
2
+ // pending marker, staged under `.meta/.tmp/` and renamed into place in the one order whose every
3
+ // crash point reads as absent, whole or untyped; refused by code when the disk says no.
4
+
5
+ // why: Bun has no `rename` and no `mkdir` — `Bun.write` creates parents and replaces in place,
6
+ // which is exactly the non-atomic write this file exists to stop. Delete both the day Bun ships them.
7
+ import { mkdir, rename } from 'node:fs/promises';
8
+ import { renderFixShellArg, stringField } from '@ultimat3/core';
9
+ import type { Sidecar } from './driver-local-sidecar';
10
+ import { isStorageError, keyConflict, putFailed } from './errors';
11
+ import { META_DIR } from './path';
12
+
13
+ /** Inside `.meta/`, so the listing's one filter already keeps a staged file out of the keys. */
14
+ const STAGING_DIR = `${META_DIR}/.tmp`;
15
+
16
+ export const sidecarPathOf = (key: string): string => `${META_DIR}/${key}.json`;
17
+ /** Beside the sidecar, and never one itself: a sidecar's name ends in `.json`. */
18
+ export const pendingPathOf = (key: string): string => `${META_DIR}/${key}.json.pending`;
19
+
20
+ const codeOf = (error: unknown): string | undefined => stringField(error, 'code');
21
+ /** `mkdir -p` through a file, a rename onto a directory, a write under a file. */
22
+ const isPathConflict = (error: unknown): boolean =>
23
+ ['ENOTDIR', 'EEXIST', 'EISDIR', 'ENOTEMPTY'].includes(codeOf(error) ?? '');
24
+ const isAbsent = (error: unknown): boolean => ['ENOENT', 'ENOTDIR'].includes(codeOf(error) ?? '');
25
+
26
+ /** The object KEY a path inside the root stands for — a sidecar's path names its object. */
27
+ const keyAt = (relative: string): string =>
28
+ relative.startsWith(`${META_DIR}/`)
29
+ ? relative.slice(META_DIR.length + 1).replace(/\.json(\.pending)?$/, '')
30
+ : relative;
31
+
32
+ /** The first ancestor of `relative` that is a FILE: the stored object standing where a directory must be. */
33
+ async function fileInTheWay(root: string, relative: string): Promise<string | undefined> {
34
+ const segments = relative.split('/').slice(0, -1);
35
+ for (let depth = 1; depth <= segments.length; depth += 1) {
36
+ const ancestor = segments.slice(0, depth).join('/');
37
+ if (await Bun.file(`${root}/${ancestor}`).exists()) return keyAt(ancestor);
38
+ }
39
+ // Nothing INSIDE the root is in the way: the root itself is the problem, and that is the
40
+ // disk refusing the write, not two keys colliding.
41
+ return undefined;
42
+ }
43
+
44
+ /**
45
+ * Make `relative` writable as a FILE, or refuse. Runs for both of an object's paths BEFORE either
46
+ * is touched: the sidecar tree collides where the object tree does not (`a` and `a.json/b`), and a
47
+ * put() that wrote its bytes and then failed on its sidecar left an object nobody had recorded.
48
+ */
49
+ async function claim(write: ObjectWrite, relative: string): Promise<void> {
50
+ const { root, key } = write;
51
+ const target = `${root}/${relative}`;
52
+ try {
53
+ await mkdir(target.slice(0, target.lastIndexOf('/')), { recursive: true });
54
+ } catch (error) {
55
+ const blocking = isPathConflict(error) ? await fileInTheWay(root, relative) : undefined;
56
+ throw blocking === undefined ? error : keyConflict(write.disk, key, blocking);
57
+ }
58
+ let directory = false;
59
+ try {
60
+ directory = (await Bun.file(target).stat()).isDirectory();
61
+ } catch (error) {
62
+ if (!isAbsent(error)) throw error;
63
+ }
64
+ // The keys in the way live UNDER this path: `<key>/…`, or `<key>.json/…` in the sidecar tree.
65
+ const beneath = relative.startsWith(`${META_DIR}/`)
66
+ ? relative.slice(META_DIR.length + 1)
67
+ : relative;
68
+ if (directory) throw keyConflict(write.disk, key, `${beneath}/`);
69
+ }
70
+
71
+ const discard = async (path: string): Promise<void> => {
72
+ await Bun.file(path)
73
+ .delete()
74
+ .catch(() => undefined);
75
+ };
76
+
77
+ export interface ObjectWrite {
78
+ readonly root: string;
79
+ readonly key: string;
80
+ /** The name the disk was REGISTERED under — what a refusal's `disk('…')` call must name. */
81
+ readonly disk: string;
82
+ /** A `BunFile` is a `Blob`: a copy hands the source file over and no byte crosses the heap. */
83
+ readonly body: Uint8Array | Blob;
84
+ /** `etag` MUST be the body's own: it is what the pending marker announces. */
85
+ readonly sidecar: Sidecar;
86
+ }
87
+
88
+ /** The four mutations a commit makes, in order — the seam a crash test dies at. */
89
+ export interface WriteSteps {
90
+ rename(from: string, to: string): Promise<void>;
91
+ unlink(path: string): Promise<void>;
92
+ }
93
+
94
+ const REAL_STEPS: WriteSteps = {
95
+ rename: (from, to) => rename(from, to),
96
+ unlink: async (path) => {
97
+ await Bun.file(path).delete();
98
+ },
99
+ };
100
+
101
+ /**
102
+ * Stage three files, then four steps: (1) rename the pending MARKER, which holds the new etag;
103
+ * (2) rename the sidecar; (3) rename the bytes; (4) remove the marker. Each rename replaces a
104
+ * directory entry whole, so no reader ever opens half a file.
105
+ *
106
+ * The order is chosen by what a crash leaves (`driver-local-crash.test.ts` dies at every step):
107
+ * - before (3), a FRESH key has no bytes file and so no object — absent, never half-present;
108
+ * - before (2), an overwrite still has the previous sidecar over the previous bytes — whole;
109
+ * - between (2) and (3), the new sidecar sits over the OLD bytes, and the marker naming that
110
+ * sidecar's etag is what tells `headObject` not to believe it;
111
+ * - between (3) and (4) the pair is whole and still marked, so a read re-checks it and a listing
112
+ * reports it untyped until the next put.
113
+ * Bytes-first has no such table: its torn overwrite is new bytes under the old type, undetectable.
114
+ */
115
+ async function commit(write: ObjectWrite, steps: WriteSteps): Promise<void> {
116
+ const sidecarPath = sidecarPathOf(write.key);
117
+ const pendingPath = pendingPathOf(write.key);
118
+ await claim(write, write.key);
119
+ await claim(write, sidecarPath);
120
+ await claim(write, pendingPath);
121
+ const staged = `${write.root}/${STAGING_DIR}/${crypto.randomUUID()}`;
122
+ // A second writer can make the colliding key between `claim` and the rename; only a RENAME's
123
+ // refusal is that, never a failed staging write.
124
+ const settle = async (suffix: string, relative: string): Promise<void> => {
125
+ await steps.rename(`${staged}.${suffix}`, `${write.root}/${relative}`).catch((error) => {
126
+ throw isPathConflict(error) ? keyConflict(write.disk, write.key, write.key) : error;
127
+ });
128
+ };
129
+ try {
130
+ await Bun.write(`${staged}.object`, write.body);
131
+ await Bun.write(`${staged}.sidecar`, JSON.stringify(write.sidecar));
132
+ await Bun.write(`${staged}.pending`, write.sidecar.etag);
133
+ await settle('pending', pendingPath);
134
+ await settle('sidecar', sidecarPath);
135
+ await settle('object', write.key);
136
+ } catch (error) {
137
+ for (const suffix of ['object', 'sidecar', 'pending']) await discard(`${staged}.${suffix}`);
138
+ throw error;
139
+ }
140
+ // The object is COMMITTED: all three renames landed. A marker that will not clear is not a
141
+ // failed put — reporting one told the caller to retry a write every reader can already see.
142
+ // What it leaves is exactly the crash-at-step-4 state, which `headObject` re-checks.
143
+ await steps.unlink(`${write.root}/${pendingPath}`).catch(() => undefined);
144
+ }
145
+
146
+ /**
147
+ * The local write, coded. Whatever the filesystem refuses with — `EACCES`, `ENOSPC`, `EROFS`, a
148
+ * root that is a file — leaves here as `X_STORAGE_PUT_FAILED` carrying the errno, where it used to
149
+ * be the bare `Error` out of `Bun.write`. A refusal this package already named passes through.
150
+ */
151
+ export async function commitObject(
152
+ write: ObjectWrite,
153
+ steps: WriteSteps = REAL_STEPS,
154
+ ): Promise<void> {
155
+ try {
156
+ await commit(write, steps);
157
+ } catch (error) {
158
+ if (isStorageError(error)) throw error;
159
+ const at = renderFixShellArg(write.root, '<the disk root>');
160
+ throw putFailed(write.disk, write.key, error, `ls -ld ${at} && df -h ${at}`);
161
+ }
162
+ }
163
+
164
+ /** Runs `task` after every earlier task queued under the same key has settled. */
165
+ export type KeyedQueue = <T>(key: string, task: () => Promise<T>) => Promise<T>;
166
+
167
+ /**
168
+ * One writer — or one reader — per key at a time, inside this process. An object is two files, so
169
+ * two unserialised `put()`s of one key interleaved their four writes and left one's bytes under the
170
+ * other's etag; a `get()` between a writer's two renames read the same mismatch.
171
+ */
172
+ export function keyedQueue(): KeyedQueue {
173
+ const tails = new Map<string, Promise<unknown>>();
174
+ return async <T>(key: string, task: () => Promise<T>): Promise<T> => {
175
+ const result = (tails.get(key) ?? Promise.resolve()).then(task, task);
176
+ const tail = result.catch(() => undefined);
177
+ tails.set(key, tail);
178
+ try {
179
+ return await result;
180
+ } finally {
181
+ // Only the LAST task clears the entry, or a Map of every key ever written is a leak.
182
+ if (tails.get(key) === tail) tails.delete(key);
183
+ }
184
+ };
185
+ }
@@ -6,13 +6,14 @@
6
6
  import {
7
7
  type Clock,
8
8
  finiteCount,
9
- isLocal,
9
+ NotImplementedError,
10
10
  type ResolveEnvironmentOptions,
11
- resolveEnvironment,
12
11
  stringField,
13
12
  systemClock,
14
13
  } from '@ultimat3/core';
15
14
  import {
15
+ assertListOptions,
16
+ assertPutOptions,
16
17
  DEFAULT_CONTENT_TYPE,
17
18
  etagOf,
18
19
  type ListOptions,
@@ -28,52 +29,18 @@ import {
28
29
  sha256Base64,
29
30
  toBytes,
30
31
  } from './driver';
31
- import {
32
- checksumMismatch,
33
- deleteFailed,
34
- listFailed,
35
- objectNotFound,
36
- signingSecretMissing,
37
- storageNotImplemented,
38
- } from './errors';
32
+ import { etagOfFile, headObject, readObjectBytes } from './driver-local-read';
33
+ import type { Sidecar } from './driver-local-sidecar';
34
+ import { commitObject, keyedQueue, pendingPathOf, sidecarPathOf } from './driver-local-write';
35
+ import { checksumMismatch, deleteFailed, getTooLarge, listFailed, objectNotFound } from './errors';
39
36
  import { assertSafeKey, META_DIR } from './path';
40
37
  import type { SignedUrlVerification } from './signed-url';
41
38
  import { buildSignedUrl, signedUrlBaseFor, verifySignedUrl } from './signed-url';
39
+ import { resolveSigningSecret } from './signing-secret';
42
40
  import { DEFAULT_MAX_UPLOAD_BYTES } from './upload';
43
41
 
44
42
  const DRIVER_NAME = 'local';
45
43
 
46
- /**
47
- * The dev-only fallback signing key. A literal, not a per-process random one, so a restart does
48
- * not invalidate every URL `x dev` handed out — and published in this repo, which is exactly why
49
- * `localDriver` refuses to use it outside a development or test environment.
50
- */
51
- export const DEV_SIGNING_SECRET = 'ultimate-dev-signing-secret';
52
-
53
- /** The env key production must set. Named once, read by the driver and by the predicate below. */
54
- export const STORAGE_SIGNING_SECRET_KEY = 'STORAGE_SIGNING_SECRET';
55
-
56
- /**
57
- * True while a local disk built without an explicit `signingSecret` would sign with the shipped
58
- * development key — `x doctor` reports it, exactly as it reports `usesDevCursorSecret()`.
59
- *
60
- * Reads the environment, not a driver instance: this is the same question `x doctor` asks about
61
- * the cursor secret, and a disk handed an explicit `signingSecret` in `app.config.ts` never
62
- * consults the variable at all.
63
- *
64
- * `env` is core's own slot, so this half of the guard reads the SAME table its other half does:
65
- * `dev-runtime.ts` asks `!isLocal({ env }) && usesDevStorageSecret({ env })`, and an embedding
66
- * caller (`serveApp({ env })`, a test fixture) whose `env` is not `process.env` used to get one
67
- * answer about the boot and one about the process — for the decision of whether a disk may be
68
- * signed with the published development key. Defaulted to `process.env`, so a bare call is
69
- * unchanged.
70
- */
71
- export function usesDevStorageSecret(options?: Pick<ResolveEnvironmentOptions, 'env'>): boolean {
72
- const source = options?.env ?? (process.env as Record<string, string | undefined>);
73
- const configured = source[STORAGE_SIGNING_SECRET_KEY];
74
- return configured === undefined || configured === '' || configured === DEV_SIGNING_SECRET;
75
- }
76
-
77
44
  export interface LocalDriverOptions {
78
45
  /** Directory the disk owns outright. Created on first write. */
79
46
  readonly root: string;
@@ -104,72 +71,11 @@ export interface LocalDriverOptions {
104
71
  * discovers by being OOM-killed.
105
72
  */
106
73
  readonly maxPutBytes?: number | undefined;
107
- }
108
-
109
- interface Sidecar {
110
- readonly contentType: string;
111
- readonly etag: string;
112
- readonly cacheControl?: string | undefined;
113
- readonly metadata?: Readonly<Record<string, string>> | undefined;
114
- }
115
-
116
- // `!Array.isArray` is the load-bearing clause, matching `isPlainObject` in
117
- // `@ultimat3/schema`'s `builder.ts`: `typeof [] === 'object'` and every value of `['a','b']` is a
118
- // string, so an array in the `metadata` slot was handed back through `head()`/`get()` as object
119
- // metadata — against a `Record<string, string>` every reader downstream is typed on.
120
- const isStringRecord = (value: unknown): value is Readonly<Record<string, string>> =>
121
- typeof value === 'object' &&
122
- value !== null &&
123
- !Array.isArray(value) &&
124
- Object.values(value as Record<string, unknown>).every((entry) => typeof entry === 'string');
125
-
126
- function parseSidecar(raw: unknown): Sidecar | undefined {
127
- if (typeof raw !== 'object' || raw === null) return undefined;
128
- const record = raw as Record<string, unknown>;
129
- const contentType = record['contentType'];
130
- const etag = record['etag'];
131
- if (typeof contentType !== 'string' || typeof etag !== 'string') return undefined;
132
- // `put()` writes cacheControl/metadata into the same sidecar (below) — dropping them here
133
- // silently truncated what was just written, even though `Sidecar` itself declares both.
134
- const cacheControl = record['cacheControl'];
135
- const metadata = record['metadata'];
136
- return {
137
- contentType,
138
- etag,
139
- ...(typeof cacheControl === 'string' ? { cacheControl } : {}),
140
- ...(isStringRecord(metadata) ? { metadata } : {}),
141
- };
142
- }
143
-
144
- /**
145
- * The secret a disk that mints its OWN URLs signs with — `localDriver` and `memoryDriver` both.
146
- *
147
- * A dev disk must work with zero config. Outside development the fallback is refused rather than
148
- * used: the literal is published, so signing with it hands every reader the power to mint a PUT
149
- * for any key with any size and type limit — which `acceptSignedUpload` then trusts over the app's
150
- * own `uploadPolicy`. Refused at construction, so the boot fails rather than the first upload.
151
- *
152
- * The published literal counts as no secret at all, whichever way it arrives: an env var or an
153
- * `app.config.ts` that pasted it in signs exactly as weakly as the fallback does. One table for
154
- * all three reads — the secret, the environment test and the environment the refusal names.
155
- * Splitting them is how the guard and the disk came to answer about two different processes.
156
- */
157
- export function resolveSigningSecret(
158
- disk: string,
159
- options: Pick<LocalDriverOptions, 'signingSecret' | 'env'>,
160
- ): string {
161
- const env = options.env ?? (process.env as Record<string, string | undefined>);
162
- const supplied = options.signingSecret ?? env[STORAGE_SIGNING_SECRET_KEY];
163
- const configured =
164
- supplied === undefined || supplied === '' || supplied === DEV_SIGNING_SECRET
165
- ? undefined
166
- : supplied;
167
- // FAILS CLOSED: a process that names no environment resolves as `production` here, the answer
168
- // core's `assertNoDevSecretsOutsideLocal` gives. `isLocal`'s own fallback is `development`, so the
169
- // process that forgot to say signed with the published key — exactly the one that must not.
170
- if (configured === undefined && !isLocal({ env, fallback: 'production' }))
171
- throw signingSecretMissing(resolveEnvironment({ env, fallback: 'production' }), disk);
172
- return configured ?? DEV_SIGNING_SECRET;
74
+ /**
75
+ * Ceiling on ONE `get()`, because `get()` buffers the whole object too — and an object's size
76
+ * is the uploader's, not this process's. Defaults to `maxPutBytes`; past it, `stream()`.
77
+ */
78
+ readonly maxGetBytes?: number | undefined;
173
79
  }
174
80
 
175
81
  /**
@@ -180,10 +86,11 @@ export function resolveSigningSecret(
180
86
  */
181
87
  function refuseUnsupportedPut(putOptions?: PutOptions): void {
182
88
  if (putOptions?.serverSideEncryption === undefined) return;
183
- throw storageNotImplemented(
184
- 'server-side encryption on the local driver (it writes plain files under one root)',
185
- 'drop serverSideEncryption from put(), and encrypt the disk itself — an s3Driver over a bucket with a default KMS rule, or a LUKS/FileVault volume under `root`',
186
- );
89
+ throw new NotImplementedError({
90
+ cause:
91
+ 'server-side encryption on the local driver (it writes plain files under one root) is not implemented by this driver — drop serverSideEncryption from put() and encrypt the disk itself: an s3Driver over a bucket with a default KMS rule, or a LUKS/FileVault volume under root',
92
+ fix: 'put(key, body)',
93
+ });
187
94
  }
188
95
 
189
96
  /** `ENOENT` is the one delete failure that means "already in the desired state". */
@@ -199,7 +106,16 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
199
106
  options.maxPutBytes === undefined ? DEFAULT_MAX_UPLOAD_BYTES : options.maxPutBytes,
200
107
  1,
201
108
  );
109
+ const maxGetBytes = finiteCount(
110
+ 'the local disk driver',
111
+ 'maxGetBytes',
112
+ options.maxGetBytes === undefined ? maxPutBytes : options.maxGetBytes,
113
+ 1,
114
+ );
202
115
  const clock = options.clock ?? systemClock;
116
+ const oneAtATime = keyedQueue();
117
+ // What `defineStorage` registered this driver as — the name a refusal's `disk('…')` must use.
118
+ let registered = DRIVER_NAME;
203
119
  // The segment is the disk's REGISTERED name, learned from `defineStorage` at boot — the driver
204
120
  // kind is not a mount point, and minting under it made every disk not literally named `local`
205
121
  // 404 its own URLs. An explicit `baseUrl` outranks the registration: that is the operator
@@ -208,45 +124,16 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
208
124
  const secret = resolveSigningSecret(DRIVER_NAME, options);
209
125
 
210
126
  const filePath = (key: string): string => `${root}/${key}`;
211
- const metaPath = (key: string): string => `${root}/${META_DIR}/${key}.json`;
127
+ const metaPath = (key: string): string => `${root}/${sidecarPathOf(key)}`;
212
128
 
213
- const readSidecar = async (key: string): Promise<Sidecar | undefined> => {
214
- const file = Bun.file(metaPath(key));
215
- if (!(await file.exists())) return undefined;
216
- try {
217
- const raw: unknown = await file.json();
218
- return parseSidecar(raw);
219
- } catch {
220
- return undefined;
221
- }
222
- };
223
-
224
- // No `contentType` fallback: the sidecar is the only thing that knows, so a missing one means
225
- // this driver does not know either — exactly what the s3 driver's `list()` reports. `get()`
226
- // fills the default below, because a `StorageObject` promises a type and a read has one.
227
- //
228
- // `hash` is the ONLY thing that reads the object's bytes, and it defaults off. The etag used to
229
- // be computed unconditionally when the sidecar was missing, under a comment saying "`list()`
230
- // must not read every file it lists" — which is exactly what `list()` then did, one whole
231
- // object at a time, sequentially, for every sidecar-less key on the disk (a `put()` that died
232
- // between its two writes leaves one). `copy()` inherited it too, so a copy documented as never
233
- // routing bytes through the heap buffered the whole source. A listing that cannot know an etag
234
- // reports `''`, which is what the s3 listing already answers for a provider that returns none.
235
- const head = async (key: string, hash = false): Promise<StorageListEntry | undefined> => {
236
- const file = Bun.file(filePath(key));
237
- if (!(await file.exists())) return undefined;
238
- const sidecar = await readSidecar(key);
239
- const etag = sidecar?.etag ?? (hash ? etagOf(new Uint8Array(await file.arrayBuffer())) : '');
240
- return {
241
- key,
242
- size: file.size,
243
- etag,
244
- lastModified: new Date(file.lastModified),
245
- ...(sidecar?.contentType === undefined ? {} : { contentType: sidecar.contentType }),
246
- ...(sidecar?.cacheControl === undefined ? {} : { cacheControl: sidecar.cacheControl }),
247
- ...(sidecar?.metadata === undefined ? {} : { metadata: sidecar.metadata }),
248
- };
249
- };
129
+ // Both QUEUED behind the key's writers, as `get()` is: unqueued, a measurement read the size of
130
+ // one generation before a commit and the sidecar of the next after it.
131
+ /** A listing's view: no bytes read, so a pair in doubt reports no type and no etag. */
132
+ const head = (key: string): Promise<StorageListEntry | undefined> =>
133
+ oneAtATime(key, () => headObject(root, key));
134
+ /** The same, settled against the bytes on disk — streamed through a hasher, never buffered. */
135
+ const measured = (key: string): Promise<StorageListEntry | undefined> =>
136
+ oneAtATime(key, () => headObject(root, key, () => etagOfFile(root, key)));
250
137
 
251
138
  /** Removes one path, or reports WHY it could not — a swallowed refusal is a false erasure. */
252
139
  const removeIfPresent = async (path: string, key: string): Promise<void> => {
@@ -272,12 +159,14 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
272
159
  },
273
160
 
274
161
  registerAs(diskName: string): void {
162
+ registered = diskName;
275
163
  if (options.baseUrl === undefined) baseUrl = signedUrlBaseFor(diskName);
276
164
  },
277
165
 
278
166
  async put(key: string, body: StorageBody, putOptions?: PutOptions): Promise<StorageObject> {
279
167
  const safe = assertSafeKey(key);
280
168
  refuseUnsupportedPut(putOptions);
169
+ assertPutOptions(DRIVER_NAME, putOptions);
281
170
  const bytes = await toBytes(body, { driver: DRIVER_NAME, key: safe, maxBytes: maxPutBytes });
282
171
  const claimed = putOptions?.checksum;
283
172
  if (claimed !== undefined) {
@@ -290,8 +179,9 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
290
179
  cacheControl: putOptions?.cacheControl,
291
180
  metadata: putOptions?.metadata,
292
181
  };
293
- await Bun.write(filePath(safe), bytes);
294
- await Bun.write(metaPath(safe), JSON.stringify(sidecar));
182
+ await oneAtATime(safe, () =>
183
+ commitObject({ root, key: safe, disk: registered, body: bytes, sidecar }),
184
+ );
295
185
  return {
296
186
  key: safe,
297
187
  size: bytes.byteLength,
@@ -303,21 +193,29 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
303
193
  };
304
194
  },
305
195
 
196
+ async stat(key: string): Promise<StorageObject | undefined> {
197
+ const entry = await measured(assertSafeKey(key));
198
+ return entry && { ...entry, contentType: entry.contentType ?? DEFAULT_CONTENT_TYPE };
199
+ },
200
+
306
201
  async get(key: string): Promise<StorageRead> {
307
202
  const safe = assertSafeKey(key);
308
- const entry = await head(safe);
309
- if (entry === undefined) throw objectNotFound(DRIVER_NAME, safe);
310
- const bytes = new Uint8Array(await Bun.file(filePath(safe)).arrayBuffer());
311
- return {
312
- object: {
313
- ...entry,
314
- contentType: entry.contentType ?? DEFAULT_CONTENT_TYPE,
315
- // Hashed HERE and not inside `head`, so a sidecar-less object is read exactly once: a
316
- // `get()` already holds every byte, and `head(key, true)` would have read them again.
317
- etag: entry.etag === '' ? etagOf(bytes) : entry.etag,
318
- },
319
- bytes,
320
- };
203
+ // Queued behind this key's writers: an object is two files, and a read between a put()'s
204
+ // renames is a pair in doubt this process has no need to meet.
205
+ return oneAtATime(safe, async () => {
206
+ const file = Bun.file(filePath(safe));
207
+ if (!(await file.exists())) throw objectNotFound(DRIVER_NAME, safe);
208
+ if (file.size > maxGetBytes) throw getTooLarge(DRIVER_NAME, safe, file.size, maxGetBytes);
209
+ const bytes = await readObjectBytes(root, safe);
210
+ // Settled against the bytes this read ALREADY holds: hashed only for a sidecar-less object
211
+ // or a pair in doubt, and then exactly once.
212
+ const entry = await headObject(root, safe, () => etagOf(bytes));
213
+ if (entry === undefined) throw objectNotFound(DRIVER_NAME, safe);
214
+ return {
215
+ object: { ...entry, contentType: entry.contentType ?? DEFAULT_CONTENT_TYPE },
216
+ bytes,
217
+ };
218
+ });
321
219
  },
322
220
 
323
221
  async stream(key: string): Promise<ReadableStream<Uint8Array>> {
@@ -331,19 +229,30 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
331
229
  async copy(from: string, to: string): Promise<StorageObject> {
332
230
  const source = assertSafeKey(from);
333
231
  const destination = assertSafeKey(to);
334
- // `hash: true` — the destination gets a sidecar, and a sidecar carrying `etag: ''` is a
335
- // durable lie every later `get()` of the copy would trust. The read is bounded to the one
336
- // case the source has no sidecar of its own; the common path still touches no bytes.
337
- const entry = await head(source, true);
232
+ // Measured, not merely read: the destination gets a sidecar, and one carrying `etag: ''` or
233
+ // a torn source's borrowed type is a durable lie every later `get()` of the copy would
234
+ // trust. The hash is streamed, and only for a sidecar-less source or a pair in doubt.
235
+ const entry = await measured(source);
338
236
  if (entry === undefined) throw objectNotFound(DRIVER_NAME, source);
339
- await Bun.write(filePath(destination), Bun.file(filePath(source)));
340
237
  const sidecar: Sidecar = {
341
238
  contentType: entry.contentType ?? DEFAULT_CONTENT_TYPE,
342
239
  etag: entry.etag,
343
240
  cacheControl: entry.cacheControl,
344
241
  metadata: entry.metadata,
345
242
  };
346
- await Bun.write(metaPath(destination), JSON.stringify(sidecar));
243
+ await oneAtATime(destination, () =>
244
+ commitObject({
245
+ root,
246
+ key: destination,
247
+ disk: registered,
248
+ body: Bun.file(filePath(source)),
249
+ sidecar,
250
+ }),
251
+ ).catch(async (error: unknown) => {
252
+ // The source was deleted while this copy waited its turn on the destination.
253
+ if (!(await Bun.file(filePath(source)).exists())) throw objectNotFound(DRIVER_NAME, source);
254
+ throw error;
255
+ });
347
256
  return {
348
257
  ...entry,
349
258
  key: destination,
@@ -357,8 +266,11 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
357
266
  // Idempotent by contract: a missing key is already in the desired state. A REFUSED unlink
358
267
  // is not — a read-only mount or a root this process cannot write reports the bytes gone
359
268
  // when they are still on disk, which is the one lie an erasure sweep must never repeat.
360
- await removeIfPresent(filePath(safe), safe);
361
- await removeIfPresent(metaPath(safe), safe);
269
+ await oneAtATime(safe, async () => {
270
+ await removeIfPresent(filePath(safe), safe);
271
+ await removeIfPresent(metaPath(safe), safe);
272
+ await removeIfPresent(`${root}/${pendingPathOf(safe)}`, safe);
273
+ });
362
274
  },
363
275
 
364
276
  async exists(key: string): Promise<boolean> {
@@ -367,6 +279,7 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
367
279
 
368
280
  async list(listOptions?: ListOptions): Promise<ListPage> {
369
281
  const prefix = listOptions?.prefix ?? '';
282
+ assertListOptions(listOptions);
370
283
  const limit = resolveListLimit(listOptions?.limit);
371
284
  const cursor = listOptions?.cursor;
372
285
  const keys: string[] = [];
@@ -5,8 +5,10 @@
5
5
  // It exists because the alternative was written by hand in every suite that needed a bucket: a
6
6
  // fake with three methods and five `unsupported()` stubs, each a slightly different contract.
7
7
 
8
- import { type Clock, finiteCount, systemClock } from '@ultimat3/core';
8
+ import { type Clock, finiteCount, NotImplementedError, systemClock } from '@ultimat3/core';
9
9
  import {
10
+ assertListOptions,
11
+ assertPutOptions,
10
12
  DEFAULT_CONTENT_TYPE,
11
13
  etagOf,
12
14
  type ListOptions,
@@ -22,11 +24,11 @@ import {
22
24
  toBytes,
23
25
  } from './driver';
24
26
  import type { LocalDriverOptions } from './driver-local';
25
- import { resolveSigningSecret } from './driver-local';
26
- import { checksumMismatch, objectNotFound, storageNotImplemented } from './errors';
27
+ import { checksumMismatch, getTooLarge, objectNotFound } from './errors';
27
28
  import { assertSafeKey } from './path';
28
29
  import type { SignedUrlVerification } from './signed-url';
29
30
  import { buildSignedUrl, signedUrlBaseFor, verifySignedUrl } from './signed-url';
31
+ import { resolveSigningSecret } from './signing-secret';
30
32
  import { DEFAULT_MAX_UPLOAD_BYTES } from './upload';
31
33
 
32
34
  const DRIVER_NAME = 'memory';
@@ -54,7 +56,9 @@ interface Stored {
54
56
  */
55
57
  const snapshot = (object: StorageObject): StorageObject => ({
56
58
  ...object,
57
- lastModified: new Date(object.lastModified.getTime()),
59
+ ...(object.lastModified === undefined
60
+ ? {}
61
+ : { lastModified: new Date(object.lastModified.getTime()) }),
58
62
  ...(object.metadata === undefined ? {} : { metadata: { ...object.metadata } }),
59
63
  });
60
64
 
@@ -65,6 +69,12 @@ export function memoryDriver(options: MemoryDriverOptions = {}): MemoryStorageDr
65
69
  options.maxPutBytes === undefined ? DEFAULT_MAX_UPLOAD_BYTES : options.maxPutBytes,
66
70
  1,
67
71
  );
72
+ const maxGetBytes = finiteCount(
73
+ 'the memory disk driver',
74
+ 'maxGetBytes',
75
+ options.maxGetBytes === undefined ? maxPutBytes : options.maxGetBytes,
76
+ 1,
77
+ );
68
78
  const clock = options.clock ?? systemClock;
69
79
  let baseUrl = options.baseUrl ?? signedUrlBaseFor(DRIVER_NAME);
70
80
  const secret = resolveSigningSecret(DRIVER_NAME, options);
@@ -96,11 +106,13 @@ export function memoryDriver(options: MemoryDriverOptions = {}): MemoryStorageDr
96
106
  // The same refusal both shipped disks make: a `put()` that succeeds in a test and throws in
97
107
  // production is a gap an app meets on the worst day.
98
108
  if (putOptions?.serverSideEncryption !== undefined) {
99
- throw storageNotImplemented(
100
- 'server-side encryption on the memory driver (it holds bytes in a Map)',
101
- 'drop serverSideEncryption from put(), and encrypt the disk itself — an s3Driver over a bucket with a default KMS rule',
102
- );
109
+ throw new NotImplementedError({
110
+ cause:
111
+ 'server-side encryption on the memory driver (it holds bytes in a Map) is not implemented by this driver — drop serverSideEncryption from put() and encrypt the disk itself: an s3Driver over a bucket with a default KMS rule',
112
+ fix: 'put(key, body)',
113
+ });
103
114
  }
115
+ assertPutOptions(DRIVER_NAME, putOptions);
104
116
  const bytes = await toBytes(body, { driver: DRIVER_NAME, key: safe, maxBytes: maxPutBytes });
105
117
  const claimed = putOptions?.checksum;
106
118
  if (claimed !== undefined) {
@@ -123,8 +135,18 @@ export function memoryDriver(options: MemoryDriverOptions = {}): MemoryStorageDr
123
135
  return snapshot(object);
124
136
  },
125
137
 
138
+ async stat(key: string): Promise<StorageObject | undefined> {
139
+ const hit = stored.get(assertSafeKey(key));
140
+ return hit && snapshot(hit.object);
141
+ },
142
+
126
143
  async get(key: string): Promise<StorageRead> {
127
- const hit = found(assertSafeKey(key));
144
+ const safe = assertSafeKey(key);
145
+ const hit = found(safe);
146
+ // The local disk's refusal, so a suite on this disk meets the ceiling production has.
147
+ if (hit.bytes.byteLength > maxGetBytes) {
148
+ throw getTooLarge(DRIVER_NAME, safe, hit.bytes.byteLength, maxGetBytes);
149
+ }
128
150
  return { object: snapshot(hit.object), bytes: hit.bytes.slice() };
129
151
  },
130
152
 
@@ -134,8 +156,10 @@ export function memoryDriver(options: MemoryDriverOptions = {}): MemoryStorageDr
134
156
  },
135
157
 
136
158
  async copy(from: string, to: string): Promise<StorageObject> {
137
- const source = found(assertSafeKey(from));
138
- const destination = assertSafeKey(to);
159
+ // Both keys before either lookup, as the other two disks do: an unsafe destination is the
160
+ // refusal whether or not the source exists.
161
+ const [from_, destination] = [assertSafeKey(from), assertSafeKey(to)];
162
+ const source = found(from_);
139
163
  const object: StorageObject = {
140
164
  ...source.object,
141
165
  key: destination,
@@ -155,6 +179,7 @@ export function memoryDriver(options: MemoryDriverOptions = {}): MemoryStorageDr
155
179
 
156
180
  async list(listOptions?: ListOptions): Promise<ListPage> {
157
181
  const prefix = listOptions?.prefix ?? '';
182
+ assertListOptions(listOptions);
158
183
  const limit = resolveListLimit(listOptions?.limit);
159
184
  const cursor = listOptions?.cursor;
160
185
  // Code-unit order and "the cursor is the last key of the page before" — the local disk's own.