@poe-platform/safe-fs 0.1.726 → 0.1.728
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 +8 -1
- package/dist/safe-fs/contracts/errors.js +8 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +68 -2
- package/dist/safe-fs/contracts/io.js +183 -27
- package/dist/safe-fs/contracts/virtual-path.d.ts +3 -1
- package/dist/safe-fs/contracts/virtual-path.js +20 -1
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/capabilities.d.ts +1 -1
- package/dist/safe-fs/fs/capabilities.js +59 -21
- package/dist/safe-fs/fs/conditional-chmod.d.ts +7 -0
- package/dist/safe-fs/fs/conditional-chmod.js +19 -0
- package/dist/safe-fs/fs/descriptor.js +51 -25
- package/dist/safe-fs/fs/devices/index.d.ts +7 -2
- package/dist/safe-fs/fs/devices/index.js +210 -29
- package/dist/safe-fs/fs/devices/path.d.ts +3 -1
- package/dist/safe-fs/fs/devices/path.js +70 -12
- package/dist/safe-fs/fs/memory/index.d.ts +34 -48
- package/dist/safe-fs/fs/memory/index.js +1150 -235
- package/dist/safe-fs/fs/memory/ledger.d.ts +2 -0
- package/dist/safe-fs/fs/memory/ledger.js +15 -3
- package/dist/safe-fs/fs/memory/missing-target.js +21 -1
- package/dist/safe-fs/fs/mount/index.d.ts +11 -3
- package/dist/safe-fs/fs/mount/index.js +326 -15
- package/dist/safe-fs/fs/object-publication/index.js +79 -3
- package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
- package/dist/safe-fs/fs/overlay/index.js +22 -2
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +2 -1
- package/dist/safe-fs/fs/overlay/memory-publication.js +39 -2
- package/dist/safe-fs/fs/quota/index.js +1 -1
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +13 -0
- package/dist/safe-fs/fs/real/index.d.ts +6 -2
- package/dist/safe-fs/fs/real/index.js +78 -8
- package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
- package/dist/safe-fs/fs/s3/filesystem.js +48 -16
- package/dist/safe-fs/fs/s3/http/transport.js +20 -3
- package/dist/safe-fs/fs/s3/mock.js +19 -2
- package/dist/safe-fs/fs/s3/namespace.js +8 -2
- package/dist/safe-fs/fs/s3/registry.js +2 -0
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +261 -74
- package/dist/safe-fs/fs/staging-ancestry.d.ts +8 -0
- package/dist/safe-fs/fs/staging-ancestry.js +115 -0
- package/dist/safe-fs/fs/staging-cleanup.d.ts +5 -0
- package/dist/safe-fs/fs/staging-cleanup.js +63 -0
- package/dist/safe-fs/platform/browser.d.ts +5 -0
- package/dist/safe-fs/platform/browser.js +17 -0
- package/dist/safe-fs/platform/node.d.ts +5 -0
- package/dist/safe-fs/platform/node.js +28 -0
- package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
- package/dist/safe-fs/platform/transport-budget.js +4 -0
- package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
- package/dist/safe-fs/testing/object-io-metrics.js +29 -0
- package/dist/safe-fs/testing/object-publication.d.ts +3 -1
- package/dist/safe-fs/testing/object-publication.js +2 -2
- package/package.json +1 -1
|
@@ -45,6 +45,37 @@ function integer(value, syscall, path) {
|
|
|
45
45
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
46
46
|
throw new FsError("EINVAL", { syscall, path });
|
|
47
47
|
}
|
|
48
|
+
const OPEN_FILE_OPTION_KEYS = new Set(["access", "creation", "truncate", "append", "mode", "exactMode", "noFollow", "synchronization", "signal"]);
|
|
49
|
+
const managedCapabilitiesCache = new WeakMap();
|
|
50
|
+
function getManagedCapabilities(capabilities, access, append) {
|
|
51
|
+
let byKey = managedCapabilitiesCache.get(capabilities);
|
|
52
|
+
const key = `${access}:${append ? 1 : 0}`;
|
|
53
|
+
if (byKey) {
|
|
54
|
+
const cached = byKey.get(key);
|
|
55
|
+
if (cached)
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
byKey = new Map();
|
|
60
|
+
managedCapabilitiesCache.set(capabilities, byKey);
|
|
61
|
+
}
|
|
62
|
+
const positionedAppendWrite = capabilities.positionedAppendWrite === true && capabilities.positionedWrite && access !== "read";
|
|
63
|
+
const frozen = Object.freeze({
|
|
64
|
+
...(capabilities.noFollow === undefined ? {} : { noFollow: capabilities.noFollow }),
|
|
65
|
+
...(capabilities.publication === undefined ? {} : { publication: capabilities.publication }),
|
|
66
|
+
...(capabilities.position === undefined ? {} : { position: capabilities.position }),
|
|
67
|
+
...(capabilities.readObservation === undefined ? {} : { readObservation: capabilities.readObservation }),
|
|
68
|
+
...(capabilities.openTruncate === undefined ? {} : { openTruncate: capabilities.openTruncate }),
|
|
69
|
+
positionedRead: capabilities.positionedRead && access !== "write",
|
|
70
|
+
positionedWrite: capabilities.positionedWrite && access !== "read" && (!append || positionedAppendWrite),
|
|
71
|
+
...(capabilities.positionedAppendWrite === undefined ? {} : { positionedAppendWrite }),
|
|
72
|
+
...(capabilities.delegateZeroLengthWrite === undefined ? {} : { delegateZeroLengthWrite: capabilities.delegateZeroLengthWrite && access !== "read" }),
|
|
73
|
+
truncate: capabilities.truncate && access !== "read",
|
|
74
|
+
synchronization: capabilities.synchronization,
|
|
75
|
+
});
|
|
76
|
+
byKey.set(key, frozen);
|
|
77
|
+
return frozen;
|
|
78
|
+
}
|
|
48
79
|
class ManagedFileDescriptor {
|
|
49
80
|
capabilities;
|
|
50
81
|
#path;
|
|
@@ -62,26 +93,13 @@ class ManagedFileDescriptor {
|
|
|
62
93
|
const getPosition = capabilities.position === true ? backend.getPosition : undefined;
|
|
63
94
|
if (capabilities.position === true && typeof getPosition !== "function")
|
|
64
95
|
throw new FsError("ENOTSUP", { syscall: "getPosition", path });
|
|
65
|
-
this.#getPosition = getPosition
|
|
96
|
+
this.#getPosition = getPosition;
|
|
66
97
|
const probeRead = capabilities.readObservation === true ? backend.probeRead : undefined;
|
|
67
98
|
if (capabilities.readObservation === true && typeof probeRead !== "function")
|
|
68
99
|
throw new FsError("ENOTSUP", { syscall: "probeRead", path });
|
|
69
|
-
this.#probeRead = probeRead
|
|
100
|
+
this.#probeRead = probeRead;
|
|
70
101
|
this.#backend = backend;
|
|
71
|
-
|
|
72
|
-
this.capabilities = Object.freeze({
|
|
73
|
-
...(capabilities.noFollow === undefined ? {} : { noFollow: capabilities.noFollow }),
|
|
74
|
-
...(capabilities.publication === undefined ? {} : { publication: capabilities.publication }),
|
|
75
|
-
...(capabilities.position === undefined ? {} : { position: capabilities.position }),
|
|
76
|
-
...(capabilities.readObservation === undefined ? {} : { readObservation: capabilities.readObservation }),
|
|
77
|
-
...(capabilities.openTruncate === undefined ? {} : { openTruncate: capabilities.openTruncate }),
|
|
78
|
-
positionedRead: capabilities.positionedRead && options.access !== "write",
|
|
79
|
-
positionedWrite: capabilities.positionedWrite && options.access !== "read" && (!options.append || positionedAppendWrite),
|
|
80
|
-
...(capabilities.positionedAppendWrite === undefined ? {} : { positionedAppendWrite }),
|
|
81
|
-
...(capabilities.delegateZeroLengthWrite === undefined ? {} : { delegateZeroLengthWrite: capabilities.delegateZeroLengthWrite && options.access !== "read" }),
|
|
82
|
-
truncate: capabilities.truncate && options.access !== "read",
|
|
83
|
-
synchronization: capabilities.synchronization,
|
|
84
|
-
});
|
|
102
|
+
this.capabilities = getManagedCapabilities(capabilities, options.access, options.append);
|
|
85
103
|
}
|
|
86
104
|
#run(syscall, options, action) {
|
|
87
105
|
const signal = options.signal;
|
|
@@ -128,7 +146,7 @@ class ManagedFileDescriptor {
|
|
|
128
146
|
return this.#run("getPosition", options, async (backend, forwarded) => {
|
|
129
147
|
if (!this.capabilities.position || !this.#getPosition)
|
|
130
148
|
throw new FsError("ENOTSUP", { syscall: "getPosition", path: this.#path });
|
|
131
|
-
const position = await this.#getPosition(backend.resource, forwarded);
|
|
149
|
+
const position = await this.#getPosition.call(backend, backend.resource, forwarded);
|
|
132
150
|
if (!Number.isSafeInteger(position) || position < 0)
|
|
133
151
|
throw new FsError("EIO", { syscall: "getPosition", path: this.#path });
|
|
134
152
|
return position;
|
|
@@ -138,7 +156,7 @@ class ManagedFileDescriptor {
|
|
|
138
156
|
return this.#run("probeRead", options, async (backend, forwarded) => {
|
|
139
157
|
if (!this.capabilities.readObservation || !this.#probeRead)
|
|
140
158
|
throw new FsError("ENOTSUP", { syscall: "probeRead", path: this.#path });
|
|
141
|
-
const readiness = await this.#probeRead(backend.resource, forwarded);
|
|
159
|
+
const readiness = await this.#probeRead.call(backend, backend.resource, forwarded);
|
|
142
160
|
if (readiness !== "ready" && readiness !== "blocked" && readiness !== "unknown")
|
|
143
161
|
throw new FsError("EIO", { syscall: "probeRead", path: this.#path });
|
|
144
162
|
return readiness;
|
|
@@ -209,11 +227,17 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
|
|
|
209
227
|
throw new FsError("EINVAL", { syscall: "open", path });
|
|
210
228
|
const signal = options.signal;
|
|
211
229
|
signal?.throwIfAborted();
|
|
212
|
-
const keys = ["access", "creation", "truncate", "append", "mode", "exactMode", "noFollow", "synchronization", "signal"];
|
|
213
230
|
const { access, creation = "never", truncate = false, append = false, mode = 0o666, synchronization } = options;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
let invalidKey = false;
|
|
232
|
+
for (const key in options) {
|
|
233
|
+
if (Object.prototype.hasOwnProperty.call(options, key) && !OPEN_FILE_OPTION_KEYS.has(key)) {
|
|
234
|
+
invalidKey = true;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (invalidKey
|
|
239
|
+
|| (access !== "read" && access !== "write" && access !== "readwrite")
|
|
240
|
+
|| (creation !== "never" && creation !== "ifMissing" && creation !== "exclusive")
|
|
217
241
|
|| options.noFollow !== undefined && typeof options.noFollow !== "boolean"
|
|
218
242
|
|| options.exactMode !== undefined && typeof options.exactMode !== "boolean"
|
|
219
243
|
|| typeof truncate !== "boolean" || typeof append !== "boolean"
|
|
@@ -226,7 +250,7 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
|
|
|
226
250
|
...(options.noFollow === undefined ? {} : { noFollow: options.noFollow }),
|
|
227
251
|
...(options.exactMode === undefined ? {} : { exactMode: options.exactMode }),
|
|
228
252
|
...(signal === undefined ? {} : { signal }), ...(synchronization === undefined ? {} : { synchronization }) });
|
|
229
|
-
const admittedCapabilities = Object.freeze({ ...capabilities });
|
|
253
|
+
const admittedCapabilities = Object.isFrozen(capabilities) ? capabilities : Object.freeze({ ...capabilities });
|
|
230
254
|
admitCapabilities(path, admitted, admittedCapabilities);
|
|
231
255
|
let backend;
|
|
232
256
|
try {
|
|
@@ -238,9 +262,11 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
|
|
|
238
262
|
}
|
|
239
263
|
try {
|
|
240
264
|
signal?.throwIfAborted();
|
|
241
|
-
const
|
|
265
|
+
const rawBackendCapabilities = backend.capabilities;
|
|
266
|
+
const selected = rawBackendCapabilities ? Object.freeze({ ...rawBackendCapabilities }) : admittedCapabilities;
|
|
242
267
|
signal?.throwIfAborted();
|
|
243
|
-
|
|
268
|
+
if (rawBackendCapabilities)
|
|
269
|
+
admitCapabilities(path, admitted, selected);
|
|
244
270
|
const descriptor = new ManagedFileDescriptor(path, admitted, selected, backend);
|
|
245
271
|
signal?.throwIfAborted();
|
|
246
272
|
return descriptor;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
2
|
-
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
3
3
|
import type { ByteSource } from "../../contracts/io.js";
|
|
4
4
|
export declare class DeviceFileSystem implements FileSystem {
|
|
5
5
|
#private;
|
|
@@ -27,11 +27,16 @@ export declare class DeviceFileSystem implements FileSystem {
|
|
|
27
27
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
28
28
|
publishFileConditional(path: string, source: ByteSource, options: ConditionalFilePublicationOptions): Promise<FileStat>;
|
|
29
29
|
writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
30
|
-
removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions
|
|
30
|
+
removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions & {
|
|
31
|
+
readonly returnRemainingStat?: false | undefined;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
removeEntryConditional(path: string, options: ConditionalRemoveEntryReceiptOptions): Promise<void | FileStat>;
|
|
31
34
|
removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
32
35
|
removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
|
|
33
36
|
createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
34
37
|
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
38
|
+
prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
|
|
39
|
+
prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
|
|
35
40
|
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
36
41
|
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
37
42
|
rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
|
|
@@ -1,29 +1,37 @@
|
|
|
1
|
+
import { snapshotStagingCreation } from "../staging-cleanup.js";
|
|
1
2
|
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
2
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
3
4
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
4
5
|
import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
|
|
5
6
|
import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
|
|
7
|
+
import { tryResolveMemoryDevicePath } from "../memory/index.js";
|
|
6
8
|
import { createDeviceYield, deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
|
|
7
9
|
import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
|
|
8
10
|
import { pathNamespace } from "../path-namespace.js";
|
|
11
|
+
import { runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "../staging-ancestry.js";
|
|
9
12
|
import { openFileDescriptor } from "../descriptor.js";
|
|
13
|
+
import { normalizePath } from "../../contracts/virtual-path.js";
|
|
10
14
|
const views = new WeakMap();
|
|
11
15
|
const deviceCapabilities = Object.freeze({
|
|
16
|
+
synchronousStagingResolution: false,
|
|
12
17
|
readOnly: false, read: true, stat: true, realpath: true, access: true, readdir: false,
|
|
13
18
|
write: true, append: true, exclusiveCreate: true, streamingRead: true, retainedRead: true,
|
|
14
19
|
streamingWrite: true, streamingAppend: true, independentWriteStreams: true, copy: true, exclusiveCopy: true,
|
|
15
20
|
remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
|
|
16
21
|
mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
|
|
17
22
|
permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
|
|
18
|
-
open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
23
|
+
open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
19
24
|
atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
|
|
20
25
|
});
|
|
21
26
|
function globalCapabilities(filesystem) {
|
|
22
27
|
const capabilities = { readOnly: false };
|
|
23
28
|
const optional = {
|
|
24
29
|
open: ["open"],
|
|
30
|
+
retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
31
|
+
synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
32
|
+
synchronousStagingResolution: ["prepareStagingResolution"],
|
|
25
33
|
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
26
|
-
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
34
|
+
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
27
35
|
streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
|
|
28
36
|
streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
29
37
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
|
|
@@ -36,6 +44,8 @@ function globalCapabilities(filesystem) {
|
|
|
36
44
|
if (key === "copy" || key === "exclusiveCopy")
|
|
37
45
|
return undefined;
|
|
38
46
|
let declared = filesystem.capabilities[key];
|
|
47
|
+
if (key === "atomicEntryRemovalReceipt")
|
|
48
|
+
declared = ownedMutationCapabilities(filesystem).atomicEntryRemovalReceipt;
|
|
39
49
|
if (declared === true && optional[key]?.some(method => typeof filesystem[method] !== "function"))
|
|
40
50
|
declared = false;
|
|
41
51
|
if (key === "retainedResize" && filesystem.capabilities.readOnly === true)
|
|
@@ -94,8 +104,16 @@ export class DeviceFileSystem {
|
|
|
94
104
|
registerEntryAuthority(this, async () => "unknown");
|
|
95
105
|
return view;
|
|
96
106
|
}
|
|
97
|
-
#resolve(path, options, followFinal = true, resizeCreate) {
|
|
98
|
-
return resolveDevicePath(this.#filesystem, path, options, followFinal, resizeCreate);
|
|
107
|
+
#resolve(path, options, followFinal = true, resizeCreate, traversal) {
|
|
108
|
+
return resolveDevicePath(this.#filesystem, path, options, followFinal, resizeCreate, traversal);
|
|
109
|
+
}
|
|
110
|
+
#tryResolveSync(path, options) {
|
|
111
|
+
if (options.signal?.aborted)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (Reflect.get(this.#filesystem, pathNamespace) === undefined) {
|
|
114
|
+
return tryResolveMemoryDevicePath(this.#filesystem, path);
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
99
117
|
}
|
|
100
118
|
async #mutable(path, options, followFinal = true) {
|
|
101
119
|
if (reserved(await this.#resolve(path, options, followFinal)))
|
|
@@ -129,10 +147,14 @@ export class DeviceFileSystem {
|
|
|
129
147
|
return reserved(resolved) ? resolved : this.#filesystem.canonicalizeMissingTarget?.(path, options);
|
|
130
148
|
}
|
|
131
149
|
async capabilitiesFor(path, options = {}) {
|
|
150
|
+
options.signal?.throwIfAborted();
|
|
151
|
+
if (options.stagingResolution === true && (normalizePath(path) !== path || !path.startsWith("/")))
|
|
152
|
+
throw new FsError("EINVAL", { path });
|
|
132
153
|
let resolved;
|
|
133
154
|
let selected;
|
|
155
|
+
const traversal = options.stagingResolution === true ? { virtual: false } : undefined;
|
|
134
156
|
try {
|
|
135
|
-
resolved = await this.#resolve(path, options, options.create !== undefined || options.creation !== "exclusive", options.create);
|
|
157
|
+
resolved = await this.#resolve(path, options, options.stagingResolution !== true && (options.create !== undefined || options.creation !== "exclusive"), options.create, traversal);
|
|
136
158
|
}
|
|
137
159
|
catch (error) {
|
|
138
160
|
options.signal?.throwIfAborted();
|
|
@@ -160,6 +182,10 @@ export class DeviceFileSystem {
|
|
|
160
182
|
return { ...deviceCapabilities, open: false, readdir: true, write: false, append: false,
|
|
161
183
|
exclusiveCreate: false, streamingWrite: false, streamingAppend: false, independentWriteStreams: false, descriptorWriteStream: false,
|
|
162
184
|
retainedRead: false, retainedResize: false, streamingRead: false, copy: false, exclusiveCopy: false };
|
|
185
|
+
// This intent cannot bind virtual traversal. Do not infer other path
|
|
186
|
+
// capabilities by resolving the shadowed entry in the backing filesystem.
|
|
187
|
+
if (traversal?.virtual)
|
|
188
|
+
return Object.freeze({ synchronousStagingResolution: false });
|
|
163
189
|
if (selected === undefined) {
|
|
164
190
|
const query = this.#filesystem.capabilitiesFor;
|
|
165
191
|
options.signal?.throwIfAborted();
|
|
@@ -169,26 +195,58 @@ export class DeviceFileSystem {
|
|
|
169
195
|
const observed = ownedMutationCapabilities(this.#filesystem, selected ?? this.#filesystem.capabilities);
|
|
170
196
|
options.signal?.throwIfAborted();
|
|
171
197
|
const capabilities = observed.retainedResize === true ? retainedResizeCapabilities(this.#filesystem, observed) : observed;
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
198
|
+
let unavailable;
|
|
199
|
+
if (options.stagingResolution === true && observed.synchronousStagingResolution === true) {
|
|
200
|
+
try {
|
|
201
|
+
const resolution = snapshotStagingResolution(await this.#filesystem.prepareStagingResolution(path, options));
|
|
202
|
+
options.signal?.throwIfAborted();
|
|
203
|
+
if (resolution.path !== resolved)
|
|
204
|
+
throw new FsError("EAGAIN", { path });
|
|
205
|
+
if ([resolution.path, ...resolution.traversed.map(entry => entry.path)].some(entry => entry === deviceDirectory || entry.startsWith(`${deviceDirectory}/`))) {
|
|
206
|
+
(unavailable ??= {}).synchronousStagingResolution = false;
|
|
207
|
+
}
|
|
208
|
+
runStagingGuard(resolution.validate);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
options.signal?.throwIfAborted();
|
|
212
|
+
if (!isFsError(error, "ENOTSUP"))
|
|
213
|
+
throw error;
|
|
214
|
+
(unavailable ??= {}).synchronousStagingResolution = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (resolved.startsWith(`${deviceDirectory}/`)) {
|
|
218
|
+
const u = unavailable ??= {};
|
|
219
|
+
u.atomicStagingAncestry = false;
|
|
220
|
+
u.synchronousDirectoryValidation = false;
|
|
221
|
+
u.synchronousStagingResolution = false;
|
|
222
|
+
}
|
|
223
|
+
if (typeof this.#filesystem.open !== "function" && capabilities.open !== false)
|
|
224
|
+
(unavailable ??= {}).open = false;
|
|
225
|
+
if (typeof this.#filesystem.readStream !== "function" && capabilities.streamingRead !== false)
|
|
226
|
+
(unavailable ??= {}).streamingRead = false;
|
|
177
227
|
if (typeof this.#filesystem.writeStream !== "function") {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
228
|
+
if (capabilities.streamingWrite !== false)
|
|
229
|
+
(unavailable ??= {}).streamingWrite = false;
|
|
230
|
+
if (capabilities.streamingAppend !== false)
|
|
231
|
+
(unavailable ??= {}).streamingAppend = false;
|
|
232
|
+
if (capabilities.descriptorWriteStream !== false)
|
|
233
|
+
(unavailable ??= {}).descriptorWriteStream = false;
|
|
234
|
+
}
|
|
235
|
+
if (typeof this.#filesystem.openReadFile !== "function" && capabilities.retainedRead !== false)
|
|
236
|
+
(unavailable ??= {}).retainedRead = false;
|
|
237
|
+
if (observed.atomicResize === true && (typeof this.#filesystem.resizeFile !== "function" || observed.readOnly === true) && capabilities.atomicResize !== false)
|
|
238
|
+
(unavailable ??= {}).atomicResize = false;
|
|
239
|
+
const result = unavailable ? { ...capabilities, ...unavailable } : capabilities;
|
|
188
240
|
options.signal?.throwIfAborted();
|
|
189
241
|
return result;
|
|
190
242
|
}
|
|
191
|
-
|
|
243
|
+
stat(path, options = {}) {
|
|
244
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
245
|
+
return this.#filesystem.stat(path, options);
|
|
246
|
+
}
|
|
247
|
+
return this.#statSlow(path, options);
|
|
248
|
+
}
|
|
249
|
+
async #statSlow(path, options) {
|
|
192
250
|
const resolved = await this.#resolve(path, options);
|
|
193
251
|
options.signal?.throwIfAborted();
|
|
194
252
|
if (resolved === nullPath)
|
|
@@ -201,7 +259,13 @@ export class DeviceFileSystem {
|
|
|
201
259
|
options.signal?.throwIfAborted();
|
|
202
260
|
return result;
|
|
203
261
|
}
|
|
204
|
-
|
|
262
|
+
lstat(path, options = {}) {
|
|
263
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
264
|
+
return this.#filesystem.lstat(path, options);
|
|
265
|
+
}
|
|
266
|
+
return this.#lstatSlow(path, options);
|
|
267
|
+
}
|
|
268
|
+
async #lstatSlow(path, options) {
|
|
205
269
|
const resolved = await this.#resolve(path, options, false);
|
|
206
270
|
options.signal?.throwIfAborted();
|
|
207
271
|
if (resolved === nullPath)
|
|
@@ -217,11 +281,23 @@ export class DeviceFileSystem {
|
|
|
217
281
|
compareEntry(path, peer, peerPath, options = {}) {
|
|
218
282
|
return compareEntries(this, path, peer, peerPath, options);
|
|
219
283
|
}
|
|
220
|
-
|
|
284
|
+
realpath(path, options = {}) {
|
|
285
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
286
|
+
return this.#filesystem.realpath(path, options);
|
|
287
|
+
}
|
|
288
|
+
return this.#realpathSlow(path, options);
|
|
289
|
+
}
|
|
290
|
+
async #realpathSlow(path, options) {
|
|
221
291
|
const resolved = await this.#resolve(path, options);
|
|
222
292
|
return reserved(resolved) ? resolved : this.#filesystem.realpath(path, options);
|
|
223
293
|
}
|
|
224
|
-
|
|
294
|
+
readFile(path, options = {}) {
|
|
295
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
296
|
+
return this.#filesystem.readFile(path, options);
|
|
297
|
+
}
|
|
298
|
+
return this.#readFileSlow(path, options);
|
|
299
|
+
}
|
|
300
|
+
async #readFileSlow(path, options) {
|
|
225
301
|
const resolved = await this.#resolve(path, options);
|
|
226
302
|
if (resolved === nullPath) {
|
|
227
303
|
nonnegative(options.maxBytes, path);
|
|
@@ -389,7 +465,14 @@ export class DeviceFileSystem {
|
|
|
389
465
|
throw new FsError("ENOTSUP", { path });
|
|
390
466
|
await this.#filesystem.writeStream(path, source, options);
|
|
391
467
|
}
|
|
392
|
-
|
|
468
|
+
readdir(path, options = {}) {
|
|
469
|
+
const fast = this.#tryResolveSync(path, options);
|
|
470
|
+
if (fast !== undefined && fast !== "/") {
|
|
471
|
+
return this.#filesystem.readdir(path, options);
|
|
472
|
+
}
|
|
473
|
+
return this.#readdirSlow(path, options);
|
|
474
|
+
}
|
|
475
|
+
async #readdirSlow(path, options) {
|
|
393
476
|
const resolved = await this.#resolve(path, options);
|
|
394
477
|
if (resolved === nullPath)
|
|
395
478
|
throw new FsError("ENOTDIR", { path });
|
|
@@ -421,7 +504,13 @@ export class DeviceFileSystem {
|
|
|
421
504
|
admitDirectoryEntries(merged.size, limit, path);
|
|
422
505
|
return [...merged.values()];
|
|
423
506
|
}
|
|
424
|
-
|
|
507
|
+
mkdir(path, options = {}) {
|
|
508
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
509
|
+
return this.#filesystem.mkdir(path, options);
|
|
510
|
+
}
|
|
511
|
+
return this.#mkdirSlow(path, options);
|
|
512
|
+
}
|
|
513
|
+
async #mkdirSlow(path, options) {
|
|
425
514
|
const resolved = await this.#resolve(path, options);
|
|
426
515
|
if (reserved(resolved)) {
|
|
427
516
|
if (resolved !== nullPath && options.recursive)
|
|
@@ -430,7 +519,13 @@ export class DeviceFileSystem {
|
|
|
430
519
|
}
|
|
431
520
|
await this.#filesystem.mkdir(path, options);
|
|
432
521
|
}
|
|
433
|
-
|
|
522
|
+
rm(path, options = {}) {
|
|
523
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
524
|
+
return this.#filesystem.rm(path, options);
|
|
525
|
+
}
|
|
526
|
+
return this.#rmSlow(path, options);
|
|
527
|
+
}
|
|
528
|
+
async #rmSlow(path, options) {
|
|
434
529
|
await this.#mutable(path, options, false);
|
|
435
530
|
await this.#filesystem.rm(path, options);
|
|
436
531
|
}
|
|
@@ -466,11 +561,16 @@ export class DeviceFileSystem {
|
|
|
466
561
|
return this.#filesystem.writeFileConditional(path, data, options);
|
|
467
562
|
}
|
|
468
563
|
async removeEntryConditional(path, options) {
|
|
564
|
+
options = { ...options };
|
|
565
|
+
const requested = options.returnRemainingStat === true;
|
|
469
566
|
await this.#mutable(path, options, false);
|
|
470
567
|
await requireOwnedMutation(this.#filesystem, path, "atomicEntryRemoval", options);
|
|
568
|
+
if (requested)
|
|
569
|
+
await requireOwnedMutation(this.#filesystem, path, "atomicEntryRemovalReceipt", options);
|
|
471
570
|
if (!this.#filesystem.removeEntryConditional)
|
|
472
571
|
throw new FsError("ENOTSUP", { path });
|
|
473
|
-
await this.#filesystem.removeEntryConditional(path, options);
|
|
572
|
+
const receipt = await this.#filesystem.removeEntryConditional(path, options);
|
|
573
|
+
return requested && receipt !== undefined ? Object.freeze({ ...receipt }) : undefined;
|
|
474
574
|
}
|
|
475
575
|
async removeFileConditional(path, options) {
|
|
476
576
|
await this.#mutable(path, options, false);
|
|
@@ -487,20 +587,95 @@ export class DeviceFileSystem {
|
|
|
487
587
|
await this.#filesystem.removeTreeConditional(path, options);
|
|
488
588
|
}
|
|
489
589
|
async createStagedFile(path, name, content, options) {
|
|
590
|
+
options = snapshotStagingCreation(options, path);
|
|
490
591
|
await this.#mutable(path, options, false);
|
|
491
592
|
await requireOwnedMutation(this.#filesystem, path, "atomicFileStaging", options, true);
|
|
593
|
+
if (options.retainCleanup === true)
|
|
594
|
+
await requireOwnedMutation(this.#filesystem, path, "retainedStagingCleanup", options, true);
|
|
492
595
|
if (!this.#filesystem.createStagedFile)
|
|
493
596
|
throw new FsError("ENOTSUP", { path });
|
|
494
597
|
return this.#filesystem.createStagedFile(path, name, content, options);
|
|
495
598
|
}
|
|
496
599
|
async publishStagedFile(staging, destination, options) {
|
|
600
|
+
options.signal?.throwIfAborted();
|
|
601
|
+
if (options.ancestors !== undefined) {
|
|
602
|
+
const entries = snapshotDirectoryAncestry(options.ancestors);
|
|
603
|
+
if (entries.some(entry => entry.path === deviceDirectory || entry.path.startsWith(`${deviceDirectory}/`)))
|
|
604
|
+
throw new FsError("ENOTSUP", { path: destination });
|
|
605
|
+
await requireOwnedMutation(this.#filesystem, destination, "atomicStagingAncestry", options, options.destination === null);
|
|
606
|
+
}
|
|
497
607
|
for (const path of [staging.directory.path, staging.file.path, destination])
|
|
498
608
|
await this.#mutable(path, options, false);
|
|
499
609
|
await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
|
|
610
|
+
await requireOwnedMutation(this.#filesystem, destination, "atomicFileStaging", options, options.destination === null);
|
|
500
611
|
if (!this.#filesystem.publishStagedFile)
|
|
501
612
|
throw new FsError("ENOTSUP", { path: destination });
|
|
613
|
+
if (options.commitGuard !== undefined)
|
|
614
|
+
await requireOwnedMutation(this.#filesystem, destination, "guardedStagingPublication", options, options.destination === null);
|
|
502
615
|
await this.#filesystem.publishStagedFile(staging, destination, options);
|
|
503
616
|
}
|
|
617
|
+
async prepareDirectoryAncestry(ancestors, options = {}) {
|
|
618
|
+
options.signal?.throwIfAborted();
|
|
619
|
+
try {
|
|
620
|
+
const entries = snapshotDirectoryAncestry(ancestors);
|
|
621
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
622
|
+
controls.signal?.throwIfAborted();
|
|
623
|
+
if (entries.some(entry => entry.path === deviceDirectory || entry.path.startsWith(`${deviceDirectory}/`))
|
|
624
|
+
|| !this.#filesystem.prepareDirectoryAncestry) {
|
|
625
|
+
throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry" });
|
|
626
|
+
}
|
|
627
|
+
for (const entry of entries) {
|
|
628
|
+
const declared = await this.#filesystem.capabilitiesFor?.(entry.path, { ...controls, stagingAncestry: true }) ?? this.#filesystem.capabilities;
|
|
629
|
+
controls.signal?.throwIfAborted();
|
|
630
|
+
if (declared.synchronousDirectoryValidation !== true)
|
|
631
|
+
throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry", path: entry.path });
|
|
632
|
+
}
|
|
633
|
+
const guard = await this.#filesystem.prepareDirectoryAncestry(entries, controls);
|
|
634
|
+
controls.signal?.throwIfAborted();
|
|
635
|
+
if (typeof guard !== "function")
|
|
636
|
+
throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry" });
|
|
637
|
+
return () => {
|
|
638
|
+
controls.signal?.throwIfAborted();
|
|
639
|
+
runStagingGuard(guard);
|
|
640
|
+
return true;
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
options.signal?.throwIfAborted();
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async prepareStagingResolution(path, options = {}) {
|
|
649
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
650
|
+
controls.signal?.throwIfAborted();
|
|
651
|
+
if (normalizePath(path) !== path || !path.startsWith("/"))
|
|
652
|
+
throw new FsError("EINVAL", { path });
|
|
653
|
+
try {
|
|
654
|
+
const traversal = { virtual: false };
|
|
655
|
+
const resolved = await this.#resolve(path, controls, false, undefined, traversal);
|
|
656
|
+
controls.signal?.throwIfAborted();
|
|
657
|
+
if (traversal.virtual)
|
|
658
|
+
throw new FsError("ENOTSUP", { path });
|
|
659
|
+
const declared = ownedMutationCapabilities(this.#filesystem, await this.#filesystem.capabilitiesFor?.(path, { ...controls, stagingResolution: true }) ?? this.#filesystem.capabilities);
|
|
660
|
+
controls.signal?.throwIfAborted();
|
|
661
|
+
if (declared.synchronousStagingResolution !== true || !this.#filesystem.prepareStagingResolution)
|
|
662
|
+
throw new FsError("ENOTSUP", { path });
|
|
663
|
+
const receipt = snapshotStagingResolution(await this.#filesystem.prepareStagingResolution(path, controls));
|
|
664
|
+
controls.signal?.throwIfAborted();
|
|
665
|
+
if (receipt.path !== resolved)
|
|
666
|
+
throw new FsError("EAGAIN", { path });
|
|
667
|
+
if ([receipt.path, ...receipt.traversed.map(entry => entry.path)].some(entry => entry === deviceDirectory || entry.startsWith(`${deviceDirectory}/`))) {
|
|
668
|
+
throw new FsError("ENOTSUP", { path });
|
|
669
|
+
}
|
|
670
|
+
const validate = () => { controls.signal?.throwIfAborted(); runStagingGuard(receipt.validate); return true; };
|
|
671
|
+
runStagingGuard(validate);
|
|
672
|
+
return snapshotStagingResolution({ ...receipt, validate });
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
controls.signal?.throwIfAborted();
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
504
679
|
async removeStagedFile(staging, options = {}) {
|
|
505
680
|
for (const path of [staging.directory.path, staging.file.path])
|
|
506
681
|
await this.#mutable(path, options, false);
|
|
@@ -558,7 +733,13 @@ export class DeviceFileSystem {
|
|
|
558
733
|
throw new FsError("EISDIR", { path: destination });
|
|
559
734
|
await this.#filesystem.writeFile(destination, new Uint8Array(), { ...options, flag: options.exclusive ? "wx" : "w" });
|
|
560
735
|
}
|
|
561
|
-
|
|
736
|
+
access(path, mode = 0, options = {}) {
|
|
737
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
738
|
+
return this.#filesystem.access(path, mode, options);
|
|
739
|
+
}
|
|
740
|
+
return this.#accessSlow(path, mode, options);
|
|
741
|
+
}
|
|
742
|
+
async #accessSlow(path, mode, options) {
|
|
562
743
|
const resolved = await this.#resolve(path, options);
|
|
563
744
|
if (resolved === nullPath || resolved === deviceDirectory) {
|
|
564
745
|
if (!Number.isInteger(mode) || mode < 0 || mode > 7)
|
|
@@ -2,4 +2,6 @@ import type { FileSystem, FsOptions } from "../../contracts/filesystem.js";
|
|
|
2
2
|
export declare const nullPath = "/dev/null";
|
|
3
3
|
export declare const deviceDirectory = "/dev";
|
|
4
4
|
export declare function lexicalDevicePath(path: string): string;
|
|
5
|
-
export declare function resolveDevicePath(filesystem: FileSystem, path: string, options: FsOptions, followFinal?: boolean, resizeCreate?: boolean
|
|
5
|
+
export declare function resolveDevicePath(filesystem: FileSystem, path: string, options: FsOptions, followFinal?: boolean, resizeCreate?: boolean, traversal?: {
|
|
6
|
+
virtual: boolean;
|
|
7
|
+
}): Promise<string>;
|