@nifrajs/storage 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/file.d.ts +5 -0
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +85 -14
- package/dist/file.js.map +1 -1
- package/package.json +1 -1
- package/src/file.ts +86 -14
package/dist/file.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export declare class FileStorage implements StorageAdapter {
|
|
|
6
6
|
private metadataPathFor;
|
|
7
7
|
/** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
|
|
8
8
|
private pathFor;
|
|
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;
|
|
9
14
|
put(key: string, data: StorageData, options?: PutOptions): Promise<void>;
|
|
10
15
|
get(key: string): Promise<StorageObject | null>;
|
|
11
16
|
delete(key: string): Promise<void>;
|
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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
|
|
5
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";
|
|
@@ -58,36 +59,98 @@ export class FileStorage {
|
|
|
58
59
|
}
|
|
59
60
|
return full;
|
|
60
61
|
}
|
|
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);
|
|
98
|
+
await mkdir(dirname(path), { recursive: true });
|
|
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
|
+
}
|
|
61
115
|
async put(key, data, options = {}) {
|
|
62
116
|
const path = this.pathFor(key);
|
|
63
|
-
await
|
|
64
|
-
await writeFile(path, toBytes(data));
|
|
117
|
+
await this.writeContained(this.root, path, key, toBytes(data));
|
|
65
118
|
const metadataPath = this.metadataPathFor(key);
|
|
66
119
|
if (options.contentType !== undefined || options.metadata !== undefined) {
|
|
67
|
-
await
|
|
68
|
-
await writeFile(metadataPath, JSON.stringify({
|
|
120
|
+
await this.writeContained(this.metadataRoot, metadataPath, key, JSON.stringify({
|
|
69
121
|
...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
|
|
70
122
|
...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
|
|
71
123
|
}));
|
|
72
124
|
}
|
|
73
125
|
else {
|
|
126
|
+
await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
|
|
74
127
|
await rm(metadataPath, { force: true });
|
|
75
128
|
}
|
|
76
129
|
}
|
|
77
130
|
async get(key) {
|
|
78
131
|
const path = this.pathFor(key);
|
|
132
|
+
await this.assertNoSymlinkPath(this.root, path, key);
|
|
79
133
|
let body;
|
|
80
134
|
try {
|
|
81
135
|
body = new Uint8Array(await readFile(path));
|
|
82
136
|
}
|
|
83
|
-
catch {
|
|
84
|
-
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code === "ENOENT")
|
|
139
|
+
return null;
|
|
140
|
+
throw error;
|
|
85
141
|
}
|
|
86
142
|
let stored = {};
|
|
87
143
|
try {
|
|
88
|
-
|
|
144
|
+
const metadataPath = this.metadataPathFor(key);
|
|
145
|
+
await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
|
|
146
|
+
stored = JSON.parse(await readFile(metadataPath, "utf8"));
|
|
89
147
|
}
|
|
90
|
-
catch {
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof StorageKeyError)
|
|
150
|
+
throw error;
|
|
151
|
+
if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
91
154
|
// Objects created before sidecar support still infer a useful MIME type from their extension.
|
|
92
155
|
}
|
|
93
156
|
const contentType = stored.contentType ?? inferContentType(key);
|
|
@@ -99,19 +162,25 @@ export class FileStorage {
|
|
|
99
162
|
};
|
|
100
163
|
}
|
|
101
164
|
async delete(key) {
|
|
165
|
+
const path = this.pathFor(key);
|
|
166
|
+
const metadataPath = this.metadataPathFor(key);
|
|
102
167
|
await Promise.all([
|
|
103
|
-
|
|
104
|
-
|
|
168
|
+
this.assertNoSymlinkPath(this.root, path, key),
|
|
169
|
+
this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
|
|
105
170
|
]);
|
|
171
|
+
await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })]);
|
|
106
172
|
}
|
|
107
173
|
async exists(key) {
|
|
108
174
|
const path = this.pathFor(key);
|
|
175
|
+
await this.assertNoSymlinkPath(this.root, path, key);
|
|
109
176
|
try {
|
|
110
177
|
await stat(path);
|
|
111
178
|
return true;
|
|
112
179
|
}
|
|
113
|
-
catch {
|
|
114
|
-
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code === "ENOENT")
|
|
182
|
+
return false;
|
|
183
|
+
throw error;
|
|
115
184
|
}
|
|
116
185
|
}
|
|
117
186
|
async list(options = {}) {
|
|
@@ -136,7 +205,9 @@ export class FileStorage {
|
|
|
136
205
|
const full = join(dir, name);
|
|
137
206
|
// POSIX-join the key segments so listed keys are portable regardless of the host separator.
|
|
138
207
|
const key = prefix === "" ? name : posix.join(prefix, name);
|
|
139
|
-
const info = await
|
|
208
|
+
const info = await lstat(full);
|
|
209
|
+
if (info.isSymbolicLink())
|
|
210
|
+
continue;
|
|
140
211
|
if (info.isDirectory())
|
|
141
212
|
await this.walk(full, key, out);
|
|
142
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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/storage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
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",
|
package/src/file.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
|
|
5
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 {
|
|
@@ -74,37 +75,102 @@ export class FileStorage implements StorageAdapter {
|
|
|
74
75
|
return full
|
|
75
76
|
}
|
|
76
77
|
|
|
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)
|
|
115
|
+
await mkdir(dirname(path), { recursive: true })
|
|
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
|
+
|
|
77
134
|
async put(key: string, data: StorageData, options: PutOptions = {}): Promise<void> {
|
|
78
135
|
const path = this.pathFor(key)
|
|
79
|
-
await
|
|
80
|
-
await writeFile(path, toBytes(data))
|
|
136
|
+
await this.writeContained(this.root, path, key, toBytes(data))
|
|
81
137
|
const metadataPath = this.metadataPathFor(key)
|
|
82
138
|
if (options.contentType !== undefined || options.metadata !== undefined) {
|
|
83
|
-
await
|
|
84
|
-
|
|
139
|
+
await this.writeContained(
|
|
140
|
+
this.metadataRoot,
|
|
85
141
|
metadataPath,
|
|
142
|
+
key,
|
|
86
143
|
JSON.stringify({
|
|
87
144
|
...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
|
|
88
145
|
...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
|
|
89
146
|
}),
|
|
90
147
|
)
|
|
91
148
|
} else {
|
|
149
|
+
await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key)
|
|
92
150
|
await rm(metadataPath, { force: true })
|
|
93
151
|
}
|
|
94
152
|
}
|
|
95
153
|
|
|
96
154
|
async get(key: string): Promise<StorageObject | null> {
|
|
97
155
|
const path = this.pathFor(key)
|
|
156
|
+
await this.assertNoSymlinkPath(this.root, path, key)
|
|
98
157
|
let body: Uint8Array
|
|
99
158
|
try {
|
|
100
159
|
body = new Uint8Array(await readFile(path))
|
|
101
|
-
} catch {
|
|
102
|
-
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null
|
|
162
|
+
throw error
|
|
103
163
|
}
|
|
104
164
|
let stored: { contentType?: string; metadata?: Readonly<Record<string, string>> } = {}
|
|
105
165
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
174
|
// Objects created before sidecar support still infer a useful MIME type from their extension.
|
|
109
175
|
}
|
|
110
176
|
const contentType = stored.contentType ?? inferContentType(key)
|
|
@@ -117,19 +183,24 @@ export class FileStorage implements StorageAdapter {
|
|
|
117
183
|
}
|
|
118
184
|
|
|
119
185
|
async delete(key: string): Promise<void> {
|
|
186
|
+
const path = this.pathFor(key)
|
|
187
|
+
const metadataPath = this.metadataPathFor(key)
|
|
120
188
|
await Promise.all([
|
|
121
|
-
|
|
122
|
-
|
|
189
|
+
this.assertNoSymlinkPath(this.root, path, key),
|
|
190
|
+
this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
|
|
123
191
|
])
|
|
192
|
+
await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })])
|
|
124
193
|
}
|
|
125
194
|
|
|
126
195
|
async exists(key: string): Promise<boolean> {
|
|
127
196
|
const path = this.pathFor(key)
|
|
197
|
+
await this.assertNoSymlinkPath(this.root, path, key)
|
|
128
198
|
try {
|
|
129
199
|
await stat(path)
|
|
130
200
|
return true
|
|
131
|
-
} catch {
|
|
132
|
-
return false
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false
|
|
203
|
+
throw error
|
|
133
204
|
}
|
|
134
205
|
}
|
|
135
206
|
|
|
@@ -154,7 +225,8 @@ export class FileStorage implements StorageAdapter {
|
|
|
154
225
|
const full = join(dir, name)
|
|
155
226
|
// POSIX-join the key segments so listed keys are portable regardless of the host separator.
|
|
156
227
|
const key = prefix === "" ? name : posix.join(prefix, name)
|
|
157
|
-
const info = await
|
|
228
|
+
const info = await lstat(full)
|
|
229
|
+
if (info.isSymbolicLink()) continue
|
|
158
230
|
if (info.isDirectory()) await this.walk(full, key, out)
|
|
159
231
|
else if (info.isFile()) out.push(key)
|
|
160
232
|
}
|