@poe-platform/safe-fs 0.1.807 → 0.1.809
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 +28 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +20 -1
- package/dist/safe-fs/core.d.ts +3 -1
- package/dist/safe-fs/core.js +3 -1
- package/dist/safe-fs/fs/capabilities.d.ts +1 -1
- package/dist/safe-fs/fs/capabilities.js +9 -5
- package/dist/safe-fs/fs/devices/index.d.ts +3 -2
- package/dist/safe-fs/fs/devices/index.js +53 -9
- package/dist/safe-fs/fs/memory/atomic-view.d.ts +1 -0
- package/dist/safe-fs/fs/memory/index.d.ts +6 -3
- package/dist/safe-fs/fs/memory/index.js +101 -13
- package/dist/safe-fs/fs/mount/index.d.ts +4 -2
- package/dist/safe-fs/fs/mount/index.js +169 -10
- package/dist/safe-fs/fs/overlay/index.d.ts +3 -0
- package/dist/safe-fs/fs/overlay/index.js +177 -59
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +1 -0
- package/dist/safe-fs/fs/overlay/memory-publication.js +25 -0
- package/dist/safe-fs/fs/quota/index.js +9 -4
- package/dist/safe-fs/fs/read-file-stream.d.ts +7 -0
- package/dist/safe-fs/fs/read-file-stream.js +103 -0
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +10 -0
- package/dist/safe-fs/fs/real/index.d.ts +1 -0
- package/dist/safe-fs/fs/real/index.js +29 -0
- package/dist/safe-fs/fs/s3/filesystem.d.ts +2 -1
- package/dist/safe-fs/fs/s3/filesystem.js +55 -16
- package/dist/safe-fs/fs/scoped.js +13 -5
- package/dist/safe-fs/index.d.ts +1 -0
- package/dist/safe-fs/index.js +1 -0
- package/dist/safe-fs/runtime-core.d.ts +1 -0
- package/dist/safe-fs/runtime-core.js +1 -0
- package/dist/safe-fs/storage.d.ts +57 -0
- package/dist/safe-fs/storage.js +364 -0
- package/dist/safe-fs/testing/object-io-metrics.js +1 -0
- package/dist/xml-ast/errors.d.ts +4 -0
- package/dist/xml-ast/errors.js +7 -0
- package/dist/xml-ast/index.d.ts +2 -4
- package/dist/xml-ast/index.js +3 -7
- package/dist/xml-ast/stream.d.ts +31 -0
- package/dist/xml-ast/stream.js +179 -0
- package/package.json +10 -6
|
@@ -405,6 +405,7 @@ const ext4HtreeEof64 = (1n << 63n) - 1n;
|
|
|
405
405
|
const extractionStreamGuard = Symbol("extractionStreamGuard");
|
|
406
406
|
const ownedStats = new WeakMap();
|
|
407
407
|
const ownedStores = new WeakMap();
|
|
408
|
+
const receiverCapabilities = new WeakMap();
|
|
408
409
|
// Ensure V8 uses Tagged representation for timestamp fields so all 4 fields share one HeapNumber pointer.
|
|
409
410
|
{
|
|
410
411
|
const dummyAlloc = new MemoryAllocation(new Uint8Array(0), new MemoryLedger(normalizeMemoryFileSystemLimits({})));
|
|
@@ -583,8 +584,8 @@ const SHARED_STOCK_MEMORY_CAPABILITIES = Object.freeze({
|
|
|
583
584
|
permissions: true,
|
|
584
585
|
timestamps: true,
|
|
585
586
|
atomicRename: true,
|
|
586
|
-
atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
|
|
587
|
-
synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
|
|
587
|
+
atomicStagedFileMutation: true, atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
|
|
588
|
+
synchronousDirectoryValidation: true, synchronousStagingResolution: true, synchronousFollowedStagingResolution: true, guardedStagingPublication: true,
|
|
588
589
|
atomicDirectoryMetadata: true,
|
|
589
590
|
streamingRead: true,
|
|
590
591
|
retainedRead: true,
|
|
@@ -601,17 +602,24 @@ function customMemoryCapabilities(filesystem) {
|
|
|
601
602
|
}
|
|
602
603
|
export class MemoryFileSystem {
|
|
603
604
|
capabilitiesFor;
|
|
604
|
-
#customCapabilities;
|
|
605
|
-
#identityScopeSym;
|
|
606
605
|
get identityScope() {
|
|
607
|
-
return (this
|
|
606
|
+
return (this._owner.identityScope ??= Symbol());
|
|
608
607
|
}
|
|
609
608
|
get capabilities() {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
609
|
+
const owner = ownedStores.get(this);
|
|
610
|
+
if (owner) {
|
|
611
|
+
if (!owner.customCapabilities) {
|
|
612
|
+
owner.customCapabilities = customMemoryCapabilities(this);
|
|
613
|
+
owner.capabilities = owner.customCapabilities;
|
|
614
|
+
}
|
|
615
|
+
return owner.customCapabilities;
|
|
613
616
|
}
|
|
614
|
-
|
|
617
|
+
let caps = receiverCapabilities.get(this);
|
|
618
|
+
if (!caps) {
|
|
619
|
+
caps = customMemoryCapabilities(this);
|
|
620
|
+
receiverCapabilities.set(this, caps);
|
|
621
|
+
}
|
|
622
|
+
return caps;
|
|
615
623
|
}
|
|
616
624
|
nextInode = 1;
|
|
617
625
|
ledger;
|
|
@@ -621,7 +629,6 @@ export class MemoryFileSystem {
|
|
|
621
629
|
root;
|
|
622
630
|
totalBytes = 0;
|
|
623
631
|
symlinkCount = 0;
|
|
624
|
-
#atomicView;
|
|
625
632
|
_getAtomicView() {
|
|
626
633
|
const owner = ownedStores.get(this);
|
|
627
634
|
if (!owner ||
|
|
@@ -648,7 +655,27 @@ export class MemoryFileSystem {
|
|
|
648
655
|
return undefined;
|
|
649
656
|
}
|
|
650
657
|
}
|
|
651
|
-
return
|
|
658
|
+
return owner.atomicView ??= {
|
|
659
|
+
replaceContents: (source, destination) => {
|
|
660
|
+
const from = this.entry(source, "overlayAtomicView").node;
|
|
661
|
+
const to = this.entry(destination, "overlayAtomicView").node;
|
|
662
|
+
if (from?.type !== "file" || to?.type !== "file" || from === to || from.nlink !== 1)
|
|
663
|
+
this.fail("EINVAL", "overlayAtomicView", destination);
|
|
664
|
+
this.permission(to, 2, "overlayAtomicView", destination);
|
|
665
|
+
// Exchange owned storage, preserving destination inode/hardlinks and
|
|
666
|
+
// retained allocation snapshots. Stage cleanup releases the old contents.
|
|
667
|
+
const allocation = to.allocation, length = to.byteLength;
|
|
668
|
+
to.allocation = from.allocation;
|
|
669
|
+
to.byteLength = from.byteLength;
|
|
670
|
+
from.allocation = allocation;
|
|
671
|
+
from.byteLength = length;
|
|
672
|
+
to.view = undefined;
|
|
673
|
+
from.view = undefined;
|
|
674
|
+
to.sourceRef = undefined;
|
|
675
|
+
from.sourceRef = undefined;
|
|
676
|
+
this.changed(to);
|
|
677
|
+
this.changed(from);
|
|
678
|
+
},
|
|
652
679
|
stat: (path) => {
|
|
653
680
|
this.validatePath(path, "overlayAtomicView");
|
|
654
681
|
let node = this.root;
|
|
@@ -2159,13 +2186,14 @@ export class MemoryFileSystem {
|
|
|
2159
2186
|
return () => this.verifyDirectoryAncestry(entries, controls);
|
|
2160
2187
|
}
|
|
2161
2188
|
async prepareStagingResolution(path, options = {}) {
|
|
2189
|
+
const followFinal = options.followFinalSymlink === true;
|
|
2162
2190
|
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
2163
2191
|
controls.signal?.throwIfAborted();
|
|
2164
2192
|
this.validatePath(path, "prepareStagingResolution");
|
|
2165
2193
|
if (!path.startsWith("/") || normalizePath(path) !== path)
|
|
2166
2194
|
this.fail("EINVAL", "prepareStagingResolution", path);
|
|
2167
2195
|
const traversed = [];
|
|
2168
|
-
const location = this.resolve(path, "prepareStagingResolution", { followFinal
|
|
2196
|
+
const location = this.resolve(path, "prepareStagingResolution", { followFinal, allowMissing: true, resolutionSteps: traversed });
|
|
2169
2197
|
if (location.node && location.node.type !== "file")
|
|
2170
2198
|
this.fail("ENOTSUP", "prepareStagingResolution", path);
|
|
2171
2199
|
const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
|
|
@@ -2180,7 +2208,7 @@ export class MemoryFileSystem {
|
|
|
2180
2208
|
const currentSteps = [];
|
|
2181
2209
|
let current;
|
|
2182
2210
|
try {
|
|
2183
|
-
current = this.resolve(path, "verifyStagingResolution", { followFinal
|
|
2211
|
+
current = this.resolve(path, "verifyStagingResolution", { followFinal, allowMissing: true, resolutionSteps: currentSteps });
|
|
2184
2212
|
}
|
|
2185
2213
|
catch (error) {
|
|
2186
2214
|
if (error instanceof FsError && ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code))
|
|
@@ -2218,6 +2246,7 @@ export class MemoryFileSystem {
|
|
|
2218
2246
|
}
|
|
2219
2247
|
async publishStagedFile(receipt, destination, options) {
|
|
2220
2248
|
const signal = options.signal;
|
|
2249
|
+
const preserveIdentity = options.preserveIdentity === true;
|
|
2221
2250
|
const guard = options.commitGuard;
|
|
2222
2251
|
const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
|
|
2223
2252
|
const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
|
|
@@ -2245,9 +2274,48 @@ export class MemoryFileSystem {
|
|
|
2245
2274
|
this.fail("EINVAL", "publishStagedFile", destination);
|
|
2246
2275
|
if (target.node && target.node.type !== "file")
|
|
2247
2276
|
this.fail("EAGAIN", "publishStagedFile", destination);
|
|
2277
|
+
if (preserveIdentity) {
|
|
2278
|
+
if (this.capabilities.atomicStagedFileMutation !== true)
|
|
2279
|
+
this.fail("ENOTSUP", "publishStagedFile", destination);
|
|
2280
|
+
if (file.node?.type !== "file" || target.node?.type !== "file" || expected === null)
|
|
2281
|
+
this.fail("EINVAL", "publishStagedFile", destination);
|
|
2282
|
+
this.permission(file.parent, 3, "publishStagedFile", staging.file.path);
|
|
2283
|
+
this.permission(target.parent, 3, "publishStagedFile", destination);
|
|
2284
|
+
this.permission(target.node, 2, "publishStagedFile", destination);
|
|
2285
|
+
// Provider-owned storage is copied directly, without assembling an archive
|
|
2286
|
+
// in caller memory. Preserve retained staging readers and independent writes.
|
|
2287
|
+
this.admitSize(target.node, file.node.byteLength, "publishStagedFile", destination);
|
|
2288
|
+
const allocation = this.allocate(file.node.byteLength, "publishStagedFile", destination);
|
|
2289
|
+
try {
|
|
2290
|
+
for (let offset = 0; offset < file.node.byteLength; offset += 65536) {
|
|
2291
|
+
allocation.data.set(file.node.data.subarray(offset, offset + 65536), offset);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
catch (error) {
|
|
2295
|
+
allocation.release();
|
|
2296
|
+
throw error;
|
|
2297
|
+
}
|
|
2298
|
+
// No awaits or overridable operations occur between admission and commit.
|
|
2299
|
+
const now = Date.now();
|
|
2300
|
+
this.replaceData(target.node, allocation);
|
|
2301
|
+
this.changed(target.node, now);
|
|
2302
|
+
target.node.atimeMs = file.node.atimeMs;
|
|
2303
|
+
target.node.mtimeMs = file.node.mtimeMs;
|
|
2304
|
+
file.parent.entries.delete(file.name);
|
|
2305
|
+
this.ledger.release(file.name.length * 2, 1);
|
|
2306
|
+
file.node.nlink--;
|
|
2307
|
+
file.node.ctimeMs = now;
|
|
2308
|
+
file.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, file.node.revision + 1);
|
|
2309
|
+
this.releaseNode(file.node);
|
|
2310
|
+
this.changed(file.parent, now);
|
|
2311
|
+
memoryCaches.get(this.ledger).clearWrites();
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2248
2314
|
return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
|
|
2249
2315
|
}
|
|
2250
2316
|
async publishStagedFileSet(receipt, destination, options) {
|
|
2317
|
+
if (options.preserveIdentity)
|
|
2318
|
+
this.fail("ENOTSUP", "publishStagedFileSet", destination);
|
|
2251
2319
|
const signal = options.signal;
|
|
2252
2320
|
const guard = options.commitGuard;
|
|
2253
2321
|
const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
|
|
@@ -2481,6 +2549,26 @@ export class MemoryFileSystem {
|
|
|
2481
2549
|
ownedStats.set(stat, { filesystem: this, path, root: this.root });
|
|
2482
2550
|
return stat;
|
|
2483
2551
|
}
|
|
2552
|
+
async *iterateDirectory(path, options = {}) {
|
|
2553
|
+
options.signal?.throwIfAborted();
|
|
2554
|
+
const root = this.root, node = this.resolveNode(path, "iterateDirectory", true);
|
|
2555
|
+
if (node.type !== "directory")
|
|
2556
|
+
this.fail("ENOTDIR", "iterateDirectory", path);
|
|
2557
|
+
this.permission(node, 4, "iterateDirectory", path);
|
|
2558
|
+
const revision = node.revision, ino = node.ino;
|
|
2559
|
+
const check = () => {
|
|
2560
|
+
options.signal?.throwIfAborted();
|
|
2561
|
+
if (this.root !== root || node.ino !== ino || node.revision !== revision || this.resolveNode(path, "iterateDirectory", true) !== node)
|
|
2562
|
+
this.fail("EBUSY", "iterateDirectory", path);
|
|
2563
|
+
this.permission(node, 4, "iterateDirectory", path);
|
|
2564
|
+
};
|
|
2565
|
+
node.atimeMs = Date.now();
|
|
2566
|
+
for (const [name, entry] of node.entries) {
|
|
2567
|
+
check();
|
|
2568
|
+
yield { name, type: entry.type };
|
|
2569
|
+
}
|
|
2570
|
+
check();
|
|
2571
|
+
}
|
|
2484
2572
|
async readdir(path, options = {}) {
|
|
2485
2573
|
const limit = directoryEntryLimit(options, path);
|
|
2486
2574
|
const node = this.resolveNode(path, "readdir", true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
4
4
|
export interface MountFileSystemOptions {
|
|
@@ -18,7 +18,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
18
18
|
private supportsStagingAncestry;
|
|
19
19
|
private supportsDirectoryValidation;
|
|
20
20
|
prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
|
|
21
|
-
prepareStagingResolution(path: string, options?:
|
|
21
|
+
prepareStagingResolution(path: string, options?: PrepareStagingResolutionOptions): Promise<FileStagingResolution>;
|
|
22
22
|
private prepareAncestry;
|
|
23
23
|
private protected;
|
|
24
24
|
openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
@@ -40,6 +40,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
40
40
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
41
41
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
42
42
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
43
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
43
44
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
44
45
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
45
46
|
unlink(path: string, options?: FsOptions): Promise<void>;
|
|
@@ -55,6 +56,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
55
56
|
removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
|
|
56
57
|
createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
57
58
|
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
59
|
+
publishStagedFileSet(staging: FileStaging, destination: string, supplied: PublishStagedFileSetOptions): Promise<FileStat>;
|
|
58
60
|
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
59
61
|
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
60
62
|
rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
|
|
@@ -109,6 +109,7 @@ export class MountFileSystem {
|
|
|
109
109
|
retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
110
110
|
synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
111
111
|
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
112
|
+
atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
112
113
|
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
113
114
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
|
|
114
115
|
descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
@@ -128,7 +129,7 @@ export class MountFileSystem {
|
|
|
128
129
|
return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
|
|
129
130
|
};
|
|
130
131
|
const semantics = Object.fromEntries([
|
|
131
|
-
"guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt",
|
|
132
|
+
"guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt", "atomicStagedFileMutation",
|
|
132
133
|
"trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
|
|
133
134
|
"write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
|
|
134
135
|
"remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
|
|
@@ -190,7 +191,8 @@ export class MountFileSystem {
|
|
|
190
191
|
fail("EINVAL");
|
|
191
192
|
const location = await this.resolve(path, options, {
|
|
192
193
|
allowMissing: options.create ?? true,
|
|
193
|
-
|
|
194
|
+
retainedResolution: options.stagingResolution === true && options.followFinalSymlink === true,
|
|
195
|
+
followFinal: options.stagingResolution === true ? options.followFinalSymlink === true : options.create !== undefined || options.creation !== "exclusive",
|
|
194
196
|
...(options.create === undefined ? {} : { resizeCreate: options.create }),
|
|
195
197
|
});
|
|
196
198
|
const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
@@ -211,15 +213,15 @@ export class MountFileSystem {
|
|
|
211
213
|
const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
|
|
212
214
|
const declared = input === location.local ? resize : ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(input, options) ?? location.mount.backend.capabilities);
|
|
213
215
|
options.signal?.throwIfAborted();
|
|
214
|
-
resolution = declared.synchronousStagingResolution === true && await this.supportsStagingAncestry(location, resize, options);
|
|
216
|
+
resolution = declared.synchronousStagingResolution === true && (options.followFinalSymlink !== true || declared.synchronousFollowedStagingResolution === true) && await this.supportsStagingAncestry(location, resize, options);
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
219
|
const conditionalChmod = options.conditionalChmod === true
|
|
218
220
|
? resize.conditionalChmod === true && typeof location.mount.backend.chmod === "function"
|
|
219
221
|
&& await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
|
|
220
222
|
: this.capabilities.conditionalChmod;
|
|
221
|
-
const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, ...ordinary } = resize;
|
|
222
|
-
const withOpen = { ...ordinary,
|
|
223
|
+
const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, synchronousFollowedStagingResolution: ignoredFollowedResolution, ...ordinary } = resize;
|
|
224
|
+
const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution, ...(options.followFinalSymlink === true ? { synchronousFollowedStagingResolution: resolution } : {}) }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
|
|
223
225
|
const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
|
|
224
226
|
: retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
|
|
225
227
|
if (location.synthetic)
|
|
@@ -263,19 +265,19 @@ export class MountFileSystem {
|
|
|
263
265
|
return this.operation("prepareDirectoryAncestry", ancestors.at(-1)?.path ?? "/", options, () => this.prepareAncestry(ancestors, options));
|
|
264
266
|
}
|
|
265
267
|
prepareStagingResolution(path, options = {}) {
|
|
266
|
-
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
268
|
+
const controls = { ...(options.signal === undefined ? {} : { signal: options.signal }), ...(options.followFinalSymlink === true ? { followFinalSymlink: true } : {}) };
|
|
267
269
|
return this.operation("prepareStagingResolution", path, controls, async () => {
|
|
268
270
|
if (normalizePath(globalPath(path)) !== path)
|
|
269
271
|
fail("EINVAL");
|
|
270
272
|
const observed = [];
|
|
271
|
-
const location = await this.resolve(path, controls, { followFinal:
|
|
273
|
+
const location = await this.resolve(path, controls, { followFinal: controls.followFinalSymlink === true, retainedResolution: controls.followFinalSymlink === true, allowMissing: true, resolutionSteps: observed });
|
|
272
274
|
if (location.synthetic || !within(location.mount.path, path))
|
|
273
275
|
fail("ENOTSUP");
|
|
274
276
|
const backend = location.mount.backend;
|
|
275
277
|
const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
|
|
276
278
|
const declared = ownedMutationCapabilities(backend, await backend.capabilitiesFor?.(input, { ...controls, stagingResolution: true }) ?? backend.capabilities);
|
|
277
279
|
controls.signal?.throwIfAborted();
|
|
278
|
-
if (declared.synchronousStagingResolution !== true || !backend.prepareStagingResolution)
|
|
280
|
+
if (declared.synchronousStagingResolution !== true || controls.followFinalSymlink === true && declared.synchronousFollowedStagingResolution !== true || !backend.prepareStagingResolution)
|
|
279
281
|
fail("ENOTSUP");
|
|
280
282
|
const prefix = directoryAncestryPaths(location.mount.path).map(ancestor => {
|
|
281
283
|
const entry = observed.find(step => step.path === ancestor);
|
|
@@ -483,7 +485,7 @@ export class MountFileSystem {
|
|
|
483
485
|
release = await this.acquireNamespace(options, [
|
|
484
486
|
"rename", "symlink", "link", "unlink", "rm", "rmdir",
|
|
485
487
|
"removeEntryConditional", "removeFileConditional", "removeTreeConditional",
|
|
486
|
-
"publishStagedFile", "removeStagedFile",
|
|
488
|
+
"publishStagedFile", "publishStagedFileSet", "removeStagedFile",
|
|
487
489
|
].includes(syscall));
|
|
488
490
|
const result = await action();
|
|
489
491
|
if (!preserveReceipt)
|
|
@@ -545,6 +547,10 @@ export class MountFileSystem {
|
|
|
545
547
|
let followedFinalLink = false;
|
|
546
548
|
const verified = async (location) => {
|
|
547
549
|
let inspection = !location.stat && settings.resizeCreate === true ? creationVerification : verification;
|
|
550
|
+
// A guarded resolution binds the original link chain and missing target
|
|
551
|
+
// through the authoritative backend receipt; realpath cannot name absence.
|
|
552
|
+
if (settings.retainedResolution && !location.stat && followedFinalLink)
|
|
553
|
+
inspection = undefined;
|
|
548
554
|
// A missing entry has no realpath. Verify its existing parent while
|
|
549
555
|
// leaving dangling final symlinks on the ordinary following path.
|
|
550
556
|
if (inspection && !location.stat && settings.allowMissing && !followedFinalLink && inspection.finalName === undefined) {
|
|
@@ -802,6 +808,76 @@ export class MountFileSystem {
|
|
|
802
808
|
return [...entries.values()].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
803
809
|
});
|
|
804
810
|
}
|
|
811
|
+
async *iterateDirectory(path, options = {}) {
|
|
812
|
+
try {
|
|
813
|
+
const location = await this.operation("iterateDirectory", path, options, async () => {
|
|
814
|
+
const resolved = await this.resolve(path, options);
|
|
815
|
+
if (resolved.stat?.type !== "directory")
|
|
816
|
+
fail("ENOTDIR");
|
|
817
|
+
return resolved;
|
|
818
|
+
});
|
|
819
|
+
// Only configured mount children are retained, never backend entries.
|
|
820
|
+
const mounted = new Set();
|
|
821
|
+
for (const mount of this.mounts) {
|
|
822
|
+
if (mount.path !== location.path && within(location.path, mount.path)) {
|
|
823
|
+
const suffix = mount.path.slice(location.path === "/" ? 1 : location.path.length + 1);
|
|
824
|
+
mounted.add(suffix.split("/")[0]);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const admit = async () => {
|
|
828
|
+
const current = await this.resolve(path, options);
|
|
829
|
+
if (current.path !== location.path || current.mount !== location.mount ||
|
|
830
|
+
current.local !== location.local || current.synthetic !== location.synthetic ||
|
|
831
|
+
current.stat?.type !== "directory")
|
|
832
|
+
fail("EBUSY");
|
|
833
|
+
};
|
|
834
|
+
if (!location.synthetic) {
|
|
835
|
+
const iterator = await this.operation("iterateDirectory", path, options, async () => {
|
|
836
|
+
await admit();
|
|
837
|
+
const backend = location.mount.backend;
|
|
838
|
+
if (!backend.iterateDirectory)
|
|
839
|
+
fail("ENOTSUP");
|
|
840
|
+
return backend.iterateDirectory(location.local, options)[Symbol.asyncIterator]();
|
|
841
|
+
}, undefined, true); // Transfer ownership before checking cancellation on the first pull.
|
|
842
|
+
// Admission covers each backend pull, but never a consumer pause. In
|
|
843
|
+
// particular, caller-backed traversal may publish scratch files between pulls.
|
|
844
|
+
let complete = false, failed = false;
|
|
845
|
+
try {
|
|
846
|
+
while (true) {
|
|
847
|
+
const result = await this.operation("iterateDirectory", path, options, async () => {
|
|
848
|
+
await admit();
|
|
849
|
+
return iterator.next();
|
|
850
|
+
});
|
|
851
|
+
if (result.done) {
|
|
852
|
+
complete = true;
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
const { name, type } = result.value;
|
|
856
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
|
|
857
|
+
fail("EIO");
|
|
858
|
+
if (!mounted.has(name))
|
|
859
|
+
yield { name, type };
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
failed = true;
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
finally {
|
|
867
|
+
if (!complete)
|
|
868
|
+
await finishCleanup(() => iterator.return?.(), failed);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const name of mounted) {
|
|
872
|
+
await this.operation("iterateDirectory", path, options, admit);
|
|
873
|
+
yield { name, type: "directory" };
|
|
874
|
+
}
|
|
875
|
+
options.signal?.throwIfAborted();
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
throw this.error(error, "iterateDirectory", path, options);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
805
881
|
mkdir(path, options = {}) {
|
|
806
882
|
return this.operation("mkdir", path, options, async () => {
|
|
807
883
|
const directories = new Map();
|
|
@@ -973,7 +1049,11 @@ export class MountFileSystem {
|
|
|
973
1049
|
return Object.freeze(staging);
|
|
974
1050
|
const backendCleanup = receipt.cleanup;
|
|
975
1051
|
const cleanup = createStagingCleanup(staging.directory.path, controls => this.operation("removeStagedFile", staging.directory.path, controls, () => backendCleanup.remove(controls), undefined, true), () => backendCleanup.close());
|
|
976
|
-
|
|
1052
|
+
const writer = receipt.writer;
|
|
1053
|
+
return Object.freeze({ ...staging, cleanup, ...(writer ? { writer: Object.freeze({
|
|
1054
|
+
write: (bytes, controls = {}) => this.operation("stagedWrite", staging.file.path, controls, () => writer.write(bytes, controls), undefined, true),
|
|
1055
|
+
finish: (controls = {}) => this.operation("stagedFinish", staging.file.path, controls, async () => snapshotStat(await writer.finish(controls)), undefined, true),
|
|
1056
|
+
}) } : {}) });
|
|
977
1057
|
}
|
|
978
1058
|
catch (error) {
|
|
979
1059
|
await finishCleanup(() => receipt.cleanup?.close(), true);
|
|
@@ -1022,6 +1102,8 @@ export class MountFileSystem {
|
|
|
1022
1102
|
await requireOwnedMutation(local.mount.backend, target.local, "atomicFileStaging", options, options.destination === null);
|
|
1023
1103
|
if (!local.mount.backend.publishStagedFile)
|
|
1024
1104
|
fail("ENOTSUP");
|
|
1105
|
+
if (options.preserveIdentity)
|
|
1106
|
+
await requireOwnedMutation(local.mount.backend, target.local, "atomicStagedFileMutation", options);
|
|
1025
1107
|
if (ancestors || callerGuard !== undefined)
|
|
1026
1108
|
await requireOwnedMutation(local.mount.backend, target.local, "guardedStagingPublication", options, options.destination === null);
|
|
1027
1109
|
if (!ancestors) {
|
|
@@ -1065,6 +1147,83 @@ export class MountFileSystem {
|
|
|
1065
1147
|
});
|
|
1066
1148
|
}, destination, true);
|
|
1067
1149
|
}
|
|
1150
|
+
async publishStagedFileSet(staging, destination, supplied) {
|
|
1151
|
+
const entry = (value) => ({ path: value.path, stat: snapshotStat(value.stat) });
|
|
1152
|
+
staging = { parent: entry(staging.parent), directory: entry(staging.directory), file: entry(staging.file) };
|
|
1153
|
+
const options = {
|
|
1154
|
+
...supplied, parent: snapshotStat(supplied.parent),
|
|
1155
|
+
destination: supplied.destination === null ? null : snapshotStat(supplied.destination),
|
|
1156
|
+
companions: supplied.companions.map(value => ({ ...value, expected: value.expected === null ? null : snapshotStat(value.expected) })),
|
|
1157
|
+
...(supplied.ancestors === undefined ? {} : { ancestors: snapshotDirectoryAncestry(supplied.ancestors) }),
|
|
1158
|
+
};
|
|
1159
|
+
return this.operation("publishStagedFileSet", staging.file.path, options, async () => {
|
|
1160
|
+
validatePath(destination);
|
|
1161
|
+
if (normalizePath(globalPath(destination)) !== destination || destination === "/")
|
|
1162
|
+
fail("EINVAL");
|
|
1163
|
+
const parent = destination.slice(0, destination.lastIndexOf("/")) || "/";
|
|
1164
|
+
const paths = new Set([destination]);
|
|
1165
|
+
for (const companion of options.companions) {
|
|
1166
|
+
validatePath(companion.path);
|
|
1167
|
+
if (normalizePath(globalPath(companion.path)) !== companion.path || paths.has(companion.path) ||
|
|
1168
|
+
(companion.path.slice(0, companion.path.lastIndexOf("/")) || "/") !== parent)
|
|
1169
|
+
fail("EINVAL");
|
|
1170
|
+
paths.add(companion.path);
|
|
1171
|
+
}
|
|
1172
|
+
const ancestors = options.ancestors;
|
|
1173
|
+
if (ancestors && ancestors.at(-1)?.path !== parent)
|
|
1174
|
+
fail("EINVAL");
|
|
1175
|
+
const guard = ancestors ? await this.prepareAncestry(ancestors, options) : undefined;
|
|
1176
|
+
const local = await this.localStaging(staging, options);
|
|
1177
|
+
const target = await this.resolve(destination, options, { followFinal: false, entry: true, allowMissing: true });
|
|
1178
|
+
if (this.protected(target.path))
|
|
1179
|
+
fail("EBUSY");
|
|
1180
|
+
if (local.mount !== target.mount)
|
|
1181
|
+
fail("EXDEV");
|
|
1182
|
+
if (ancestors && target.path !== destination)
|
|
1183
|
+
fail("EAGAIN");
|
|
1184
|
+
this.mutable(target);
|
|
1185
|
+
const backend = local.mount.backend;
|
|
1186
|
+
if (!backend.publishStagedFileSet)
|
|
1187
|
+
fail("ENOTSUP");
|
|
1188
|
+
await requireOwnedMutation(backend, local.staging.directory.path, "atomicFileStaging", options);
|
|
1189
|
+
await requireOwnedMutation(backend, target.local, "atomicFileStaging", options, options.destination === null);
|
|
1190
|
+
const companions = [];
|
|
1191
|
+
for (const companion of options.companions) {
|
|
1192
|
+
const resolved = await this.resolve(companion.path, options, { followFinal: false, entry: true, allowMissing: true });
|
|
1193
|
+
if (this.protected(resolved.path))
|
|
1194
|
+
fail("EBUSY");
|
|
1195
|
+
if (resolved.mount !== target.mount)
|
|
1196
|
+
fail("EXDEV");
|
|
1197
|
+
this.mutable(resolved);
|
|
1198
|
+
companions.push({ ...companion, path: resolved.local });
|
|
1199
|
+
}
|
|
1200
|
+
let localAncestors;
|
|
1201
|
+
if (ancestors) {
|
|
1202
|
+
const first = ancestors.findIndex(value => value.path === target.mount.path);
|
|
1203
|
+
if (first < 0)
|
|
1204
|
+
fail("EINVAL");
|
|
1205
|
+
localAncestors = ancestors.slice(first).map(value => ({
|
|
1206
|
+
path: target.mount.path === "/" ? value.path : value.path.slice(target.mount.path.length) || "/", stat: value.stat,
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
const callerGuard = options.commitGuard;
|
|
1210
|
+
if (guard || callerGuard !== undefined)
|
|
1211
|
+
await requireOwnedMutation(backend, target.local, "guardedStagingPublication", options, options.destination === null);
|
|
1212
|
+
// One backend performs all version checks and the indivisible commit.
|
|
1213
|
+
// The namespace lock covers admission through its committed receipt.
|
|
1214
|
+
return snapshotStat(await backend.publishStagedFileSet(local.staging, target.local, {
|
|
1215
|
+
...options, companions,
|
|
1216
|
+
...(localAncestors === undefined ? {} : { ancestors: localAncestors }),
|
|
1217
|
+
...(guard || callerGuard !== undefined ? { commitGuard: () => {
|
|
1218
|
+
if (callerGuard !== undefined)
|
|
1219
|
+
runStagingGuard(callerGuard);
|
|
1220
|
+
if (guard)
|
|
1221
|
+
runStagingGuard(guard);
|
|
1222
|
+
return true;
|
|
1223
|
+
} } : {}),
|
|
1224
|
+
}));
|
|
1225
|
+
}, destination, true);
|
|
1226
|
+
}
|
|
1068
1227
|
removeStagedFile(staging, options = {}) {
|
|
1069
1228
|
return this.operation("removeStagedFile", staging.directory.path, options, async () => {
|
|
1070
1229
|
const local = await this.localStaging(staging, options);
|
|
@@ -43,6 +43,7 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
43
43
|
private bytes;
|
|
44
44
|
private cleanGarbage;
|
|
45
45
|
cleanup(options?: FsOptions): Promise<void>;
|
|
46
|
+
private stagingOptions;
|
|
46
47
|
private staged;
|
|
47
48
|
private preserve;
|
|
48
49
|
private clone;
|
|
@@ -64,6 +65,7 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
64
65
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
65
66
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
|
|
66
67
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
68
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
67
69
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
68
70
|
access(path: string, requestedMode?: number, options?: FsOptions): Promise<void>;
|
|
69
71
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
@@ -82,6 +84,7 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
82
84
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
83
85
|
private bounded;
|
|
84
86
|
private streamToUpper;
|
|
87
|
+
private entryBytes;
|
|
85
88
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
86
89
|
}
|
|
87
90
|
export declare function createOverlayFileSystem(options: OverlayFileSystemOptions): OverlayFileSystem;
|