@nestm/storage 0.1.0-alpha.0 → 0.1.0-alpha.10
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/CHANGELOG.md +264 -0
- package/README.md +724 -25
- package/SECURITY.md +101 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +91 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.js +491 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
- package/dist/ai-sdk/index.d.ts +2 -0
- package/dist/ai-sdk/index.d.ts.map +1 -0
- package/dist/ai-sdk/index.js +2 -0
- package/dist/ai-sdk/index.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +85 -3
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +1437 -37
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/fs/index.d.ts +42 -0
- package/dist/files-sdk/fs/index.d.ts.map +1 -0
- package/dist/files-sdk/fs/index.js +1117 -0
- package/dist/files-sdk/fs/index.js.map +1 -0
- package/dist/files-sdk/index.d.ts +1 -1
- package/dist/files-sdk/index.d.ts.map +1 -1
- package/dist/files-sdk/index.js.map +1 -1
- package/dist/files-sdk/provider/index.d.ts +58 -0
- package/dist/files-sdk/provider/index.d.ts.map +1 -0
- package/dist/files-sdk/provider/index.js +147 -0
- package/dist/files-sdk/provider/index.js.map +1 -0
- package/dist/files-sdk/s3/construction-metadata.d.ts +9 -0
- package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
- package/dist/files-sdk/s3/construction-metadata.js +24 -0
- package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
- package/dist/files-sdk/s3/index.d.ts +54 -0
- package/dist/files-sdk/s3/index.d.ts.map +1 -0
- package/dist/files-sdk/s3/index.js +1505 -0
- package/dist/files-sdk/s3/index.js.map +1 -0
- package/dist/gateway/index.d.ts +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/storage-gateway-fastify-parser.d.ts.map +1 -1
- package/dist/gateway/storage-gateway-fastify-parser.js.map +1 -1
- package/dist/gateway/storage-gateway.controller.d.ts +12 -11
- package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.controller.js +240 -70
- package/dist/gateway/storage-gateway.controller.js.map +1 -1
- package/dist/gateway/storage-gateway.guard.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.guard.js.map +1 -1
- package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.module.js +60 -1
- package/dist/gateway/storage-gateway.module.js.map +1 -1
- package/dist/gateway/storage-gateway.tokens.d.ts +1 -0
- package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.tokens.js +1 -0
- package/dist/gateway/storage-gateway.tokens.js.map +1 -1
- package/dist/gateway/storage-gateway.types.d.ts +53 -10
- package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.types.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/inject-storage.decorator.js.map +1 -1
- package/dist/storage-etag.d.ts +13 -0
- package/dist/storage-etag.d.ts.map +1 -0
- package/dist/storage-etag.js +31 -0
- package/dist/storage-etag.js.map +1 -0
- package/dist/storage-upload-control.d.ts.map +1 -1
- package/dist/storage.client.d.ts +15 -3
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +204 -2
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +28 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.error.d.ts +17 -10
- package/dist/storage.error.d.ts.map +1 -1
- package/dist/storage.error.js +47 -1
- package/dist/storage.error.js.map +1 -1
- package/dist/storage.module.d.ts.map +1 -1
- package/dist/storage.module.js.map +1 -1
- package/dist/storage.service.d.ts.map +1 -1
- package/dist/storage.service.js +28 -3
- package/dist/storage.service.js.map +1 -1
- package/dist/storage.tokens.js.map +1 -1
- package/dist/storage.types.d.ts +150 -1
- package/dist/storage.types.d.ts.map +1 -1
- package/dist/storage.types.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/provider-conformance.d.ts +65 -0
- package/dist/testing/provider-conformance.d.ts.map +1 -0
- package/dist/testing/provider-conformance.js +869 -0
- package/dist/testing/provider-conformance.js.map +1 -0
- package/dist/workspace/index.d.ts +5 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +5 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/storage-workspace.cursor.d.ts +71 -0
- package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.cursor.js +408 -0
- package/dist/workspace/storage-workspace.cursor.js.map +1 -0
- package/dist/workspace/storage-workspace.d.ts +6 -0
- package/dist/workspace/storage-workspace.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.error.d.ts +32 -0
- package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.error.js +75 -0
- package/dist/workspace/storage-workspace.error.js.map +1 -0
- package/dist/workspace/storage-workspace.js +849 -0
- package/dist/workspace/storage-workspace.js.map +1 -0
- package/dist/workspace/storage-workspace.path.d.ts +9 -0
- package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.path.js +68 -0
- package/dist/workspace/storage-workspace.path.js.map +1 -0
- package/dist/workspace/storage-workspace.types.d.ts +123 -0
- package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.types.js +22 -0
- package/dist/workspace/storage-workspace.types.js.map +1 -0
- package/package.json +103 -35
|
@@ -0,0 +1,1117 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import * as fsp from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { createStoredFile, } from 'files-sdk';
|
|
7
|
+
import { fs } from 'files-sdk/fs';
|
|
8
|
+
import { StorageError, StorageErrorCode } from '../../storage.error.js';
|
|
9
|
+
import { isCanonicalStorageEtag, normalizeProviderStorageEtag, } from '../../storage-etag.js';
|
|
10
|
+
import { createFilesSdkDriver, mapStorageErrorToFilesSdkError, } from '../files-sdk.driver.js';
|
|
11
|
+
const SIDECAR_SUFFIX = '.meta.json';
|
|
12
|
+
const TEMP_SUFFIX = '.fls-part';
|
|
13
|
+
const MAX_SIDECAR_BYTES = 1024 * 1024;
|
|
14
|
+
function storedFileOfStorageObject(object) {
|
|
15
|
+
return createStoredFile({
|
|
16
|
+
...(object.etag !== undefined && { etag: object.etag }),
|
|
17
|
+
key: object.key,
|
|
18
|
+
...(object.lastModified !== undefined && {
|
|
19
|
+
lastModified: object.lastModified.getTime(),
|
|
20
|
+
}),
|
|
21
|
+
...(object.metadata !== undefined && {
|
|
22
|
+
metadata: { ...object.metadata },
|
|
23
|
+
}),
|
|
24
|
+
size: object.size,
|
|
25
|
+
type: object.contentType,
|
|
26
|
+
}, { factory: () => object.body, kind: 'stream' });
|
|
27
|
+
}
|
|
28
|
+
function conditionalUploadResultOf(result) {
|
|
29
|
+
if (!isCanonicalStorageEtag(result.etag)) {
|
|
30
|
+
throw new StorageError('A conditional filesystem upload committed without a canonical ETag.', {
|
|
31
|
+
applied: true,
|
|
32
|
+
code: StorageErrorCode.PROVIDER,
|
|
33
|
+
operation: 'upload',
|
|
34
|
+
permanent: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
contentType: result.contentType,
|
|
39
|
+
etag: result.etag,
|
|
40
|
+
key: result.key,
|
|
41
|
+
...(result.lastModified !== undefined && {
|
|
42
|
+
lastModified: result.lastModified.getTime(),
|
|
43
|
+
}),
|
|
44
|
+
size: result.size,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function fsErrorCode(error) {
|
|
48
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
49
|
+
return typeof error.code === 'string' ? error.code : undefined;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function storageFsError(error, key, operation) {
|
|
54
|
+
if (error instanceof StorageError) {
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
const systemCode = fsErrorCode(error);
|
|
58
|
+
const code = systemCode === 'ENOENT' || systemCode === 'ENOTDIR'
|
|
59
|
+
? StorageErrorCode.NOT_FOUND
|
|
60
|
+
: systemCode === 'EEXIST'
|
|
61
|
+
? StorageErrorCode.CONFLICT
|
|
62
|
+
: systemCode === 'EACCES' || systemCode === 'EPERM'
|
|
63
|
+
? StorageErrorCode.UNAUTHORIZED
|
|
64
|
+
: StorageErrorCode.PROVIDER;
|
|
65
|
+
return new StorageError(`Filesystem ${operation} failed for "${key}".`, {
|
|
66
|
+
code,
|
|
67
|
+
key,
|
|
68
|
+
operation,
|
|
69
|
+
permanent: code === StorageErrorCode.NOT_FOUND ||
|
|
70
|
+
code === StorageErrorCode.CONFLICT ||
|
|
71
|
+
code === StorageErrorCode.UNAUTHORIZED,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function appliedStorageFsError(error, key, operation, appliedEtag) {
|
|
75
|
+
const mapped = storageFsError(error, key, operation);
|
|
76
|
+
return new StorageError(mapped.message, {
|
|
77
|
+
aborted: mapped.aborted,
|
|
78
|
+
applied: true,
|
|
79
|
+
...(appliedEtag !== undefined && { appliedEtag }),
|
|
80
|
+
code: mapped.code,
|
|
81
|
+
key: mapped.key ?? key,
|
|
82
|
+
operation: mapped.operation ?? operation,
|
|
83
|
+
permanent: true,
|
|
84
|
+
timedOut: mapped.timedOut,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function invalidFsPath(key, message) {
|
|
88
|
+
throw new StorageError(`Invalid filesystem storage key: ${message}.`, {
|
|
89
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
90
|
+
key,
|
|
91
|
+
permanent: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function strictSegments(key) {
|
|
95
|
+
if (key.length === 0 || key.includes('\0')) {
|
|
96
|
+
invalidFsPath(key, 'the key is empty or contains a null byte');
|
|
97
|
+
}
|
|
98
|
+
if (path.isAbsolute(key) || key.includes('\\')) {
|
|
99
|
+
invalidFsPath(key, 'only relative POSIX paths are accepted');
|
|
100
|
+
}
|
|
101
|
+
const segments = key.split('/');
|
|
102
|
+
if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
|
|
103
|
+
invalidFsPath(key, 'empty, dot, and parent segments are not accepted');
|
|
104
|
+
}
|
|
105
|
+
if (segments.some((segment) => /[. ]$/u.test(segment))) {
|
|
106
|
+
invalidFsPath(key, 'segments ending in a dot or space are not accepted');
|
|
107
|
+
}
|
|
108
|
+
const leaf = segments.at(-1)?.toLowerCase() ?? '';
|
|
109
|
+
if (leaf.endsWith(SIDECAR_SUFFIX) || leaf.endsWith(TEMP_SUFFIX)) {
|
|
110
|
+
invalidFsPath(key, 'the final segment uses a reserved adapter suffix');
|
|
111
|
+
}
|
|
112
|
+
return segments;
|
|
113
|
+
}
|
|
114
|
+
function symlinkError(key) {
|
|
115
|
+
return new StorageError(`Filesystem storage key "${key}" crosses a symbolic link.`, {
|
|
116
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
117
|
+
key,
|
|
118
|
+
permanent: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function unsupportedUpload(key, message) {
|
|
122
|
+
throw new StorageError(message, {
|
|
123
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
124
|
+
key,
|
|
125
|
+
operation: 'upload',
|
|
126
|
+
permanent: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function conflict(key) {
|
|
130
|
+
throw new StorageError(`Conditional mutation conflicted for "${key}".`, {
|
|
131
|
+
code: StorageErrorCode.CONFLICT,
|
|
132
|
+
key,
|
|
133
|
+
permanent: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function notFound(key) {
|
|
137
|
+
throw new StorageError(`Storage object "${key}" was not found.`, {
|
|
138
|
+
code: StorageErrorCode.NOT_FOUND,
|
|
139
|
+
key,
|
|
140
|
+
permanent: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function runtimeOf(options) {
|
|
144
|
+
const timeoutSignal = options.timeout === undefined || options.timeout <= 0
|
|
145
|
+
? undefined
|
|
146
|
+
: AbortSignal.timeout(options.timeout);
|
|
147
|
+
return {
|
|
148
|
+
callerSignal: options.signal,
|
|
149
|
+
signal: options.signal === undefined
|
|
150
|
+
? timeoutSignal
|
|
151
|
+
: timeoutSignal === undefined
|
|
152
|
+
? options.signal
|
|
153
|
+
: AbortSignal.any([options.signal, timeoutSignal]),
|
|
154
|
+
timeoutSignal,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function assertActive(runtime, key) {
|
|
158
|
+
if (runtime.signal?.aborted !== true) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const timedOut = runtime.timeoutSignal?.aborted === true &&
|
|
162
|
+
runtime.callerSignal?.aborted !== true;
|
|
163
|
+
throw new StorageError(timedOut
|
|
164
|
+
? `Filesystem mutation timed out for "${key}".`
|
|
165
|
+
: `Filesystem mutation was aborted for "${key}".`, {
|
|
166
|
+
aborted: true,
|
|
167
|
+
code: timedOut ? StorageErrorCode.TIMEOUT : StorageErrorCode.ABORTED,
|
|
168
|
+
key,
|
|
169
|
+
permanent: true,
|
|
170
|
+
timedOut,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async function ensureRoot(root, key) {
|
|
174
|
+
await fsp.mkdir(root, { mode: 0o700, recursive: true });
|
|
175
|
+
const stat = await fsp.lstat(root);
|
|
176
|
+
if (stat.isSymbolicLink()) {
|
|
177
|
+
throw symlinkError(key);
|
|
178
|
+
}
|
|
179
|
+
if (!stat.isDirectory()) {
|
|
180
|
+
invalidFsPath(key, 'the configured root is not a directory');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function ensureParents(root, segments, key, create, runtime) {
|
|
184
|
+
await ensureRoot(root, key);
|
|
185
|
+
let current = root;
|
|
186
|
+
for (const segment of segments.slice(0, -1)) {
|
|
187
|
+
assertActive(runtime, key);
|
|
188
|
+
current = path.join(current, segment);
|
|
189
|
+
let stat;
|
|
190
|
+
try {
|
|
191
|
+
stat = await fsp.lstat(current);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (fsErrorCode(error) !== 'ENOENT') {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
if (!create) {
|
|
198
|
+
notFound(key);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await fsp.mkdir(current, { mode: 0o700 });
|
|
202
|
+
}
|
|
203
|
+
catch (mkdirError) {
|
|
204
|
+
if (fsErrorCode(mkdirError) !== 'EEXIST') {
|
|
205
|
+
throw mkdirError;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
stat = await fsp.lstat(current);
|
|
209
|
+
}
|
|
210
|
+
if (stat.isSymbolicLink()) {
|
|
211
|
+
throw symlinkError(key);
|
|
212
|
+
}
|
|
213
|
+
if (!stat.isDirectory()) {
|
|
214
|
+
invalidFsPath(key, 'a parent segment is not a directory');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return path.join(root, ...segments);
|
|
218
|
+
}
|
|
219
|
+
async function regularFileOrMissing(target, key) {
|
|
220
|
+
try {
|
|
221
|
+
const stat = await fsp.lstat(target);
|
|
222
|
+
if (stat.isSymbolicLink()) {
|
|
223
|
+
throw symlinkError(key);
|
|
224
|
+
}
|
|
225
|
+
if (!stat.isFile()) {
|
|
226
|
+
invalidFsPath(key, 'an object path is not a regular file');
|
|
227
|
+
}
|
|
228
|
+
if (stat.nlink !== 1) {
|
|
229
|
+
invalidFsPath(key, 'hard-linked object files are not accepted');
|
|
230
|
+
}
|
|
231
|
+
return stat;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (fsErrorCode(error) === 'ENOENT') {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function assertSymlinkFreeReadPath(root, key) {
|
|
241
|
+
const segments = strictSegments(key);
|
|
242
|
+
let rootStat;
|
|
243
|
+
try {
|
|
244
|
+
rootStat = await fsp.lstat(root);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (fsErrorCode(error) === 'ENOENT') {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
if (rootStat.isSymbolicLink()) {
|
|
253
|
+
throw symlinkError(key);
|
|
254
|
+
}
|
|
255
|
+
if (!rootStat.isDirectory()) {
|
|
256
|
+
invalidFsPath(key, 'the configured root is not a directory');
|
|
257
|
+
}
|
|
258
|
+
let current = root;
|
|
259
|
+
for (const [index, segment] of segments.entries()) {
|
|
260
|
+
current = path.join(current, segment);
|
|
261
|
+
let stat;
|
|
262
|
+
try {
|
|
263
|
+
stat = await fsp.lstat(current);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (fsErrorCode(error) === 'ENOENT') {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
if (stat.isSymbolicLink()) {
|
|
272
|
+
throw symlinkError(key);
|
|
273
|
+
}
|
|
274
|
+
const leaf = index === segments.length - 1;
|
|
275
|
+
if ((!leaf && !stat.isDirectory()) || (leaf && !stat.isFile())) {
|
|
276
|
+
invalidFsPath(key, leaf
|
|
277
|
+
? 'an object path is not a regular file'
|
|
278
|
+
: 'a parent segment is not a directory');
|
|
279
|
+
}
|
|
280
|
+
if (leaf && stat.nlink !== 1) {
|
|
281
|
+
invalidFsPath(key, 'hard-linked object files are not accepted');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const sidecar = await regularFileOrMissing(current + SIDECAR_SUFFIX, key);
|
|
285
|
+
if (sidecar?.isSymbolicLink()) {
|
|
286
|
+
throw symlinkError(key);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function readSidecar(target, key) {
|
|
290
|
+
const handle = await fsp.open(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
291
|
+
try {
|
|
292
|
+
const stat = await handle.stat();
|
|
293
|
+
if (!stat.isFile() || stat.size > MAX_SIDECAR_BYTES) {
|
|
294
|
+
throw new StorageError(`Filesystem metadata is invalid for "${key}".`, {
|
|
295
|
+
code: StorageErrorCode.PROVIDER,
|
|
296
|
+
key,
|
|
297
|
+
permanent: true,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const parsed = JSON.parse(await handle.readFile('utf8'));
|
|
301
|
+
if (typeof parsed !== 'object' ||
|
|
302
|
+
parsed === null ||
|
|
303
|
+
!('etag' in parsed) ||
|
|
304
|
+
typeof parsed.etag !== 'string' ||
|
|
305
|
+
!('contentType' in parsed) ||
|
|
306
|
+
typeof parsed.contentType !== 'string' ||
|
|
307
|
+
!('lastModified' in parsed) ||
|
|
308
|
+
typeof parsed.lastModified !== 'number') {
|
|
309
|
+
throw new StorageError(`Filesystem metadata is invalid for "${key}".`, {
|
|
310
|
+
code: StorageErrorCode.PROVIDER,
|
|
311
|
+
key,
|
|
312
|
+
permanent: true,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const sidecar = parsed;
|
|
316
|
+
const etag = normalizeProviderStorageEtag(sidecar.etag);
|
|
317
|
+
if (etag === undefined) {
|
|
318
|
+
throw new StorageError(`Filesystem metadata is invalid for "${key}".`, {
|
|
319
|
+
code: StorageErrorCode.PROVIDER,
|
|
320
|
+
key,
|
|
321
|
+
permanent: true,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { ...sidecar, etag };
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
await handle.close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function bodyBytes(body) {
|
|
331
|
+
if (typeof body === 'string') {
|
|
332
|
+
return new TextEncoder().encode(body);
|
|
333
|
+
}
|
|
334
|
+
if (body instanceof Uint8Array) {
|
|
335
|
+
return body;
|
|
336
|
+
}
|
|
337
|
+
if (body instanceof ArrayBuffer) {
|
|
338
|
+
return new Uint8Array(body);
|
|
339
|
+
}
|
|
340
|
+
if (ArrayBuffer.isView(body)) {
|
|
341
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
function defaultContentType(body, override) {
|
|
346
|
+
if (override !== undefined) {
|
|
347
|
+
return override;
|
|
348
|
+
}
|
|
349
|
+
if (typeof body === 'string') {
|
|
350
|
+
return 'text/plain; charset=utf-8';
|
|
351
|
+
}
|
|
352
|
+
if (body instanceof Blob && body.type.length > 0) {
|
|
353
|
+
return body.type;
|
|
354
|
+
}
|
|
355
|
+
return 'application/octet-stream';
|
|
356
|
+
}
|
|
357
|
+
async function writeAll(handle, bytes) {
|
|
358
|
+
let offset = 0;
|
|
359
|
+
while (offset < bytes.byteLength) {
|
|
360
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.byteLength - offset);
|
|
361
|
+
offset += bytesWritten;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function tempPath(directory) {
|
|
365
|
+
return path.join(directory, `.nestm-${randomUUID()}${TEMP_SUFFIX}`);
|
|
366
|
+
}
|
|
367
|
+
async function openTemp(directory) {
|
|
368
|
+
const target = tempPath(directory);
|
|
369
|
+
const handle = await fsp.open(target, fsConstants.O_CREAT |
|
|
370
|
+
fsConstants.O_EXCL |
|
|
371
|
+
fsConstants.O_WRONLY |
|
|
372
|
+
fsConstants.O_NOFOLLOW, 0o600);
|
|
373
|
+
return { handle, path: target };
|
|
374
|
+
}
|
|
375
|
+
async function removeTemp(target) {
|
|
376
|
+
if (target === undefined) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
await fsp.unlink(target);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
if (fsErrorCode(error) !== 'ENOENT') {
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function stageBody(directory, key, body, options, runtime) {
|
|
389
|
+
const staged = await openTemp(directory);
|
|
390
|
+
const hash = createHash('sha256');
|
|
391
|
+
let size = 0;
|
|
392
|
+
const knownBytes = bodyBytes(body);
|
|
393
|
+
const knownSize = knownBytes?.byteLength ?? (body instanceof Blob ? body.size : undefined);
|
|
394
|
+
options.onProgress?.({
|
|
395
|
+
loaded: 0,
|
|
396
|
+
...(knownSize !== undefined && { total: knownSize }),
|
|
397
|
+
});
|
|
398
|
+
try {
|
|
399
|
+
if (knownBytes !== undefined) {
|
|
400
|
+
assertActive(runtime, key);
|
|
401
|
+
await writeAll(staged.handle, knownBytes);
|
|
402
|
+
hash.update(knownBytes);
|
|
403
|
+
size = knownBytes.byteLength;
|
|
404
|
+
options.onProgress?.({ loaded: size, total: size });
|
|
405
|
+
}
|
|
406
|
+
else if (body instanceof Blob) {
|
|
407
|
+
const bytes = new Uint8Array(await body.arrayBuffer());
|
|
408
|
+
assertActive(runtime, key);
|
|
409
|
+
await writeAll(staged.handle, bytes);
|
|
410
|
+
hash.update(bytes);
|
|
411
|
+
size = bytes.byteLength;
|
|
412
|
+
options.onProgress?.({ loaded: size, total: size });
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
const reader = body.getReader();
|
|
416
|
+
try {
|
|
417
|
+
while (true) {
|
|
418
|
+
assertActive(runtime, key);
|
|
419
|
+
const chunk = await reader.read();
|
|
420
|
+
if (chunk.done) {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
await writeAll(staged.handle, chunk.value);
|
|
424
|
+
hash.update(chunk.value);
|
|
425
|
+
size += chunk.value.byteLength;
|
|
426
|
+
options.onProgress?.({ loaded: size });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
await reader.cancel(error).catch(() => undefined);
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
reader.releaseLock();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
await staged.handle.sync();
|
|
438
|
+
await staged.handle.close();
|
|
439
|
+
return {
|
|
440
|
+
etag: hash.digest('hex'),
|
|
441
|
+
path: staged.path,
|
|
442
|
+
size,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
await staged.handle.close().catch(() => undefined);
|
|
447
|
+
await removeTemp(staged.path).catch(() => undefined);
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function stageSidecar(directory, sidecar) {
|
|
452
|
+
const staged = await openTemp(directory);
|
|
453
|
+
try {
|
|
454
|
+
await writeAll(staged.handle, new TextEncoder().encode(JSON.stringify(sidecar)));
|
|
455
|
+
await staged.handle.sync();
|
|
456
|
+
await staged.handle.close();
|
|
457
|
+
return staged.path;
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
await staged.handle.close().catch(() => undefined);
|
|
461
|
+
await removeTemp(staged.path).catch(() => undefined);
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const CONDITIONAL_FS_TAILS = new Map();
|
|
466
|
+
async function serializeFsKey(lockKey, operation, wait) {
|
|
467
|
+
const previous = CONDITIONAL_FS_TAILS.get(lockKey) ?? Promise.resolve();
|
|
468
|
+
let release = () => undefined;
|
|
469
|
+
const gate = new Promise((resolve) => {
|
|
470
|
+
release = resolve;
|
|
471
|
+
});
|
|
472
|
+
const tail = previous.then(() => gate);
|
|
473
|
+
CONDITIONAL_FS_TAILS.set(lockKey, tail);
|
|
474
|
+
let acquired = false;
|
|
475
|
+
try {
|
|
476
|
+
if (wait === undefined || wait.runtime.signal === undefined) {
|
|
477
|
+
await previous;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const signal = wait.runtime.signal;
|
|
481
|
+
await new Promise((resolve, reject) => {
|
|
482
|
+
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
483
|
+
const abort = () => {
|
|
484
|
+
cleanup();
|
|
485
|
+
try {
|
|
486
|
+
assertActive(wait.runtime, wait.key);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
reject(error);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
493
|
+
if (signal.aborted) {
|
|
494
|
+
abort();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
void previous.then(() => {
|
|
498
|
+
cleanup();
|
|
499
|
+
resolve();
|
|
500
|
+
}, (error) => {
|
|
501
|
+
cleanup();
|
|
502
|
+
reject(error);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
acquired = true;
|
|
507
|
+
return await operation();
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
release();
|
|
511
|
+
if (acquired) {
|
|
512
|
+
if (CONDITIONAL_FS_TAILS.get(lockKey) === tail) {
|
|
513
|
+
CONDITIONAL_FS_TAILS.delete(lockKey);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
void tail.then(() => {
|
|
518
|
+
if (CONDITIONAL_FS_TAILS.get(lockKey) === tail) {
|
|
519
|
+
CONDITIONAL_FS_TAILS.delete(lockKey);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Adds symlink-defended reads and process-local exact conditional operations
|
|
527
|
+
* to a files-sdk fs adapter.
|
|
528
|
+
*
|
|
529
|
+
* The configured root must be dedicated to this decorated adapter: no other
|
|
530
|
+
* process, unwrapped adapter, or direct filesystem writer may mutate it
|
|
531
|
+
* concurrently. Ordinary adapter mutations and conditional operations share
|
|
532
|
+
* one process-local lock domain. Each call rejects symlinks in the existing key
|
|
533
|
+
* path and uses exclusive same-directory temporary files plus atomic renames,
|
|
534
|
+
* but Node has no portable `openat2` equivalent that could make those checks
|
|
535
|
+
* safe against a hostile concurrent tree rewrite. This is a CAS implementation
|
|
536
|
+
* for an exclusively-owned local workspace, not a host-filesystem sandbox.
|
|
537
|
+
* Body and metadata sidecars are two files, so a process crash between their
|
|
538
|
+
* renames can still require cleanup.
|
|
539
|
+
*/
|
|
540
|
+
export function withFsConditionalMutation(base) {
|
|
541
|
+
const root = path.resolve(base.root);
|
|
542
|
+
const copy = base.copy.bind(base);
|
|
543
|
+
const deleteObject = base.delete.bind(base);
|
|
544
|
+
const download = base.download.bind(base);
|
|
545
|
+
const exists = base.exists.bind(base);
|
|
546
|
+
const head = base.head.bind(base);
|
|
547
|
+
const list = base.list.bind(base);
|
|
548
|
+
const move = base.move?.bind(base);
|
|
549
|
+
const resumableUpload = base.resumableUpload?.bind(base);
|
|
550
|
+
const upload = base.upload.bind(base);
|
|
551
|
+
const identity = (key) => path.resolve(root, key).normalize('NFC').toLowerCase();
|
|
552
|
+
const serialize = (key, operation, runtime) => serializeFsKey(identity(key), operation, runtime === undefined ? undefined : { key, runtime });
|
|
553
|
+
const serializeMutationPair = (first, second, operation, runtime) => {
|
|
554
|
+
const firstIdentity = identity(first);
|
|
555
|
+
const secondIdentity = identity(second);
|
|
556
|
+
if (firstIdentity === secondIdentity) {
|
|
557
|
+
return serialize(first, operation, runtime);
|
|
558
|
+
}
|
|
559
|
+
const [headKey, tailKey] = firstIdentity < secondIdentity ? [first, second] : [second, first];
|
|
560
|
+
return serialize(headKey, () => serialize(tailKey, operation, runtime), runtime);
|
|
561
|
+
};
|
|
562
|
+
const serializePair = (first, second, operation, runtime) => {
|
|
563
|
+
if (identity(first) === identity(second)) {
|
|
564
|
+
throw new StorageError('Conditional filesystem copy requires distinct source and destination keys.', {
|
|
565
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
566
|
+
key: first,
|
|
567
|
+
operation: 'copy',
|
|
568
|
+
permanent: true,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return serializeMutationPair(first, second, operation, runtime);
|
|
572
|
+
};
|
|
573
|
+
const serializeFilesMutation = async (key, options, operation) => {
|
|
574
|
+
const runtime = runtimeOf(options ?? {});
|
|
575
|
+
try {
|
|
576
|
+
return await serialize(key, operation, runtime);
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
throw mapStorageErrorToFilesSdkError(error);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
const serializeFilesMutationPair = async (first, second, options, operation) => {
|
|
583
|
+
const runtime = runtimeOf(options ?? {});
|
|
584
|
+
try {
|
|
585
|
+
return await serializeMutationPair(first, second, operation, runtime);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
throw mapStorageErrorToFilesSdkError(error);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const assertEtag = (value, label, key, operation) => {
|
|
592
|
+
if (!isCanonicalStorageEtag(value)) {
|
|
593
|
+
throw new StorageError(`${label} must be a canonical storage ETag.`, {
|
|
594
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
595
|
+
key,
|
|
596
|
+
operation,
|
|
597
|
+
permanent: true,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const adapter = Object.assign(base, {
|
|
602
|
+
conditionalCreate: Object.freeze({ resultEtag: true }),
|
|
603
|
+
conditionalReplace: Object.freeze({ resultEtag: true }),
|
|
604
|
+
conditionalDelete: Object.freeze({ etag: true }),
|
|
605
|
+
conditionalRead: Object.freeze({ etag: true, version: false }),
|
|
606
|
+
conditionalCopySource: Object.freeze({ etag: true, version: false }),
|
|
607
|
+
conditionalCopyDestination: Object.freeze({
|
|
608
|
+
atomicWithSource: true,
|
|
609
|
+
create: true,
|
|
610
|
+
replace: true,
|
|
611
|
+
}),
|
|
612
|
+
physicalKey: Object.freeze({ maxBytes: 4096 }),
|
|
613
|
+
async copy(from, to, options) {
|
|
614
|
+
return serializeFilesMutationPair(from, to, options, () => copy(from, to, options));
|
|
615
|
+
},
|
|
616
|
+
async delete(key, options) {
|
|
617
|
+
return serializeFilesMutation(key, options, () => deleteObject(key, options));
|
|
618
|
+
},
|
|
619
|
+
async download(key, options) {
|
|
620
|
+
await assertSymlinkFreeReadPath(root, key);
|
|
621
|
+
return download(key, options);
|
|
622
|
+
},
|
|
623
|
+
async downloadConditional(key, options) {
|
|
624
|
+
if (typeof options !== 'object' ||
|
|
625
|
+
options === null ||
|
|
626
|
+
typeof options.condition !== 'object' ||
|
|
627
|
+
options.condition === null ||
|
|
628
|
+
(options.condition.etag === undefined &&
|
|
629
|
+
options.condition.version === undefined)) {
|
|
630
|
+
throw new StorageError('A conditional filesystem read requires an ETag or version.', {
|
|
631
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
632
|
+
key,
|
|
633
|
+
operation: 'download',
|
|
634
|
+
permanent: true,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (options.condition.etag !== undefined) {
|
|
638
|
+
assertEtag(options.condition.etag, 'condition.etag', key, 'download');
|
|
639
|
+
}
|
|
640
|
+
if (options.condition.version !== undefined) {
|
|
641
|
+
throw new StorageError('The filesystem provider has no immutable object versions.', {
|
|
642
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
643
|
+
key,
|
|
644
|
+
operation: 'download',
|
|
645
|
+
permanent: true,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const runtime = runtimeOf(options);
|
|
649
|
+
return serialize(key, async () => {
|
|
650
|
+
let handle;
|
|
651
|
+
try {
|
|
652
|
+
const segments = strictSegments(key);
|
|
653
|
+
const bodyPath = await ensureParents(root, segments, key, false, runtime);
|
|
654
|
+
const sidecarPath = bodyPath + SIDECAR_SUFFIX;
|
|
655
|
+
if ((await regularFileOrMissing(bodyPath, key)) === undefined ||
|
|
656
|
+
(await regularFileOrMissing(sidecarPath, key)) === undefined) {
|
|
657
|
+
notFound(key);
|
|
658
|
+
}
|
|
659
|
+
const sidecar = await readSidecar(sidecarPath, key);
|
|
660
|
+
if (options.condition.etag !== undefined &&
|
|
661
|
+
sidecar.etag !== options.condition.etag) {
|
|
662
|
+
conflict(key);
|
|
663
|
+
}
|
|
664
|
+
assertActive(runtime, key);
|
|
665
|
+
handle = await fsp.open(bodyPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
666
|
+
const stat = await handle.stat();
|
|
667
|
+
if (!stat.isFile() || stat.nlink !== 1) {
|
|
668
|
+
invalidFsPath(key, 'an object path is not a regular file');
|
|
669
|
+
}
|
|
670
|
+
const start = options.range?.start ?? 0;
|
|
671
|
+
const requestedEnd = options.range?.end ?? Math.max(0, stat.size - 1);
|
|
672
|
+
const end = Math.min(requestedEnd, Math.max(0, stat.size - 1));
|
|
673
|
+
const size = start >= stat.size ? 0 : Math.max(0, end - start + 1);
|
|
674
|
+
if (size === 0) {
|
|
675
|
+
await handle.close();
|
|
676
|
+
handle = undefined;
|
|
677
|
+
return {
|
|
678
|
+
body: new ReadableStream({
|
|
679
|
+
start(controller) {
|
|
680
|
+
controller.close();
|
|
681
|
+
},
|
|
682
|
+
}),
|
|
683
|
+
contentType: sidecar.contentType,
|
|
684
|
+
etag: sidecar.etag,
|
|
685
|
+
key,
|
|
686
|
+
lastModified: new Date(sidecar.lastModified),
|
|
687
|
+
...(sidecar.metadata !== undefined && {
|
|
688
|
+
metadata: { ...sidecar.metadata },
|
|
689
|
+
}),
|
|
690
|
+
name: path.basename(key),
|
|
691
|
+
size: 0,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const nodeStream = handle.createReadStream({
|
|
695
|
+
autoClose: true,
|
|
696
|
+
...(options.range !== undefined && { end, start }),
|
|
697
|
+
});
|
|
698
|
+
handle = undefined;
|
|
699
|
+
return {
|
|
700
|
+
body: Readable.toWeb(nodeStream),
|
|
701
|
+
contentType: sidecar.contentType,
|
|
702
|
+
etag: sidecar.etag,
|
|
703
|
+
key,
|
|
704
|
+
lastModified: new Date(sidecar.lastModified),
|
|
705
|
+
...(sidecar.metadata !== undefined && {
|
|
706
|
+
metadata: { ...sidecar.metadata },
|
|
707
|
+
}),
|
|
708
|
+
name: path.basename(key),
|
|
709
|
+
size,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
await handle?.close().catch(() => undefined);
|
|
714
|
+
throw storageFsError(error, key, 'download');
|
|
715
|
+
}
|
|
716
|
+
}, runtime);
|
|
717
|
+
},
|
|
718
|
+
async exists(key, options) {
|
|
719
|
+
await assertSymlinkFreeReadPath(root, key);
|
|
720
|
+
return exists(key, options);
|
|
721
|
+
},
|
|
722
|
+
async head(key, options) {
|
|
723
|
+
await assertSymlinkFreeReadPath(root, key);
|
|
724
|
+
return head(key, options);
|
|
725
|
+
},
|
|
726
|
+
async list(options) {
|
|
727
|
+
const result = await list(options);
|
|
728
|
+
for (const item of result.items) {
|
|
729
|
+
await assertSymlinkFreeReadPath(root, item.key);
|
|
730
|
+
}
|
|
731
|
+
return result;
|
|
732
|
+
},
|
|
733
|
+
async move(from, to, options) {
|
|
734
|
+
return serializeFilesMutationPair(from, to, options, async () => {
|
|
735
|
+
if (move !== undefined) {
|
|
736
|
+
return move(from, to, options);
|
|
737
|
+
}
|
|
738
|
+
await copy(from, to, options);
|
|
739
|
+
await deleteObject(from, options);
|
|
740
|
+
});
|
|
741
|
+
},
|
|
742
|
+
async deleteConditional(key, options) {
|
|
743
|
+
if (typeof options !== 'object' ||
|
|
744
|
+
options === null ||
|
|
745
|
+
typeof options.condition !== 'object' ||
|
|
746
|
+
options.condition === null) {
|
|
747
|
+
throw new StorageError('A conditional filesystem delete requires an ETag.', {
|
|
748
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
749
|
+
key,
|
|
750
|
+
operation: 'delete',
|
|
751
|
+
permanent: true,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
assertEtag(options.condition.etag, 'condition.etag', key, 'delete');
|
|
755
|
+
const runtime = runtimeOf(options);
|
|
756
|
+
return serialize(key, async () => {
|
|
757
|
+
let applied = false;
|
|
758
|
+
try {
|
|
759
|
+
const segments = strictSegments(key);
|
|
760
|
+
const bodyPath = await ensureParents(root, segments, key, false, runtime);
|
|
761
|
+
const sidecarPath = bodyPath + SIDECAR_SUFFIX;
|
|
762
|
+
if ((await regularFileOrMissing(bodyPath, key)) === undefined ||
|
|
763
|
+
(await regularFileOrMissing(sidecarPath, key)) === undefined) {
|
|
764
|
+
notFound(key);
|
|
765
|
+
}
|
|
766
|
+
const sidecar = await readSidecar(sidecarPath, key);
|
|
767
|
+
if (sidecar.etag !== options.condition.etag) {
|
|
768
|
+
conflict(key);
|
|
769
|
+
}
|
|
770
|
+
assertActive(runtime, key);
|
|
771
|
+
await ensureParents(root, segments, key, false, runtime);
|
|
772
|
+
if ((await regularFileOrMissing(bodyPath, key)) === undefined ||
|
|
773
|
+
(await regularFileOrMissing(sidecarPath, key)) === undefined ||
|
|
774
|
+
(await readSidecar(sidecarPath, key)).etag !==
|
|
775
|
+
options.condition.etag) {
|
|
776
|
+
conflict(key);
|
|
777
|
+
}
|
|
778
|
+
await fsp.unlink(bodyPath);
|
|
779
|
+
applied = true;
|
|
780
|
+
await fsp.unlink(sidecarPath);
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
throw applied
|
|
784
|
+
? appliedStorageFsError(error, key, 'delete')
|
|
785
|
+
: storageFsError(error, key, 'delete');
|
|
786
|
+
}
|
|
787
|
+
}, runtime);
|
|
788
|
+
},
|
|
789
|
+
async promote(sourceKey, destinationKey, options) {
|
|
790
|
+
if (typeof options !== 'object' || options === null) {
|
|
791
|
+
throw new StorageError('A conditional filesystem copy requires a predicate.', {
|
|
792
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
793
|
+
key: sourceKey,
|
|
794
|
+
operation: 'copy',
|
|
795
|
+
permanent: true,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
const destination = options.destination;
|
|
799
|
+
if (destination !== undefined &&
|
|
800
|
+
(typeof destination !== 'object' ||
|
|
801
|
+
destination === null ||
|
|
802
|
+
(destination.type !== 'create' && destination.type !== 'replace'))) {
|
|
803
|
+
throw new StorageError('destination.type must be "create" or "replace".', {
|
|
804
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
805
|
+
key: destinationKey,
|
|
806
|
+
operation: 'copy',
|
|
807
|
+
permanent: true,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
if (options.sourceEtag === undefined &&
|
|
811
|
+
options.sourceVersion === undefined &&
|
|
812
|
+
destination === undefined) {
|
|
813
|
+
throw new StorageError('A conditional filesystem copy requires a source or destination predicate.', {
|
|
814
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
815
|
+
key: sourceKey,
|
|
816
|
+
operation: 'copy',
|
|
817
|
+
permanent: true,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
if (options.sourceEtag !== undefined) {
|
|
821
|
+
assertEtag(options.sourceEtag, 'sourceEtag', sourceKey, 'copy');
|
|
822
|
+
}
|
|
823
|
+
if (destination?.type === 'replace') {
|
|
824
|
+
assertEtag(destination.etag, 'destination.etag', destinationKey, 'copy');
|
|
825
|
+
}
|
|
826
|
+
if (options.sourceVersion !== undefined) {
|
|
827
|
+
throw new StorageError('The filesystem provider has no immutable object versions.', {
|
|
828
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
829
|
+
key: sourceKey,
|
|
830
|
+
operation: 'copy',
|
|
831
|
+
permanent: true,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const runtime = runtimeOf(options);
|
|
835
|
+
return serializePair(sourceKey, destinationKey, async () => {
|
|
836
|
+
let applied = false;
|
|
837
|
+
let bodyTemp;
|
|
838
|
+
let sidecarTemp;
|
|
839
|
+
try {
|
|
840
|
+
const sourceSegments = strictSegments(sourceKey);
|
|
841
|
+
const destinationSegments = strictSegments(destinationKey);
|
|
842
|
+
const sourcePath = await ensureParents(root, sourceSegments, sourceKey, false, runtime);
|
|
843
|
+
const sourceSidecarPath = sourcePath + SIDECAR_SUFFIX;
|
|
844
|
+
if ((await regularFileOrMissing(sourcePath, sourceKey)) ===
|
|
845
|
+
undefined ||
|
|
846
|
+
(await regularFileOrMissing(sourceSidecarPath, sourceKey)) ===
|
|
847
|
+
undefined) {
|
|
848
|
+
notFound(sourceKey);
|
|
849
|
+
}
|
|
850
|
+
const sourceSidecar = await readSidecar(sourceSidecarPath, sourceKey);
|
|
851
|
+
if (options.sourceEtag !== undefined &&
|
|
852
|
+
sourceSidecar.etag !== options.sourceEtag) {
|
|
853
|
+
conflict(sourceKey);
|
|
854
|
+
}
|
|
855
|
+
const destinationPath = await ensureParents(root, destinationSegments, destinationKey, true, runtime);
|
|
856
|
+
const destinationSidecarPath = destinationPath + SIDECAR_SUFFIX;
|
|
857
|
+
const assertDestination = async () => {
|
|
858
|
+
const body = await regularFileOrMissing(destinationPath, destinationKey);
|
|
859
|
+
const sidecar = await regularFileOrMissing(destinationSidecarPath, destinationKey);
|
|
860
|
+
if (options.destination?.type === 'create') {
|
|
861
|
+
if (body !== undefined || sidecar !== undefined) {
|
|
862
|
+
conflict(destinationKey);
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (options.destination?.type === 'replace') {
|
|
867
|
+
if (body === undefined || sidecar === undefined) {
|
|
868
|
+
notFound(destinationKey);
|
|
869
|
+
}
|
|
870
|
+
if ((await readSidecar(destinationSidecarPath, destinationKey))
|
|
871
|
+
.etag !== options.destination.etag) {
|
|
872
|
+
conflict(destinationKey);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
await assertDestination();
|
|
877
|
+
const staged = await openTemp(path.dirname(destinationPath));
|
|
878
|
+
await staged.handle.close();
|
|
879
|
+
bodyTemp = staged.path;
|
|
880
|
+
await fsp.copyFile(sourcePath, bodyTemp);
|
|
881
|
+
const lastModified = Date.now();
|
|
882
|
+
sidecarTemp = await stageSidecar(path.dirname(destinationPath), {
|
|
883
|
+
...sourceSidecar,
|
|
884
|
+
lastModified,
|
|
885
|
+
});
|
|
886
|
+
assertActive(runtime, sourceKey);
|
|
887
|
+
if ((await regularFileOrMissing(sourcePath, sourceKey)) ===
|
|
888
|
+
undefined ||
|
|
889
|
+
(await regularFileOrMissing(sourceSidecarPath, sourceKey)) ===
|
|
890
|
+
undefined ||
|
|
891
|
+
(options.sourceEtag !== undefined &&
|
|
892
|
+
(await readSidecar(sourceSidecarPath, sourceKey)).etag !==
|
|
893
|
+
options.sourceEtag)) {
|
|
894
|
+
conflict(sourceKey);
|
|
895
|
+
}
|
|
896
|
+
await assertDestination();
|
|
897
|
+
await fsp.rename(bodyTemp, destinationPath);
|
|
898
|
+
applied = true;
|
|
899
|
+
bodyTemp = undefined;
|
|
900
|
+
await fsp.rename(sidecarTemp, destinationSidecarPath);
|
|
901
|
+
sidecarTemp = undefined;
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
throw applied
|
|
905
|
+
? appliedStorageFsError(error, destinationKey, 'copy')
|
|
906
|
+
: storageFsError(error, sourceKey, 'copy');
|
|
907
|
+
}
|
|
908
|
+
finally {
|
|
909
|
+
await removeTemp(bodyTemp).catch(() => undefined);
|
|
910
|
+
await removeTemp(sidecarTemp).catch(() => undefined);
|
|
911
|
+
}
|
|
912
|
+
}, runtime);
|
|
913
|
+
},
|
|
914
|
+
async uploadConditional(key, body, options) {
|
|
915
|
+
if (typeof options !== 'object' ||
|
|
916
|
+
options === null ||
|
|
917
|
+
typeof options.condition !== 'object' ||
|
|
918
|
+
options.condition === null ||
|
|
919
|
+
(options.condition.type !== 'create' &&
|
|
920
|
+
options.condition.type !== 'replace')) {
|
|
921
|
+
throw new StorageError('A conditional filesystem upload requires a create or replace predicate.', {
|
|
922
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
923
|
+
key,
|
|
924
|
+
operation: 'upload',
|
|
925
|
+
permanent: true,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
if (options.condition.type === 'replace') {
|
|
929
|
+
assertEtag(options.condition.etag, 'condition.etag', key, 'upload');
|
|
930
|
+
}
|
|
931
|
+
const runtime = runtimeOf(options);
|
|
932
|
+
return serialize(key, async () => {
|
|
933
|
+
let applied = false;
|
|
934
|
+
let appliedEtag;
|
|
935
|
+
let bodyTemp;
|
|
936
|
+
let sidecarTemp;
|
|
937
|
+
try {
|
|
938
|
+
if (options.multipart !== undefined &&
|
|
939
|
+
options.multipart !== false) {
|
|
940
|
+
unsupportedUpload(key, 'Conditional filesystem uploads do not support multipart mode.');
|
|
941
|
+
}
|
|
942
|
+
if (options.control !== undefined) {
|
|
943
|
+
unsupportedUpload(key, 'Conditional filesystem uploads do not support resumable control.');
|
|
944
|
+
}
|
|
945
|
+
const segments = strictSegments(key);
|
|
946
|
+
const bodyPath = await ensureParents(root, segments, key, true, runtime);
|
|
947
|
+
const sidecarPath = bodyPath + SIDECAR_SUFFIX;
|
|
948
|
+
const existingBody = await regularFileOrMissing(bodyPath, key);
|
|
949
|
+
const existingSidecar = await regularFileOrMissing(sidecarPath, key);
|
|
950
|
+
if (options.condition.type === 'create') {
|
|
951
|
+
if (existingBody !== undefined || existingSidecar !== undefined) {
|
|
952
|
+
conflict(key);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
if (existingBody === undefined || existingSidecar === undefined) {
|
|
957
|
+
notFound(key);
|
|
958
|
+
}
|
|
959
|
+
if ((await readSidecar(sidecarPath, key)).etag !==
|
|
960
|
+
options.condition.etag) {
|
|
961
|
+
conflict(key);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const staged = await stageBody(path.dirname(bodyPath), key, body, options, runtime);
|
|
965
|
+
bodyTemp = staged.path;
|
|
966
|
+
const lastModified = Date.now();
|
|
967
|
+
const contentType = defaultContentType(body, options.contentType);
|
|
968
|
+
sidecarTemp = await stageSidecar(path.dirname(bodyPath), {
|
|
969
|
+
contentType,
|
|
970
|
+
etag: staged.etag,
|
|
971
|
+
lastModified,
|
|
972
|
+
...(options.cacheControl !== undefined && {
|
|
973
|
+
cacheControl: options.cacheControl,
|
|
974
|
+
}),
|
|
975
|
+
...(options.metadata !== undefined && {
|
|
976
|
+
metadata: options.metadata,
|
|
977
|
+
}),
|
|
978
|
+
});
|
|
979
|
+
assertActive(runtime, key);
|
|
980
|
+
await ensureParents(root, segments, key, false, runtime);
|
|
981
|
+
const currentBody = await regularFileOrMissing(bodyPath, key);
|
|
982
|
+
const currentSidecar = await regularFileOrMissing(sidecarPath, key);
|
|
983
|
+
if (options.condition.type === 'create') {
|
|
984
|
+
if (currentBody !== undefined || currentSidecar !== undefined) {
|
|
985
|
+
conflict(key);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else if (currentBody === undefined ||
|
|
989
|
+
currentSidecar === undefined ||
|
|
990
|
+
(await readSidecar(sidecarPath, key)).etag !==
|
|
991
|
+
options.condition.etag) {
|
|
992
|
+
conflict(key);
|
|
993
|
+
}
|
|
994
|
+
await fsp.rename(bodyTemp, bodyPath);
|
|
995
|
+
applied = true;
|
|
996
|
+
appliedEtag = staged.etag;
|
|
997
|
+
bodyTemp = undefined;
|
|
998
|
+
await fsp.rename(sidecarTemp, sidecarPath);
|
|
999
|
+
sidecarTemp = undefined;
|
|
1000
|
+
return {
|
|
1001
|
+
contentType,
|
|
1002
|
+
etag: staged.etag,
|
|
1003
|
+
key,
|
|
1004
|
+
lastModified: new Date(lastModified),
|
|
1005
|
+
size: staged.size,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
throw applied
|
|
1010
|
+
? appliedStorageFsError(error, key, 'upload', appliedEtag)
|
|
1011
|
+
: storageFsError(error, key, 'upload');
|
|
1012
|
+
}
|
|
1013
|
+
finally {
|
|
1014
|
+
await removeTemp(bodyTemp).catch(() => undefined);
|
|
1015
|
+
await removeTemp(sidecarTemp).catch(() => undefined);
|
|
1016
|
+
}
|
|
1017
|
+
}, runtime);
|
|
1018
|
+
},
|
|
1019
|
+
async upload(key, body, options) {
|
|
1020
|
+
return serializeFilesMutation(key, options, () => upload(key, body, options));
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
if (resumableUpload !== undefined) {
|
|
1024
|
+
adapter.resumableUpload = (key, options) => {
|
|
1025
|
+
const resumable = resumableUpload(key, options);
|
|
1026
|
+
const complete = resumable.complete.bind(resumable);
|
|
1027
|
+
resumable.complete = (parts) => serialize(key, () => complete(parts));
|
|
1028
|
+
return resumable;
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
const operationOptions = (options) => ({
|
|
1032
|
+
...(options?.signal !== undefined && { signal: options.signal }),
|
|
1033
|
+
...(options?.timeout !== undefined && { timeout: options.timeout }),
|
|
1034
|
+
});
|
|
1035
|
+
const uploadOptions = (options) => ({
|
|
1036
|
+
...operationOptions(options),
|
|
1037
|
+
...(options?.cacheControl !== undefined && {
|
|
1038
|
+
cacheControl: options.cacheControl,
|
|
1039
|
+
}),
|
|
1040
|
+
...(options?.contentType !== undefined && {
|
|
1041
|
+
contentType: options.contentType,
|
|
1042
|
+
}),
|
|
1043
|
+
...(options?.metadata !== undefined && {
|
|
1044
|
+
metadata: { ...options.metadata },
|
|
1045
|
+
}),
|
|
1046
|
+
...(options?.onProgress !== undefined && {
|
|
1047
|
+
onProgress: options.onProgress,
|
|
1048
|
+
}),
|
|
1049
|
+
});
|
|
1050
|
+
const downloadOptions = (options) => ({
|
|
1051
|
+
...operationOptions(options),
|
|
1052
|
+
...(options?.range !== undefined && { range: { ...options.range } }),
|
|
1053
|
+
});
|
|
1054
|
+
const runConditional = async (operation) => {
|
|
1055
|
+
try {
|
|
1056
|
+
return await operation();
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
throw mapStorageErrorToFilesSdkError(error);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
const conditional = Object.freeze({
|
|
1063
|
+
copy: Object.freeze({
|
|
1064
|
+
atomicSourceDestination: true,
|
|
1065
|
+
destinationCreate: true,
|
|
1066
|
+
destinationReplace: true,
|
|
1067
|
+
run: (sourceKey, destinationKey, condition, options) => runConditional(() => adapter.promote(sourceKey, destinationKey, {
|
|
1068
|
+
...operationOptions(options),
|
|
1069
|
+
destination: condition.destination,
|
|
1070
|
+
sourceEtag: condition.source.etag,
|
|
1071
|
+
})),
|
|
1072
|
+
sourceEtag: true,
|
|
1073
|
+
}),
|
|
1074
|
+
create: (key, body, options) => runConditional(async () => conditionalUploadResultOf(await adapter.uploadConditional(key, body, {
|
|
1075
|
+
...uploadOptions(options),
|
|
1076
|
+
condition: { type: 'create' },
|
|
1077
|
+
}))),
|
|
1078
|
+
delete: (key, etag, options) => runConditional(() => adapter.deleteConditional(key, {
|
|
1079
|
+
...operationOptions(options),
|
|
1080
|
+
condition: { etag },
|
|
1081
|
+
})),
|
|
1082
|
+
exactRead: (key, etag, options) => runConditional(async () => storedFileOfStorageObject(await adapter.downloadConditional(key, {
|
|
1083
|
+
...downloadOptions(options),
|
|
1084
|
+
condition: { etag },
|
|
1085
|
+
}))),
|
|
1086
|
+
replace: (key, body, etag, options) => runConditional(async () => conditionalUploadResultOf(await adapter.uploadConditional(key, body, {
|
|
1087
|
+
...uploadOptions(options),
|
|
1088
|
+
condition: { etag, type: 'replace' },
|
|
1089
|
+
}))),
|
|
1090
|
+
});
|
|
1091
|
+
return Object.assign(adapter, { conditional });
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Creates the files-sdk filesystem driver from the storage package's own
|
|
1095
|
+
* dependency context. The adapter reaches only `node:fs`, so this entry point
|
|
1096
|
+
* adds no native SDK to the install — unlike every object-store provider, it is
|
|
1097
|
+
* always available.
|
|
1098
|
+
*
|
|
1099
|
+
* The adapter keeps a `<key>.meta.json` sidecar beside each body to carry the
|
|
1100
|
+
* content type, ETag, and custom metadata that a filesystem has nowhere else to
|
|
1101
|
+
* put. Sidecars never surface as keys: `list` and `search` skip them, and
|
|
1102
|
+
* uploading a key that ends in `.meta.json` fails closed rather than colliding
|
|
1103
|
+
* with one.
|
|
1104
|
+
*
|
|
1105
|
+
* Conditional mutations additionally require an exclusively-owned root. See
|
|
1106
|
+
* {@link withFsConditionalMutation}; use an OS sandbox as well when running an
|
|
1107
|
+
* agent that has direct shell or filesystem tools.
|
|
1108
|
+
*/
|
|
1109
|
+
export function createFsStorageDriver(options) {
|
|
1110
|
+
const { adapter: adapterOptions, ...filesOptions } = options;
|
|
1111
|
+
return createFilesSdkDriver({
|
|
1112
|
+
...filesOptions,
|
|
1113
|
+
adapter: withFsConditionalMutation(fs(adapterOptions)),
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
export { fs, mapFsError } from 'files-sdk/fs';
|
|
1117
|
+
//# sourceMappingURL=index.js.map
|