@nifrajs/storage 1.3.1 → 1.5.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/README.md +16 -1
- package/dist/conformance.d.ts +23 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +178 -0
- package/dist/conformance.js.map +1 -0
- package/dist/file.d.ts +8 -1
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +125 -17
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +1 -1
- package/dist/key.d.ts.map +1 -1
- package/dist/key.js +4 -1
- package/dist/key.js.map +1 -1
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +4 -1
- package/dist/memory.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/conformance.ts +210 -0
- package/src/file.ts +130 -17
- package/src/index.ts +13 -0
- package/src/key.ts +4 -1
- package/src/memory.ts +5 -1
- package/src/types.ts +50 -0
package/README.md
CHANGED
|
@@ -20,12 +20,25 @@ const keys = await storage.list({ prefix: "avatars/" })
|
|
|
20
20
|
| Adapter | Use | Notes |
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| `MemoryStorage` | dev / tests | not durable, not shared across instances |
|
|
23
|
-
| `FileStorage(root)` | a long-running server (Bun/Node/Deno) |
|
|
23
|
+
| `FileStorage(root)` | a long-running server (Bun/Node/Deno) | persists content type + custom metadata in an adjacent sidecar tree; legacy objects still infer MIME from extension |
|
|
24
24
|
| `R2Storage(env.BUCKET)` | the edge (Cloudflare R2) | round-trips `contentType` + metadata; binding typed structurally (no `@cloudflare/workers-types`) |
|
|
25
25
|
|
|
26
26
|
Implement `StorageAdapter` (`put` / `get` / `delete` / `exists` / `list`) for **S3 / GCS / anything else** —
|
|
27
27
|
the same five methods, and your routes don't change.
|
|
28
28
|
|
|
29
|
+
Run the executable contract against third-party adapters:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { assertStorageAdapterConformance } from "@nifrajs/storage"
|
|
33
|
+
|
|
34
|
+
await assertStorageAdapterConformance({ createAdapter: () => new MyStorage() })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Provider-specific mechanics stay optional: implement `PagedStorageAdapter`, `PresignableStorageAdapter`,
|
|
38
|
+
and/or `MovableStorageAdapter` only when the provider supports cursor listing, URL signing, or server-side
|
|
39
|
+
copy/move. Asset sensitivity, bucket routing, credentials, and TTL policy belong in the consuming app or
|
|
40
|
+
private package—not in these mechanical interfaces.
|
|
41
|
+
|
|
29
42
|
## Key safety
|
|
30
43
|
|
|
31
44
|
Keys are POSIX-ish relative paths (`avatars/u1.png`). **Every** adapter rejects unsafe keys — absolute
|
|
@@ -42,3 +55,5 @@ import { assertSafeKey, StorageKeyError } from "@nifrajs/storage"
|
|
|
42
55
|
- `put(key, data, { contentType?, metadata? })` — `data` is `Uint8Array | ArrayBuffer | string`.
|
|
43
56
|
- `get(key)` → `{ body, size, contentType?, metadata? } | null` · `delete(key)` · `exists(key)` · `list({ prefix?, limit? })`.
|
|
44
57
|
- `assertSafeKey(key)` / `StorageKeyError` · `toBytes(data)`.
|
|
58
|
+
- `assertStorageAdapterConformance({ createAdapter })` — dependency-free executable adapter contract.
|
|
59
|
+
- Optional interfaces: `PagedStorageAdapter` · `PresignableStorageAdapter` · `MovableStorageAdapter`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StorageAdapter } from "./types.js";
|
|
2
|
+
/** Construction and cleanup hooks for {@link assertStorageAdapterConformance}. */
|
|
3
|
+
export interface StorageAdapterConformanceOptions {
|
|
4
|
+
/** Return an empty, isolated adapter for this run. */
|
|
5
|
+
readonly createAdapter: () => StorageAdapter | Promise<StorageAdapter>;
|
|
6
|
+
}
|
|
7
|
+
/** A failed invariant reported by {@link assertStorageAdapterConformance}. */
|
|
8
|
+
export declare class StorageAdapterConformanceError extends Error {
|
|
9
|
+
readonly check: string;
|
|
10
|
+
constructor(check: string, message: string, cause?: unknown);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Execute the observable {@link StorageAdapter} contract without depending on a test runner.
|
|
14
|
+
*
|
|
15
|
+
* The adapter must be isolated because this writes deterministic conformance keys. The promise resolves
|
|
16
|
+
* only after proving byte + metadata round-trips, overwrite, missing/delete semantics, prefix/limit listing,
|
|
17
|
+
* and uniform unsafe-key rejection across every key-taking method. When the adapter also implements an
|
|
18
|
+
* optional capability ({@link PagedStorageAdapter}, {@link PresignableStorageAdapter},
|
|
19
|
+
* {@link MovableStorageAdapter}), that capability's contract is proven too — implementing a capability
|
|
20
|
+
* partially or unsafely fails conformance rather than silently escaping it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertStorageAdapterConformance(options: StorageAdapterConformanceOptions): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,cAAc,EACf,MAAM,YAAY,CAAA;AAEnB,kFAAkF;AAClF,MAAM,WAAW,gCAAgC;IAC/C,sDAAsD;IACtD,QAAQ,CAAC,aAAa,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CACvE;AAED,8EAA8E;AAC9E,qBAAa,8BAA+B,SAAQ,KAAK;IAErD,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAKlB;AAkBD;;;;;;;;;GASG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,IAAI,CAAC,CA4Jf"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** A failed invariant reported by {@link assertStorageAdapterConformance}. */
|
|
2
|
+
export class StorageAdapterConformanceError extends Error {
|
|
3
|
+
check;
|
|
4
|
+
constructor(check, message, cause) {
|
|
5
|
+
super(`StorageAdapter conformance failed (${check}): ${message}`, { cause });
|
|
6
|
+
this.check = check;
|
|
7
|
+
this.name = "StorageAdapterConformanceError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const fail = (check, message, cause) => {
|
|
11
|
+
throw new StorageAdapterConformanceError(check, message, cause);
|
|
12
|
+
};
|
|
13
|
+
const decode = (bytes) => new TextDecoder().decode(bytes);
|
|
14
|
+
const hasPaging = (storage) => typeof storage.listPage === "function";
|
|
15
|
+
const hasPresign = (storage) => typeof storage.presign === "function";
|
|
16
|
+
const hasMove = (storage) => typeof storage.copy === "function" &&
|
|
17
|
+
typeof storage.move === "function";
|
|
18
|
+
/**
|
|
19
|
+
* Execute the observable {@link StorageAdapter} contract without depending on a test runner.
|
|
20
|
+
*
|
|
21
|
+
* The adapter must be isolated because this writes deterministic conformance keys. The promise resolves
|
|
22
|
+
* only after proving byte + metadata round-trips, overwrite, missing/delete semantics, prefix/limit listing,
|
|
23
|
+
* and uniform unsafe-key rejection across every key-taking method. When the adapter also implements an
|
|
24
|
+
* optional capability ({@link PagedStorageAdapter}, {@link PresignableStorageAdapter},
|
|
25
|
+
* {@link MovableStorageAdapter}), that capability's contract is proven too — implementing a capability
|
|
26
|
+
* partially or unsafely fails conformance rather than silently escaping it.
|
|
27
|
+
*/
|
|
28
|
+
export async function assertStorageAdapterConformance(options) {
|
|
29
|
+
let storage;
|
|
30
|
+
try {
|
|
31
|
+
storage = await options.createAdapter();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return fail("construction", "createAdapter failed", error);
|
|
35
|
+
}
|
|
36
|
+
const a = "nifra-conformance/a/one.txt";
|
|
37
|
+
const b = "nifra-conformance/a/two.bin";
|
|
38
|
+
const c = "nifra-conformance/b/three.bin";
|
|
39
|
+
try {
|
|
40
|
+
await storage.put(a, "one", {
|
|
41
|
+
contentType: "text/x-nifra-conformance",
|
|
42
|
+
metadata: { owner: "nifra", purpose: "conformance" },
|
|
43
|
+
});
|
|
44
|
+
const object = (await storage.get(a)) ?? fail("round trip", "put object was not readable");
|
|
45
|
+
if (!(object.body instanceof Uint8Array))
|
|
46
|
+
fail("round trip", "body must be Uint8Array");
|
|
47
|
+
if (decode(object.body) !== "one" || object.size !== 3) {
|
|
48
|
+
fail("round trip", "body or size changed during round-trip");
|
|
49
|
+
}
|
|
50
|
+
if (object.contentType !== "text/x-nifra-conformance") {
|
|
51
|
+
fail("metadata", "contentType did not round-trip");
|
|
52
|
+
}
|
|
53
|
+
if (object.metadata?.owner !== "nifra" || object.metadata.purpose !== "conformance") {
|
|
54
|
+
fail("metadata", "custom metadata did not round-trip");
|
|
55
|
+
}
|
|
56
|
+
if (!(await storage.exists(a)))
|
|
57
|
+
fail("exists", "stored key was reported missing");
|
|
58
|
+
if (await storage.exists("nifra-conformance/missing")) {
|
|
59
|
+
fail("exists", "missing key was reported present");
|
|
60
|
+
}
|
|
61
|
+
if ((await storage.get("nifra-conformance/missing")) !== null) {
|
|
62
|
+
fail("missing", "get must return null for a missing key");
|
|
63
|
+
}
|
|
64
|
+
await storage.put(a, "overwritten");
|
|
65
|
+
const overwritten = await storage.get(a);
|
|
66
|
+
if (overwritten === null || decode(overwritten.body) !== "overwritten") {
|
|
67
|
+
fail("overwrite", "put did not replace the existing body");
|
|
68
|
+
}
|
|
69
|
+
await storage.put(b, "two");
|
|
70
|
+
await storage.put(c, "three");
|
|
71
|
+
const prefixed = [...(await storage.list({ prefix: "nifra-conformance/a/" }))].sort();
|
|
72
|
+
if (prefixed.length !== 2 || prefixed[0] !== a || prefixed[1] !== b) {
|
|
73
|
+
fail("list prefix", "prefix listing returned unexpected keys");
|
|
74
|
+
}
|
|
75
|
+
if ((await storage.list({ prefix: "nifra-conformance/", limit: 1 })).length !== 1) {
|
|
76
|
+
fail("list limit", "limit was not respected");
|
|
77
|
+
}
|
|
78
|
+
const unsafeCalls = [
|
|
79
|
+
["put", () => storage.put("../escape", "x")],
|
|
80
|
+
["get", () => storage.get("../escape")],
|
|
81
|
+
["delete", () => storage.delete("../escape")],
|
|
82
|
+
["exists", () => storage.exists("../escape")],
|
|
83
|
+
];
|
|
84
|
+
if (hasPaging(storage)) {
|
|
85
|
+
const paged = storage;
|
|
86
|
+
// The cursor walk must reproduce exactly the keys list() reports, without duplicates,
|
|
87
|
+
// and must terminate by omitting the cursor on the final page.
|
|
88
|
+
const all = [...(await paged.list({ prefix: "nifra-conformance/" }))].sort();
|
|
89
|
+
const walked = [];
|
|
90
|
+
let cursor;
|
|
91
|
+
for (let page = 0; page <= all.length; page++) {
|
|
92
|
+
const result = await paged.listPage({
|
|
93
|
+
prefix: "nifra-conformance/",
|
|
94
|
+
limit: 1,
|
|
95
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
96
|
+
});
|
|
97
|
+
if (result.keys.length > 1)
|
|
98
|
+
fail("listPage limit", "page exceeded the requested limit");
|
|
99
|
+
walked.push(...result.keys);
|
|
100
|
+
cursor = result.cursor;
|
|
101
|
+
if (cursor === undefined)
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (cursor !== undefined)
|
|
105
|
+
fail("listPage cursor", "cursor never terminated");
|
|
106
|
+
const sortedWalk = [...walked].sort();
|
|
107
|
+
if (new Set(walked).size !== walked.length) {
|
|
108
|
+
fail("listPage cursor", "cursor walk returned duplicate keys");
|
|
109
|
+
}
|
|
110
|
+
if (sortedWalk.length !== all.length || sortedWalk.some((key, i) => key !== all[i])) {
|
|
111
|
+
fail("listPage cursor", "cursor walk and list() disagree on the key set");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (hasMove(storage)) {
|
|
115
|
+
const movable = storage;
|
|
116
|
+
const copied = "nifra-conformance/b/copied.bin";
|
|
117
|
+
const moved = "nifra-conformance/b/moved.bin";
|
|
118
|
+
await movable.copy(c, copied);
|
|
119
|
+
const copyTarget = await movable.get(copied);
|
|
120
|
+
const copySource = await movable.get(c);
|
|
121
|
+
if (copyTarget === null || decode(copyTarget.body) !== "three") {
|
|
122
|
+
fail("copy", "copied object is missing or has different bytes");
|
|
123
|
+
}
|
|
124
|
+
if (copySource === null)
|
|
125
|
+
fail("copy", "copy must not remove the source object");
|
|
126
|
+
await movable.move(copied, moved);
|
|
127
|
+
const moveTarget = await movable.get(moved);
|
|
128
|
+
if (moveTarget === null || decode(moveTarget.body) !== "three") {
|
|
129
|
+
fail("move", "moved object is missing or has different bytes");
|
|
130
|
+
}
|
|
131
|
+
if ((await movable.exists(copied)) || (await movable.get(copied)) !== null) {
|
|
132
|
+
fail("move", "move must remove the source object");
|
|
133
|
+
}
|
|
134
|
+
await movable.delete(moved);
|
|
135
|
+
unsafeCalls.push(["copy source", () => movable.copy("../escape", copied)], ["copy destination", () => movable.copy(c, "../escape")], ["move source", () => movable.move("../escape", moved)], ["move destination", () => movable.move(c, "../escape")]);
|
|
136
|
+
}
|
|
137
|
+
if (hasPresign(storage)) {
|
|
138
|
+
const presignable = storage;
|
|
139
|
+
for (const operation of ["get", "put"]) {
|
|
140
|
+
const presigned = await presignable.presign(a, operation, {
|
|
141
|
+
expiresInSeconds: 60,
|
|
142
|
+
...(operation === "put" ? { contentType: "text/plain", contentLength: 3 } : {}),
|
|
143
|
+
});
|
|
144
|
+
if (typeof presigned.url !== "string" || presigned.url.length === 0) {
|
|
145
|
+
fail("presign", `${operation} presign did not return a URL`);
|
|
146
|
+
}
|
|
147
|
+
if (presigned.expiresAt !== undefined && presigned.expiresAt.getTime() <= Date.now()) {
|
|
148
|
+
fail("presign", `${operation} presign returned an already-expired URL`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
unsafeCalls.push(["presign", () => presignable.presign("../escape", "get")]);
|
|
152
|
+
}
|
|
153
|
+
for (const [method, call] of unsafeCalls) {
|
|
154
|
+
try {
|
|
155
|
+
await call();
|
|
156
|
+
fail("key safety", `${method} accepted a traversal key`);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error instanceof StorageAdapterConformanceError)
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await storage.delete(a);
|
|
164
|
+
if ((await storage.get(a)) !== null || (await storage.exists(a))) {
|
|
165
|
+
fail("delete", "deleted object remained readable");
|
|
166
|
+
}
|
|
167
|
+
await storage.delete(a);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof StorageAdapterConformanceError)
|
|
171
|
+
throw error;
|
|
172
|
+
fail("operation", "adapter operation rejected unexpectedly", error);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
await Promise.allSettled([storage.delete(a), storage.delete(b), storage.delete(c)]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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"}
|
package/dist/file.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { type ListOptions, type PutOptions, type StorageAdapter, type StorageData, type StorageObject } from "./types.js";
|
|
2
2
|
export declare class FileStorage implements StorageAdapter {
|
|
3
3
|
private readonly root;
|
|
4
|
+
private readonly metadataRoot;
|
|
4
5
|
constructor(root: string);
|
|
6
|
+
private metadataPathFor;
|
|
5
7
|
/** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
|
|
6
8
|
private pathFor;
|
|
7
|
-
|
|
9
|
+
private symlinkError;
|
|
10
|
+
/** Reject existing symbolic links below `base`; missing suffixes are safe for later creation. */
|
|
11
|
+
private assertNoSymlinkPath;
|
|
12
|
+
/** Create parents, re-check them, and refuse a final-component symlink at open time. */
|
|
13
|
+
private writeContained;
|
|
14
|
+
put(key: string, data: StorageData, options?: PutOptions): Promise<void>;
|
|
8
15
|
get(key: string): Promise<StorageObject | null>;
|
|
9
16
|
delete(key: string): Promise<void>;
|
|
10
17
|
exists(key: string): Promise<boolean>;
|
package/dist/file.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAA;AA4BnB,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,IAAI,EAAE,MAAM;IAOxB,OAAO,CAAC,eAAe;IAWvB,wHAAwH;IACxH,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,YAAY;IAMpB,iGAAiG;YACnF,mBAAmB;IAsBjC,wFAAwF;YAC1E,cAAc;IA0BtB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IA+B/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYrC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAU1C,IAAI;CAiBnB"}
|
package/dist/file.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local-filesystem {@link StorageAdapter} for long-running servers (Bun/Node/Deno). Keys map to paths
|
|
3
3
|
* under `root`; `assertSafeKey` plus a resolved-path containment check keep writes inside `root` (no
|
|
4
|
-
* traversal).
|
|
5
|
-
*
|
|
4
|
+
* traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
|
|
5
|
+
* objects created before sidecar support still infer content type from their extension.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { lstat, mkdir, open, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
8
9
|
import { dirname, join, posix, resolve, sep } from "node:path";
|
|
9
10
|
import { assertSafeKey, StorageKeyError } from "./key.js";
|
|
10
11
|
import { toBytes, } from "./types.js";
|
|
@@ -34,8 +35,20 @@ function inferContentType(key) {
|
|
|
34
35
|
}
|
|
35
36
|
export class FileStorage {
|
|
36
37
|
root;
|
|
38
|
+
metadataRoot;
|
|
37
39
|
constructor(root) {
|
|
38
40
|
this.root = resolve(root);
|
|
41
|
+
// Keep bookkeeping outside the object tree so every otherwise-safe key remains usable, including
|
|
42
|
+
// `.nifra-metadata/*`, and list() never needs a reserved-prefix exception.
|
|
43
|
+
this.metadataRoot = `${this.root}.nifra-metadata`;
|
|
44
|
+
}
|
|
45
|
+
metadataPathFor(key) {
|
|
46
|
+
assertSafeKey(key);
|
|
47
|
+
const full = resolve(this.metadataRoot, `${key}.json`);
|
|
48
|
+
if (!full.startsWith(this.metadataRoot + sep)) {
|
|
49
|
+
throw new StorageKeyError(`storage metadata key ${JSON.stringify(key)} escapes the storage root`);
|
|
50
|
+
}
|
|
51
|
+
return full;
|
|
39
52
|
}
|
|
40
53
|
/** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
|
|
41
54
|
pathFor(key) {
|
|
@@ -46,35 +59,128 @@ export class FileStorage {
|
|
|
46
59
|
}
|
|
47
60
|
return full;
|
|
48
61
|
}
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
symlinkError(key) {
|
|
63
|
+
return new StorageKeyError(`storage key ${JSON.stringify(key)} crosses a symbolic link beneath the storage root`);
|
|
64
|
+
}
|
|
65
|
+
/** Reject existing symbolic links below `base`; missing suffixes are safe for later creation. */
|
|
66
|
+
async assertNoSymlinkPath(base, path, key) {
|
|
67
|
+
try {
|
|
68
|
+
if ((await lstat(base)).isSymbolicLink())
|
|
69
|
+
throw this.symlinkError(key);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof StorageKeyError)
|
|
73
|
+
throw error;
|
|
74
|
+
if (error.code === "ENOENT")
|
|
75
|
+
return;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
const suffix = path.slice(base.length + sep.length);
|
|
79
|
+
let current = base;
|
|
80
|
+
for (const segment of suffix.split(sep)) {
|
|
81
|
+
current = join(current, segment);
|
|
82
|
+
try {
|
|
83
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
84
|
+
throw this.symlinkError(key);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof StorageKeyError)
|
|
88
|
+
throw error;
|
|
89
|
+
if (error.code === "ENOENT")
|
|
90
|
+
return;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Create parents, re-check them, and refuse a final-component symlink at open time. */
|
|
96
|
+
async writeContained(base, path, key, data) {
|
|
97
|
+
await this.assertNoSymlinkPath(base, path, key);
|
|
51
98
|
await mkdir(dirname(path), { recursive: true });
|
|
52
|
-
await
|
|
99
|
+
await this.assertNoSymlinkPath(base, path, key);
|
|
100
|
+
try {
|
|
101
|
+
const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o666);
|
|
102
|
+
try {
|
|
103
|
+
await handle.writeFile(data);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await handle.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error.code === "ELOOP")
|
|
111
|
+
throw this.symlinkError(key);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async put(key, data, options = {}) {
|
|
116
|
+
const path = this.pathFor(key);
|
|
117
|
+
await this.writeContained(this.root, path, key, toBytes(data));
|
|
118
|
+
const metadataPath = this.metadataPathFor(key);
|
|
119
|
+
if (options.contentType !== undefined || options.metadata !== undefined) {
|
|
120
|
+
await this.writeContained(this.metadataRoot, metadataPath, key, JSON.stringify({
|
|
121
|
+
...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
|
|
122
|
+
...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
|
|
127
|
+
await rm(metadataPath, { force: true });
|
|
128
|
+
}
|
|
53
129
|
}
|
|
54
130
|
async get(key) {
|
|
55
131
|
const path = this.pathFor(key);
|
|
132
|
+
await this.assertNoSymlinkPath(this.root, path, key);
|
|
56
133
|
let body;
|
|
57
134
|
try {
|
|
58
135
|
body = new Uint8Array(await readFile(path));
|
|
59
136
|
}
|
|
60
|
-
catch {
|
|
61
|
-
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code === "ENOENT")
|
|
139
|
+
return null;
|
|
140
|
+
throw error;
|
|
62
141
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
142
|
+
let stored = {};
|
|
143
|
+
try {
|
|
144
|
+
const metadataPath = this.metadataPathFor(key);
|
|
145
|
+
await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
|
|
146
|
+
stored = JSON.parse(await readFile(metadataPath, "utf8"));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof StorageKeyError)
|
|
150
|
+
throw error;
|
|
151
|
+
if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
// Objects created before sidecar support still infer a useful MIME type from their extension.
|
|
155
|
+
}
|
|
156
|
+
const contentType = stored.contentType ?? inferContentType(key);
|
|
157
|
+
return {
|
|
158
|
+
body,
|
|
159
|
+
size: body.byteLength,
|
|
160
|
+
...(contentType !== undefined ? { contentType } : {}),
|
|
161
|
+
...(stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
|
|
162
|
+
};
|
|
67
163
|
}
|
|
68
164
|
async delete(key) {
|
|
69
|
-
|
|
165
|
+
const path = this.pathFor(key);
|
|
166
|
+
const metadataPath = this.metadataPathFor(key);
|
|
167
|
+
await Promise.all([
|
|
168
|
+
this.assertNoSymlinkPath(this.root, path, key),
|
|
169
|
+
this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
|
|
170
|
+
]);
|
|
171
|
+
await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })]);
|
|
70
172
|
}
|
|
71
173
|
async exists(key) {
|
|
174
|
+
const path = this.pathFor(key);
|
|
175
|
+
await this.assertNoSymlinkPath(this.root, path, key);
|
|
72
176
|
try {
|
|
73
|
-
await stat(
|
|
177
|
+
await stat(path);
|
|
74
178
|
return true;
|
|
75
179
|
}
|
|
76
|
-
catch {
|
|
77
|
-
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code === "ENOENT")
|
|
182
|
+
return false;
|
|
183
|
+
throw error;
|
|
78
184
|
}
|
|
79
185
|
}
|
|
80
186
|
async list(options = {}) {
|
|
@@ -99,7 +205,9 @@ export class FileStorage {
|
|
|
99
205
|
const full = join(dir, name);
|
|
100
206
|
// POSIX-join the key segments so listed keys are portable regardless of the host separator.
|
|
101
207
|
const key = prefix === "" ? name : posix.join(prefix, name);
|
|
102
|
-
const info = await
|
|
208
|
+
const info = await lstat(full);
|
|
209
|
+
if (info.isSymbolicLink())
|
|
210
|
+
continue;
|
|
103
211
|
if (info.isDirectory())
|
|
104
212
|
await this.walk(full, key, out);
|
|
105
213
|
else if (info.isFile())
|
package/dist/file.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAClF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAML,OAAO,GACR,MAAM,YAAY,CAAA;AAEnB,MAAM,aAAa,GAAqC;IACtD,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,iBAAiB;IACtB,IAAI,EAAE,kBAAkB;IACxB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,UAAU;IACf,IAAI,EAAE,WAAW;IACjB,GAAG,EAAE,UAAU;IACf,EAAE,EAAE,iBAAiB;IACrB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,iBAAiB;CACvB,CAAA;AAED,SAAS,gBAAgB,CAAC,GAAW;IACnC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;IAC7D,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;AAC3B,CAAC;AAED,MAAM,OAAO,WAAW;IACL,IAAI,CAAQ;IACZ,YAAY,CAAQ;IAErC,YAAY,IAAY;QACtB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QACzB,iGAAiG;QACjG,2EAA2E;QAC3E,IAAI,CAAC,YAAY,GAAG,GAAG,IAAI,CAAC,IAAI,iBAAiB,CAAA;IACnD,CAAC;IAEO,eAAe,CAAC,GAAW;QACjC,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,GAAG,OAAO,CAAC,CAAA;QACtD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YAC9C,MAAM,IAAI,eAAe,CACvB,wBAAwB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,2BAA2B,CACvE,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,wHAAwH;IAChH,OAAO,CAAC,GAAW;QACzB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QACpC,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YAC5D,MAAM,IAAI,eAAe,CAAC,eAAe,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;QAC1F,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAEO,YAAY,CAAC,GAAW;QAC9B,OAAO,IAAI,eAAe,CACxB,eAAe,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,mDAAmD,CACtF,CAAA;IACH,CAAC;IAED,iGAAiG;IACzF,KAAK,CAAC,mBAAmB,CAAC,IAAY,EAAE,IAAY,EAAE,GAAW;QACvE,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,EAAE;gBAAE,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAA;QACxE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,eAAe;gBAAE,MAAM,KAAK,CAAA;YACjD,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAM;YAC9D,MAAM,KAAK,CAAA;QACb,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAA;QACnD,IAAI,OAAO,GAAG,IAAI,CAAA;QAClB,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACxC,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAChC,IAAI,CAAC;gBACH,IAAI,CAAC,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,EAAE;oBAAE,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAA;YAC3E,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,eAAe;oBAAE,MAAM,KAAK,CAAA;gBACjD,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,OAAM;gBAC9D,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC;IACH,CAAC;IAED,wFAAwF;IAChF,KAAK,CAAC,cAAc,CAC1B,IAAY,EACZ,IAAY,EACZ,GAAW,EACX,IAAyB;QAEzB,MAAM,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QAC/C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QAC/C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CACvB,IAAI,EACJ,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,UAAU,EACjF,KAAK,CACN,CAAA;YACD,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;YAC9B,CAAC;oBAAS,CAAC;gBACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;YACtB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,OAAO;gBAAE,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAA;YACnF,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,IAAiB,EAAE,UAAsB,EAAE;QAChE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC9B,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;QAC9D,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;QAC9C,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACxE,MAAM,IAAI,CAAC,cAAc,CACvB,IAAI,CAAC,YAAY,EACjB,YAAY,EACZ,GAAG,EACH,IAAI,CAAC,SAAS,CAAC;gBACb,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClF,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1E,CAAC,CACH,CAAA;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,CAAC,CAAA;YACpE,MAAM,EAAE,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACzC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC9B,MAAM,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QACpD,IAAI,IAAgB,CAAA;QACpB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAA;YACnE,MAAM,KAAK,CAAA;QACb,CAAC;QACD,IAAI,MAAM,GAA0E,EAAE,CAAA;QACtF,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;YAC9C,MAAM,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,CAAC,CAAA;YACpE,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAkB,CAAA;QAC5E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,eAAe;gBAAE,MAAM,KAAK,CAAA;YACjD,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,YAAY,WAAW,CAAC,EAAE,CAAC;gBAC1F,MAAM,KAAK,CAAA;YACb,CAAC;YACD,8FAA8F;QAChG,CAAC;QACD,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAA;QAC/D,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,IAAI,CAAC,UAAU;YACrB,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxE,CAAA;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC9B,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;QAC9C,MAAM,OAAO,CAAC,GAAG,CAAC;YAChB,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC;YAC9C,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,CAAC;SAC/D,CAAC,CAAA;QACF,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IACnF,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC9B,MAAM,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QACpD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA;YAChB,OAAO,IAAI,CAAA;QACb,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,KAAK,CAAA;YACpE,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAuB,EAAE;QAClC,MAAM,IAAI,GAAa,EAAE,CAAA;QACzB,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;QACpC,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;QACrB,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;YAC9B,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,MAAgB,CAAC,CAAC,CAAA;QACjE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;YAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QAClE,OAAO,GAAG,CAAA;IACZ,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,MAAc,EAAE,GAAa;QAC3D,IAAI,KAAe,CAAA;QACnB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAA;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAM,CAAC,+BAA+B;QACxC,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;YAC5B,4FAA4F;YAC5F,MAAM,GAAG,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YAC3D,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAA;YAC9B,IAAI,IAAI,CAAC,cAAc,EAAE;gBAAE,SAAQ;YACnC,IAAI,IAAI,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;iBAClD,IAAI,IAAI,CAAC,MAAM,EAAE;gBAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACvC,CAAC;IACH,CAAC;CACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* await storage.put("avatars/u1.png", bytes, { contentType: "image/png" })
|
|
10
10
|
* const object = await storage.get("avatars/u1.png")
|
|
11
11
|
*/
|
|
12
|
+
export { assertStorageAdapterConformance, StorageAdapterConformanceError, type StorageAdapterConformanceOptions, } from "./conformance.js";
|
|
12
13
|
export { FileStorage } from "./file.js";
|
|
13
14
|
export { assertSafeKey, StorageKeyError } from "./key.js";
|
|
14
15
|
export { MemoryStorage } from "./memory.js";
|
|
15
16
|
export { type R2BucketLike, type R2ObjectLike, R2Storage } from "./r2.js";
|
|
16
|
-
export { type ListOptions, type PutOptions, type StorageAdapter, type StorageData, type StorageObject, toBytes, } from "./types.js";
|
|
17
|
+
export { type ListOptions, type MovableStorageAdapter, type PagedStorageAdapter, type PresignableStorageAdapter, type PutOptions, type StorageAdapter, type StorageData, type StorageListPage, type StorageListPageOptions, type StorageObject, type StoragePresignedUrl, type StoragePresignOperation, type StoragePresignOptions, toBytes, } from "./types.js";
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,OAAO,GACR,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,EAC9B,KAAK,gCAAgC,GACtC,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,OAAO,GACR,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* await storage.put("avatars/u1.png", bytes, { contentType: "image/png" })
|
|
10
10
|
* const object = await storage.get("avatars/u1.png")
|
|
11
11
|
*/
|
|
12
|
+
export { assertStorageAdapterConformance, StorageAdapterConformanceError, } from "./conformance.js";
|
|
12
13
|
export { FileStorage } from "./file.js";
|
|
13
14
|
export { assertSafeKey, StorageKeyError } from "./key.js";
|
|
14
15
|
export { MemoryStorage } from "./memory.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAwC,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,GAE/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAwC,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EAcL,OAAO,GACR,MAAM,YAAY,CAAA"}
|
package/dist/key.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* portable across them, and so the check can't be forgotten on the one adapter where it's a vulnerability.
|
|
6
6
|
*/
|
|
7
7
|
export declare class StorageKeyError extends Error {
|
|
8
|
-
|
|
8
|
+
constructor(message: string);
|
|
9
9
|
}
|
|
10
10
|
/** Throw {@link StorageKeyError} unless `key` is a safe relative storage key. */
|
|
11
11
|
export declare function assertSafeKey(key: string): void;
|
package/dist/key.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qBAAa,eAAgB,SAAQ,KAAK;
|
|
1
|
+
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAc/C"}
|
package/dist/key.js
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* portable across them, and so the check can't be forgotten on the one adapter where it's a vulnerability.
|
|
6
6
|
*/
|
|
7
7
|
export class StorageKeyError extends Error {
|
|
8
|
-
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "StorageKeyError";
|
|
11
|
+
}
|
|
9
12
|
}
|
|
10
13
|
/** Throw {@link StorageKeyError} unless `key` is a safe relative storage key. */
|
|
11
14
|
export function assertSafeKey(key) {
|
package/dist/key.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,OAAO,eAAgB,SAAQ,KAAK;
|
|
1
|
+
{"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;IAC/B,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,eAAe,CAAC,yCAAyC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAA;IACnF,CAAC;IACD,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,eAAe,CACvB,sBAAsB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,gCAAgC,CAC1E,CAAA;IACH,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,IAAI,eAAe,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;QAC9F,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/dist/memory.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ListOptions, type PutOptions, type StorageAdapter, type StorageData, type StorageObject } from "./types.js";
|
|
2
2
|
export declare class MemoryStorage implements StorageAdapter {
|
|
3
3
|
private readonly objects;
|
|
4
|
+
constructor();
|
|
4
5
|
put(key: string, data: StorageData, options?: PutOptions): Promise<void>;
|
|
5
6
|
get(key: string): Promise<StorageObject | null>;
|
|
6
7
|
delete(key: string): Promise<void>;
|
package/dist/memory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAA;AAQnB,qBAAa,aAAc,YAAW,cAAc;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAA;AAQnB,qBAAa,aAAc,YAAW,cAAc;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;;IAMtC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAY/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAMzD"}
|
package/dist/memory.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import { assertSafeKey } from "./key.js";
|
|
7
7
|
import { toBytes, } from "./types.js";
|
|
8
8
|
export class MemoryStorage {
|
|
9
|
-
objects
|
|
9
|
+
objects;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.objects = new Map();
|
|
12
|
+
}
|
|
10
13
|
async put(key, data, options = {}) {
|
|
11
14
|
assertSafeKey(key);
|
|
12
15
|
const entry = { body: toBytes(data) };
|
package/dist/memory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAML,OAAO,GACR,MAAM,YAAY,CAAA;AAQnB,MAAM,OAAO,aAAa;IACP,OAAO,GAAG,IAAI,GAAG,
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAML,OAAO,GACR,MAAM,YAAY,CAAA;AAQnB,MAAM,OAAO,aAAa;IACP,OAAO,CAAoB;IAE5C;QACE,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,IAAiB,EAAE,UAAsB,EAAE;QAChE,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,KAAK,GAAU,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAA;QAC5C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;YACpB,GAAG,KAAK;YACR,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClF,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1E,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAA;QACpC,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU;YAC3B,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtE,CAAA;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAuB,EAAE;QAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;QACnC,IAAI,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC9E,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACpE,OAAO,IAAI,CAAA;IACb,CAAC;CACF"}
|
package/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,31 @@ export interface ListOptions {
|
|
|
25
25
|
/** Cap the number of keys returned. */
|
|
26
26
|
readonly limit?: number;
|
|
27
27
|
}
|
|
28
|
+
/** One page of keys from stores that expose cursor-based listing. */
|
|
29
|
+
export interface StorageListPage {
|
|
30
|
+
readonly keys: readonly string[];
|
|
31
|
+
/** Opaque cursor for the next page; absent when this is the final page. */
|
|
32
|
+
readonly cursor?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Cursor-aware listing options. `cursor` is adapter-owned and must be treated as opaque. */
|
|
35
|
+
export interface StorageListPageOptions extends ListOptions {
|
|
36
|
+
readonly cursor?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Operation represented by a presigned storage URL. */
|
|
39
|
+
export type StoragePresignOperation = "get" | "put";
|
|
40
|
+
/** Mechanical constraints applied while minting a presigned URL. */
|
|
41
|
+
export interface StoragePresignOptions {
|
|
42
|
+
readonly expiresInSeconds?: number;
|
|
43
|
+
/** For PUT URLs, pin the declared MIME type. */
|
|
44
|
+
readonly contentType?: string;
|
|
45
|
+
/** For PUT URLs, pin the exact request-body size. */
|
|
46
|
+
readonly contentLength?: number;
|
|
47
|
+
}
|
|
48
|
+
/** A provider-minted URL and its known expiry. */
|
|
49
|
+
export interface StoragePresignedUrl {
|
|
50
|
+
readonly url: string;
|
|
51
|
+
readonly expiresAt?: Date;
|
|
52
|
+
}
|
|
28
53
|
/**
|
|
29
54
|
* A blob store keyed by string. Keys are POSIX-ish paths (`avatars/u1.png`); every adapter rejects unsafe
|
|
30
55
|
* keys (absolute, `..` traversal, NUL, backslash) so a key valid in one adapter is valid in all. All
|
|
@@ -42,6 +67,19 @@ export interface StorageAdapter {
|
|
|
42
67
|
/** List stored keys (optionally filtered by prefix + capped). */
|
|
43
68
|
list(options?: ListOptions): Promise<string[]>;
|
|
44
69
|
}
|
|
70
|
+
/** Optional cursor-listing capability. Kept out of {@link StorageAdapter} for simple stores. */
|
|
71
|
+
export interface PagedStorageAdapter extends StorageAdapter {
|
|
72
|
+
listPage(options?: StorageListPageOptions): Promise<StorageListPage>;
|
|
73
|
+
}
|
|
74
|
+
/** Optional provider-side URL-signing capability. Asset sensitivity and TTL policy stay with callers. */
|
|
75
|
+
export interface PresignableStorageAdapter extends StorageAdapter {
|
|
76
|
+
presign(key: string, operation: StoragePresignOperation, options?: StoragePresignOptions): Promise<StoragePresignedUrl>;
|
|
77
|
+
}
|
|
78
|
+
/** Optional server-side copy/move capability. */
|
|
79
|
+
export interface MovableStorageAdapter extends StorageAdapter {
|
|
80
|
+
copy(sourceKey: string, destinationKey: string): Promise<void>;
|
|
81
|
+
move(sourceKey: string, destinationKey: string): Promise<void>;
|
|
82
|
+
}
|
|
45
83
|
/** Normalize any accepted payload to bytes. */
|
|
46
84
|
export declare function toBytes(data: StorageData): Uint8Array;
|
|
47
85
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qEAAqE;AACrE,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,CAAA;AAE3D,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACrD;AAED,sGAAsG;AACtG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACrD;AAED,MAAM,WAAW,WAAW;IAC1B,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,uCAAuC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,+DAA+D;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC/C,+CAA+C;IAC/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,yCAAyC;IACzC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,iEAAiE;IACjE,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CAC/C;AAED,+CAA+C;AAC/C,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAIrD"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA+FH,+CAA+C;AAC/C,MAAM,UAAU,OAAO,CAAC,IAAiB;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnE,IAAI,IAAI,YAAY,UAAU;QAAE,OAAO,IAAI,CAAA;IAC3C,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;AAC7B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/storage",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Blob storage for nifra —
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Blob storage mechanics for nifra — traversal-safe memory, filesystem, and R2 adapters, optional provider capabilities, and executable adapter conformance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
7
7
|
"typescript",
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MovableStorageAdapter,
|
|
3
|
+
PagedStorageAdapter,
|
|
4
|
+
PresignableStorageAdapter,
|
|
5
|
+
StorageAdapter,
|
|
6
|
+
} from "./types.ts"
|
|
7
|
+
|
|
8
|
+
/** Construction and cleanup hooks for {@link assertStorageAdapterConformance}. */
|
|
9
|
+
export interface StorageAdapterConformanceOptions {
|
|
10
|
+
/** Return an empty, isolated adapter for this run. */
|
|
11
|
+
readonly createAdapter: () => StorageAdapter | Promise<StorageAdapter>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A failed invariant reported by {@link assertStorageAdapterConformance}. */
|
|
15
|
+
export class StorageAdapterConformanceError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
readonly check: string,
|
|
18
|
+
message: string,
|
|
19
|
+
cause?: unknown,
|
|
20
|
+
) {
|
|
21
|
+
super(`StorageAdapter conformance failed (${check}): ${message}`, { cause })
|
|
22
|
+
this.name = "StorageAdapterConformanceError"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fail = (check: string, message: string, cause?: unknown): never => {
|
|
27
|
+
throw new StorageAdapterConformanceError(check, message, cause)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const decode = (bytes: Uint8Array): string => new TextDecoder().decode(bytes)
|
|
31
|
+
|
|
32
|
+
const hasPaging = (storage: StorageAdapter): storage is PagedStorageAdapter =>
|
|
33
|
+
typeof (storage as PagedStorageAdapter).listPage === "function"
|
|
34
|
+
|
|
35
|
+
const hasPresign = (storage: StorageAdapter): storage is PresignableStorageAdapter =>
|
|
36
|
+
typeof (storage as PresignableStorageAdapter).presign === "function"
|
|
37
|
+
|
|
38
|
+
const hasMove = (storage: StorageAdapter): storage is MovableStorageAdapter =>
|
|
39
|
+
typeof (storage as MovableStorageAdapter).copy === "function" &&
|
|
40
|
+
typeof (storage as MovableStorageAdapter).move === "function"
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute the observable {@link StorageAdapter} contract without depending on a test runner.
|
|
44
|
+
*
|
|
45
|
+
* The adapter must be isolated because this writes deterministic conformance keys. The promise resolves
|
|
46
|
+
* only after proving byte + metadata round-trips, overwrite, missing/delete semantics, prefix/limit listing,
|
|
47
|
+
* and uniform unsafe-key rejection across every key-taking method. When the adapter also implements an
|
|
48
|
+
* optional capability ({@link PagedStorageAdapter}, {@link PresignableStorageAdapter},
|
|
49
|
+
* {@link MovableStorageAdapter}), that capability's contract is proven too — implementing a capability
|
|
50
|
+
* partially or unsafely fails conformance rather than silently escaping it.
|
|
51
|
+
*/
|
|
52
|
+
export async function assertStorageAdapterConformance(
|
|
53
|
+
options: StorageAdapterConformanceOptions,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
let storage: StorageAdapter
|
|
56
|
+
try {
|
|
57
|
+
storage = await options.createAdapter()
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return fail("construction", "createAdapter failed", error)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const a = "nifra-conformance/a/one.txt"
|
|
63
|
+
const b = "nifra-conformance/a/two.bin"
|
|
64
|
+
const c = "nifra-conformance/b/three.bin"
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await storage.put(a, "one", {
|
|
68
|
+
contentType: "text/x-nifra-conformance",
|
|
69
|
+
metadata: { owner: "nifra", purpose: "conformance" },
|
|
70
|
+
})
|
|
71
|
+
const object = (await storage.get(a)) ?? fail("round trip", "put object was not readable")
|
|
72
|
+
if (!(object.body instanceof Uint8Array)) fail("round trip", "body must be Uint8Array")
|
|
73
|
+
if (decode(object.body) !== "one" || object.size !== 3) {
|
|
74
|
+
fail("round trip", "body or size changed during round-trip")
|
|
75
|
+
}
|
|
76
|
+
if (object.contentType !== "text/x-nifra-conformance") {
|
|
77
|
+
fail("metadata", "contentType did not round-trip")
|
|
78
|
+
}
|
|
79
|
+
if (object.metadata?.owner !== "nifra" || object.metadata.purpose !== "conformance") {
|
|
80
|
+
fail("metadata", "custom metadata did not round-trip")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!(await storage.exists(a))) fail("exists", "stored key was reported missing")
|
|
84
|
+
if (await storage.exists("nifra-conformance/missing")) {
|
|
85
|
+
fail("exists", "missing key was reported present")
|
|
86
|
+
}
|
|
87
|
+
if ((await storage.get("nifra-conformance/missing")) !== null) {
|
|
88
|
+
fail("missing", "get must return null for a missing key")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await storage.put(a, "overwritten")
|
|
92
|
+
const overwritten = await storage.get(a)
|
|
93
|
+
if (overwritten === null || decode(overwritten.body) !== "overwritten") {
|
|
94
|
+
fail("overwrite", "put did not replace the existing body")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await storage.put(b, "two")
|
|
98
|
+
await storage.put(c, "three")
|
|
99
|
+
const prefixed = [...(await storage.list({ prefix: "nifra-conformance/a/" }))].sort()
|
|
100
|
+
if (prefixed.length !== 2 || prefixed[0] !== a || prefixed[1] !== b) {
|
|
101
|
+
fail("list prefix", "prefix listing returned unexpected keys")
|
|
102
|
+
}
|
|
103
|
+
if ((await storage.list({ prefix: "nifra-conformance/", limit: 1 })).length !== 1) {
|
|
104
|
+
fail("list limit", "limit was not respected")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const unsafeCalls: Array<readonly [string, () => Promise<unknown>]> = [
|
|
108
|
+
["put", () => storage.put("../escape", "x")],
|
|
109
|
+
["get", () => storage.get("../escape")],
|
|
110
|
+
["delete", () => storage.delete("../escape")],
|
|
111
|
+
["exists", () => storage.exists("../escape")],
|
|
112
|
+
]
|
|
113
|
+
if (hasPaging(storage)) {
|
|
114
|
+
const paged = storage
|
|
115
|
+
|
|
116
|
+
// The cursor walk must reproduce exactly the keys list() reports, without duplicates,
|
|
117
|
+
// and must terminate by omitting the cursor on the final page.
|
|
118
|
+
const all = [...(await paged.list({ prefix: "nifra-conformance/" }))].sort()
|
|
119
|
+
const walked: string[] = []
|
|
120
|
+
let cursor: string | undefined
|
|
121
|
+
for (let page = 0; page <= all.length; page++) {
|
|
122
|
+
const result = await paged.listPage({
|
|
123
|
+
prefix: "nifra-conformance/",
|
|
124
|
+
limit: 1,
|
|
125
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
126
|
+
})
|
|
127
|
+
if (result.keys.length > 1) fail("listPage limit", "page exceeded the requested limit")
|
|
128
|
+
walked.push(...result.keys)
|
|
129
|
+
cursor = result.cursor
|
|
130
|
+
if (cursor === undefined) break
|
|
131
|
+
}
|
|
132
|
+
if (cursor !== undefined) fail("listPage cursor", "cursor never terminated")
|
|
133
|
+
const sortedWalk = [...walked].sort()
|
|
134
|
+
if (new Set(walked).size !== walked.length) {
|
|
135
|
+
fail("listPage cursor", "cursor walk returned duplicate keys")
|
|
136
|
+
}
|
|
137
|
+
if (sortedWalk.length !== all.length || sortedWalk.some((key, i) => key !== all[i])) {
|
|
138
|
+
fail("listPage cursor", "cursor walk and list() disagree on the key set")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (hasMove(storage)) {
|
|
143
|
+
const movable = storage
|
|
144
|
+
const copied = "nifra-conformance/b/copied.bin"
|
|
145
|
+
const moved = "nifra-conformance/b/moved.bin"
|
|
146
|
+
|
|
147
|
+
await movable.copy(c, copied)
|
|
148
|
+
const copyTarget = await movable.get(copied)
|
|
149
|
+
const copySource = await movable.get(c)
|
|
150
|
+
if (copyTarget === null || decode(copyTarget.body) !== "three") {
|
|
151
|
+
fail("copy", "copied object is missing or has different bytes")
|
|
152
|
+
}
|
|
153
|
+
if (copySource === null) fail("copy", "copy must not remove the source object")
|
|
154
|
+
|
|
155
|
+
await movable.move(copied, moved)
|
|
156
|
+
const moveTarget = await movable.get(moved)
|
|
157
|
+
if (moveTarget === null || decode(moveTarget.body) !== "three") {
|
|
158
|
+
fail("move", "moved object is missing or has different bytes")
|
|
159
|
+
}
|
|
160
|
+
if ((await movable.exists(copied)) || (await movable.get(copied)) !== null) {
|
|
161
|
+
fail("move", "move must remove the source object")
|
|
162
|
+
}
|
|
163
|
+
await movable.delete(moved)
|
|
164
|
+
|
|
165
|
+
unsafeCalls.push(
|
|
166
|
+
["copy source", () => movable.copy("../escape", copied)],
|
|
167
|
+
["copy destination", () => movable.copy(c, "../escape")],
|
|
168
|
+
["move source", () => movable.move("../escape", moved)],
|
|
169
|
+
["move destination", () => movable.move(c, "../escape")],
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (hasPresign(storage)) {
|
|
174
|
+
const presignable = storage
|
|
175
|
+
for (const operation of ["get", "put"] as const) {
|
|
176
|
+
const presigned = await presignable.presign(a, operation, {
|
|
177
|
+
expiresInSeconds: 60,
|
|
178
|
+
...(operation === "put" ? { contentType: "text/plain", contentLength: 3 } : {}),
|
|
179
|
+
})
|
|
180
|
+
if (typeof presigned.url !== "string" || presigned.url.length === 0) {
|
|
181
|
+
fail("presign", `${operation} presign did not return a URL`)
|
|
182
|
+
}
|
|
183
|
+
if (presigned.expiresAt !== undefined && presigned.expiresAt.getTime() <= Date.now()) {
|
|
184
|
+
fail("presign", `${operation} presign returned an already-expired URL`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
unsafeCalls.push(["presign", () => presignable.presign("../escape", "get")])
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const [method, call] of unsafeCalls) {
|
|
191
|
+
try {
|
|
192
|
+
await call()
|
|
193
|
+
fail("key safety", `${method} accepted a traversal key`)
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error instanceof StorageAdapterConformanceError) throw error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await storage.delete(a)
|
|
200
|
+
if ((await storage.get(a)) !== null || (await storage.exists(a))) {
|
|
201
|
+
fail("delete", "deleted object remained readable")
|
|
202
|
+
}
|
|
203
|
+
await storage.delete(a)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof StorageAdapterConformanceError) throw error
|
|
206
|
+
fail("operation", "adapter operation rejected unexpectedly", error)
|
|
207
|
+
} finally {
|
|
208
|
+
await Promise.allSettled([storage.delete(a), storage.delete(b), storage.delete(c)])
|
|
209
|
+
}
|
|
210
|
+
}
|
package/src/file.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local-filesystem {@link StorageAdapter} for long-running servers (Bun/Node/Deno). Keys map to paths
|
|
3
3
|
* under `root`; `assertSafeKey` plus a resolved-path containment check keep writes inside `root` (no
|
|
4
|
-
* traversal).
|
|
5
|
-
*
|
|
4
|
+
* traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
|
|
5
|
+
* objects created before sidecar support still infer content type from their extension.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { constants } from "node:fs"
|
|
8
|
+
import { lstat, mkdir, open, readdir, readFile, rm, stat } from "node:fs/promises"
|
|
8
9
|
import { dirname, join, posix, resolve, sep } from "node:path"
|
|
9
10
|
import { assertSafeKey, StorageKeyError } from "./key.ts"
|
|
10
11
|
import {
|
|
@@ -44,9 +45,24 @@ function inferContentType(key: string): string | undefined {
|
|
|
44
45
|
|
|
45
46
|
export class FileStorage implements StorageAdapter {
|
|
46
47
|
private readonly root: string
|
|
48
|
+
private readonly metadataRoot: string
|
|
47
49
|
|
|
48
50
|
constructor(root: string) {
|
|
49
51
|
this.root = resolve(root)
|
|
52
|
+
// Keep bookkeeping outside the object tree so every otherwise-safe key remains usable, including
|
|
53
|
+
// `.nifra-metadata/*`, and list() never needs a reserved-prefix exception.
|
|
54
|
+
this.metadataRoot = `${this.root}.nifra-metadata`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private metadataPathFor(key: string): string {
|
|
58
|
+
assertSafeKey(key)
|
|
59
|
+
const full = resolve(this.metadataRoot, `${key}.json`)
|
|
60
|
+
if (!full.startsWith(this.metadataRoot + sep)) {
|
|
61
|
+
throw new StorageKeyError(
|
|
62
|
+
`storage metadata key ${JSON.stringify(key)} escapes the storage root`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
return full
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
/** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
|
|
@@ -59,36 +75,132 @@ export class FileStorage implements StorageAdapter {
|
|
|
59
75
|
return full
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
private symlinkError(key: string): StorageKeyError {
|
|
79
|
+
return new StorageKeyError(
|
|
80
|
+
`storage key ${JSON.stringify(key)} crosses a symbolic link beneath the storage root`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Reject existing symbolic links below `base`; missing suffixes are safe for later creation. */
|
|
85
|
+
private async assertNoSymlinkPath(base: string, path: string, key: string): Promise<void> {
|
|
86
|
+
try {
|
|
87
|
+
if ((await lstat(base)).isSymbolicLink()) throw this.symlinkError(key)
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof StorageKeyError) throw error
|
|
90
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
const suffix = path.slice(base.length + sep.length)
|
|
94
|
+
let current = base
|
|
95
|
+
for (const segment of suffix.split(sep)) {
|
|
96
|
+
current = join(current, segment)
|
|
97
|
+
try {
|
|
98
|
+
if ((await lstat(current)).isSymbolicLink()) throw this.symlinkError(key)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof StorageKeyError) throw error
|
|
101
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Create parents, re-check them, and refuse a final-component symlink at open time. */
|
|
108
|
+
private async writeContained(
|
|
109
|
+
base: string,
|
|
110
|
+
path: string,
|
|
111
|
+
key: string,
|
|
112
|
+
data: Uint8Array | string,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
await this.assertNoSymlinkPath(base, path, key)
|
|
64
115
|
await mkdir(dirname(path), { recursive: true })
|
|
65
|
-
await
|
|
116
|
+
await this.assertNoSymlinkPath(base, path, key)
|
|
117
|
+
try {
|
|
118
|
+
const handle = await open(
|
|
119
|
+
path,
|
|
120
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW,
|
|
121
|
+
0o666,
|
|
122
|
+
)
|
|
123
|
+
try {
|
|
124
|
+
await handle.writeFile(data)
|
|
125
|
+
} finally {
|
|
126
|
+
await handle.close()
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP") throw this.symlinkError(key)
|
|
130
|
+
throw error
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async put(key: string, data: StorageData, options: PutOptions = {}): Promise<void> {
|
|
135
|
+
const path = this.pathFor(key)
|
|
136
|
+
await this.writeContained(this.root, path, key, toBytes(data))
|
|
137
|
+
const metadataPath = this.metadataPathFor(key)
|
|
138
|
+
if (options.contentType !== undefined || options.metadata !== undefined) {
|
|
139
|
+
await this.writeContained(
|
|
140
|
+
this.metadataRoot,
|
|
141
|
+
metadataPath,
|
|
142
|
+
key,
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
|
|
145
|
+
...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
} else {
|
|
149
|
+
await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key)
|
|
150
|
+
await rm(metadataPath, { force: true })
|
|
151
|
+
}
|
|
66
152
|
}
|
|
67
153
|
|
|
68
154
|
async get(key: string): Promise<StorageObject | null> {
|
|
69
155
|
const path = this.pathFor(key)
|
|
156
|
+
await this.assertNoSymlinkPath(this.root, path, key)
|
|
70
157
|
let body: Uint8Array
|
|
71
158
|
try {
|
|
72
159
|
body = new Uint8Array(await readFile(path))
|
|
73
|
-
} catch {
|
|
74
|
-
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null
|
|
162
|
+
throw error
|
|
163
|
+
}
|
|
164
|
+
let stored: { contentType?: string; metadata?: Readonly<Record<string, string>> } = {}
|
|
165
|
+
try {
|
|
166
|
+
const metadataPath = this.metadataPathFor(key)
|
|
167
|
+
await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key)
|
|
168
|
+
stored = JSON.parse(await readFile(metadataPath, "utf8")) as typeof stored
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof StorageKeyError) throw error
|
|
171
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !(error instanceof SyntaxError)) {
|
|
172
|
+
throw error
|
|
173
|
+
}
|
|
174
|
+
// Objects created before sidecar support still infer a useful MIME type from their extension.
|
|
175
|
+
}
|
|
176
|
+
const contentType = stored.contentType ?? inferContentType(key)
|
|
177
|
+
return {
|
|
178
|
+
body,
|
|
179
|
+
size: body.byteLength,
|
|
180
|
+
...(contentType !== undefined ? { contentType } : {}),
|
|
181
|
+
...(stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
|
|
75
182
|
}
|
|
76
|
-
const contentType = inferContentType(key)
|
|
77
|
-
return contentType === undefined
|
|
78
|
-
? { body, size: body.byteLength }
|
|
79
|
-
: { body, size: body.byteLength, contentType }
|
|
80
183
|
}
|
|
81
184
|
|
|
82
185
|
async delete(key: string): Promise<void> {
|
|
83
|
-
|
|
186
|
+
const path = this.pathFor(key)
|
|
187
|
+
const metadataPath = this.metadataPathFor(key)
|
|
188
|
+
await Promise.all([
|
|
189
|
+
this.assertNoSymlinkPath(this.root, path, key),
|
|
190
|
+
this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
|
|
191
|
+
])
|
|
192
|
+
await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })])
|
|
84
193
|
}
|
|
85
194
|
|
|
86
195
|
async exists(key: string): Promise<boolean> {
|
|
196
|
+
const path = this.pathFor(key)
|
|
197
|
+
await this.assertNoSymlinkPath(this.root, path, key)
|
|
87
198
|
try {
|
|
88
|
-
await stat(
|
|
199
|
+
await stat(path)
|
|
89
200
|
return true
|
|
90
|
-
} catch {
|
|
91
|
-
return false
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false
|
|
203
|
+
throw error
|
|
92
204
|
}
|
|
93
205
|
}
|
|
94
206
|
|
|
@@ -113,7 +225,8 @@ export class FileStorage implements StorageAdapter {
|
|
|
113
225
|
const full = join(dir, name)
|
|
114
226
|
// POSIX-join the key segments so listed keys are portable regardless of the host separator.
|
|
115
227
|
const key = prefix === "" ? name : posix.join(prefix, name)
|
|
116
|
-
const info = await
|
|
228
|
+
const info = await lstat(full)
|
|
229
|
+
if (info.isSymbolicLink()) continue
|
|
117
230
|
if (info.isDirectory()) await this.walk(full, key, out)
|
|
118
231
|
else if (info.isFile()) out.push(key)
|
|
119
232
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,15 +10,28 @@
|
|
|
10
10
|
* const object = await storage.get("avatars/u1.png")
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
export {
|
|
14
|
+
assertStorageAdapterConformance,
|
|
15
|
+
StorageAdapterConformanceError,
|
|
16
|
+
type StorageAdapterConformanceOptions,
|
|
17
|
+
} from "./conformance.ts"
|
|
13
18
|
export { FileStorage } from "./file.ts"
|
|
14
19
|
export { assertSafeKey, StorageKeyError } from "./key.ts"
|
|
15
20
|
export { MemoryStorage } from "./memory.ts"
|
|
16
21
|
export { type R2BucketLike, type R2ObjectLike, R2Storage } from "./r2.ts"
|
|
17
22
|
export {
|
|
18
23
|
type ListOptions,
|
|
24
|
+
type MovableStorageAdapter,
|
|
25
|
+
type PagedStorageAdapter,
|
|
26
|
+
type PresignableStorageAdapter,
|
|
19
27
|
type PutOptions,
|
|
20
28
|
type StorageAdapter,
|
|
21
29
|
type StorageData,
|
|
30
|
+
type StorageListPage,
|
|
31
|
+
type StorageListPageOptions,
|
|
22
32
|
type StorageObject,
|
|
33
|
+
type StoragePresignedUrl,
|
|
34
|
+
type StoragePresignOperation,
|
|
35
|
+
type StoragePresignOptions,
|
|
23
36
|
toBytes,
|
|
24
37
|
} from "./types.ts"
|
package/src/key.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export class StorageKeyError extends Error {
|
|
9
|
-
|
|
9
|
+
constructor(message: string) {
|
|
10
|
+
super(message)
|
|
11
|
+
this.name = "StorageKeyError"
|
|
12
|
+
}
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
/** Throw {@link StorageKeyError} unless `key` is a safe relative storage key. */
|
package/src/memory.ts
CHANGED
|
@@ -20,7 +20,11 @@ interface Entry {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class MemoryStorage implements StorageAdapter {
|
|
23
|
-
private readonly objects
|
|
23
|
+
private readonly objects: Map<string, Entry>
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this.objects = new Map()
|
|
27
|
+
}
|
|
24
28
|
|
|
25
29
|
async put(key: string, data: StorageData, options: PutOptions = {}): Promise<void> {
|
|
26
30
|
assertSafeKey(key)
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,36 @@ export interface ListOptions {
|
|
|
30
30
|
readonly limit?: number
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** One page of keys from stores that expose cursor-based listing. */
|
|
34
|
+
export interface StorageListPage {
|
|
35
|
+
readonly keys: readonly string[]
|
|
36
|
+
/** Opaque cursor for the next page; absent when this is the final page. */
|
|
37
|
+
readonly cursor?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Cursor-aware listing options. `cursor` is adapter-owned and must be treated as opaque. */
|
|
41
|
+
export interface StorageListPageOptions extends ListOptions {
|
|
42
|
+
readonly cursor?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Operation represented by a presigned storage URL. */
|
|
46
|
+
export type StoragePresignOperation = "get" | "put"
|
|
47
|
+
|
|
48
|
+
/** Mechanical constraints applied while minting a presigned URL. */
|
|
49
|
+
export interface StoragePresignOptions {
|
|
50
|
+
readonly expiresInSeconds?: number
|
|
51
|
+
/** For PUT URLs, pin the declared MIME type. */
|
|
52
|
+
readonly contentType?: string
|
|
53
|
+
/** For PUT URLs, pin the exact request-body size. */
|
|
54
|
+
readonly contentLength?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A provider-minted URL and its known expiry. */
|
|
58
|
+
export interface StoragePresignedUrl {
|
|
59
|
+
readonly url: string
|
|
60
|
+
readonly expiresAt?: Date
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
/**
|
|
34
64
|
* A blob store keyed by string. Keys are POSIX-ish paths (`avatars/u1.png`); every adapter rejects unsafe
|
|
35
65
|
* keys (absolute, `..` traversal, NUL, backslash) so a key valid in one adapter is valid in all. All
|
|
@@ -48,6 +78,26 @@ export interface StorageAdapter {
|
|
|
48
78
|
list(options?: ListOptions): Promise<string[]>
|
|
49
79
|
}
|
|
50
80
|
|
|
81
|
+
/** Optional cursor-listing capability. Kept out of {@link StorageAdapter} for simple stores. */
|
|
82
|
+
export interface PagedStorageAdapter extends StorageAdapter {
|
|
83
|
+
listPage(options?: StorageListPageOptions): Promise<StorageListPage>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Optional provider-side URL-signing capability. Asset sensitivity and TTL policy stay with callers. */
|
|
87
|
+
export interface PresignableStorageAdapter extends StorageAdapter {
|
|
88
|
+
presign(
|
|
89
|
+
key: string,
|
|
90
|
+
operation: StoragePresignOperation,
|
|
91
|
+
options?: StoragePresignOptions,
|
|
92
|
+
): Promise<StoragePresignedUrl>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Optional server-side copy/move capability. */
|
|
96
|
+
export interface MovableStorageAdapter extends StorageAdapter {
|
|
97
|
+
copy(sourceKey: string, destinationKey: string): Promise<void>
|
|
98
|
+
move(sourceKey: string, destinationKey: string): Promise<void>
|
|
99
|
+
}
|
|
100
|
+
|
|
51
101
|
/** Normalize any accepted payload to bytes. */
|
|
52
102
|
export function toBytes(data: StorageData): Uint8Array {
|
|
53
103
|
if (typeof data === "string") return new TextEncoder().encode(data)
|