@poe-platform/safe-fs 0.1.511 → 0.1.513
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/safe-fs/bridge/stats.js +1 -1
- package/dist/safe-fs/contracts/errors.d.ts +1 -0
- package/dist/safe-fs/contracts/errors.js +1 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +22 -2
- package/dist/safe-fs/fs/capabilities.d.ts +4 -2
- package/dist/safe-fs/fs/capabilities.js +80 -9
- package/dist/safe-fs/fs/devices/index.d.ts +4 -3
- package/dist/safe-fs/fs/devices/index.js +75 -16
- package/dist/safe-fs/fs/devices/path.d.ts +1 -1
- package/dist/safe-fs/fs/devices/path.js +111 -7
- package/dist/safe-fs/fs/memory/index.d.ts +5 -2
- package/dist/safe-fs/fs/memory/index.js +107 -10
- package/dist/safe-fs/fs/mount/index.d.ts +4 -3
- package/dist/safe-fs/fs/mount/index.js +66 -14
- package/dist/safe-fs/fs/overlay/index.d.ts +3 -3
- package/dist/safe-fs/fs/overlay/index.js +9 -3
- package/dist/safe-fs/fs/path-namespace.d.ts +4 -0
- package/dist/safe-fs/fs/path-namespace.js +63 -0
- package/dist/safe-fs/fs/quota/index.js +142 -6
- package/dist/safe-fs/fs/readonly/index.d.ts +4 -3
- package/dist/safe-fs/fs/readonly/index.js +14 -1
- package/dist/safe-fs/fs/real/index.d.ts +3 -2
- package/dist/safe-fs/fs/real/index.js +177 -8
- package/dist/safe-fs/fs/scoped.js +111 -13
- package/dist/safe-fs/native/fs-seek/linux-x64-glibc.node +0 -0
- package/dist/safe-fs/native/fs-seek/loader.d.ts +5 -0
- package/dist/safe-fs/native/fs-seek/loader.mjs +120 -0
- package/dist/safe-fs/native/fs-seek/manifest.json +46 -0
- package/dist/safe-fs/node/native-seek.d.ts +3 -0
- package/dist/safe-fs/node/native-seek.js +54 -0
- package/package.json +7 -1
|
@@ -8,6 +8,7 @@ import { MemoryAllocation, MemoryLedger } from "./ledger.js";
|
|
|
8
8
|
import { normalizeMemoryFileSystemLimits } from "./limits.js";
|
|
9
9
|
export { defaultMemoryFileSystemLimits } from "./limits.js";
|
|
10
10
|
const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
|
|
11
|
+
const ext4HtreeEof64 = (1n << 63n) - 1n;
|
|
11
12
|
const ownedStats = new WeakMap();
|
|
12
13
|
const ownedStores = new WeakMap();
|
|
13
14
|
const registeredAuthorities = new WeakSet();
|
|
@@ -67,6 +68,7 @@ export class MemoryFileSystem {
|
|
|
67
68
|
atomicRename: true,
|
|
68
69
|
streamingRead: true,
|
|
69
70
|
retainedRead: true,
|
|
71
|
+
get retainedResize() { return stockRetainedResize(filesystem); },
|
|
70
72
|
streamingWrite: true,
|
|
71
73
|
get descriptorWriteStream() { return stockDescriptorWrite(filesystem); },
|
|
72
74
|
});
|
|
@@ -82,6 +84,7 @@ export class MemoryFileSystem {
|
|
|
82
84
|
const root = this.root;
|
|
83
85
|
ownedStores.set(this, {
|
|
84
86
|
root,
|
|
87
|
+
ledger: this.ledger,
|
|
85
88
|
capabilities: this.capabilities,
|
|
86
89
|
intact: () => this.root === root,
|
|
87
90
|
});
|
|
@@ -181,7 +184,7 @@ export class MemoryFileSystem {
|
|
|
181
184
|
this.validatePath(path, syscall);
|
|
182
185
|
const pending = path.split("/").filter(Boolean);
|
|
183
186
|
if (path.endsWith("/"))
|
|
184
|
-
pending.push("
|
|
187
|
+
pending.push("");
|
|
185
188
|
const stack = [{ node: this.root, name: "" }];
|
|
186
189
|
let links = 0;
|
|
187
190
|
while (pending.length > 0) {
|
|
@@ -189,8 +192,9 @@ export class MemoryFileSystem {
|
|
|
189
192
|
const current = stack[stack.length - 1].node;
|
|
190
193
|
if (current.type !== "directory")
|
|
191
194
|
this.fail("ENOTDIR", syscall, path);
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
if (!(component === "" && pending.length === 0 && options.resizeCreate !== undefined))
|
|
196
|
+
this.permission(current, 1, syscall, path);
|
|
197
|
+
if (component === "." || component === "")
|
|
194
198
|
continue;
|
|
195
199
|
if (component === "..") {
|
|
196
200
|
if (stack.length > 1)
|
|
@@ -199,6 +203,8 @@ export class MemoryFileSystem {
|
|
|
199
203
|
}
|
|
200
204
|
if (new TextEncoder().encode(component).byteLength > 255)
|
|
201
205
|
this.fail("ENAMETOOLONG", syscall, path);
|
|
206
|
+
if (options.resizeCreate === true && pending.length === 1 && pending[0] === "")
|
|
207
|
+
this.fail("EISDIR", syscall, path);
|
|
202
208
|
let node = current.entries.get(component);
|
|
203
209
|
if (!node && options.createDirectories !== undefined) {
|
|
204
210
|
this.permission(current, 3, syscall, path);
|
|
@@ -216,7 +222,9 @@ export class MemoryFileSystem {
|
|
|
216
222
|
this.fail("ELOOP", syscall, path);
|
|
217
223
|
const target = node.target.split("/").filter(Boolean);
|
|
218
224
|
if (node.target.endsWith("/"))
|
|
219
|
-
target.push("
|
|
225
|
+
target.push("");
|
|
226
|
+
if (options.resizeCreate !== undefined && target.at(-1) === "" && pending[0] === "")
|
|
227
|
+
target.pop();
|
|
220
228
|
pending.unshift(...target);
|
|
221
229
|
if (node.target.startsWith("/"))
|
|
222
230
|
stack.splice(1);
|
|
@@ -251,11 +259,12 @@ export class MemoryFileSystem {
|
|
|
251
259
|
snapshot(node) {
|
|
252
260
|
return {
|
|
253
261
|
type: node.type,
|
|
262
|
+
preferredIoBlockSize: 4096,
|
|
254
263
|
size: node.type === "file" ? node.data.byteLength
|
|
255
264
|
: node.type === "symlink" ? new TextEncoder().encode(node.target).byteLength : 0,
|
|
256
265
|
mode: node.mode, ...(ownedStores.get(this)?.intact() ? { identityScope: this.identityScope } : {}),
|
|
257
266
|
ino: node.ino, dev: 0, uid: 0, gid: 0,
|
|
258
|
-
nlink: node.type === "directory"
|
|
267
|
+
nlink: node.type === "directory" && node.nlink !== 0
|
|
259
268
|
? 2 + [...node.entries.values()].filter((entry) => entry.type === "directory").length : node.nlink,
|
|
260
269
|
atimeMs: node.atimeMs, mtimeMs: node.mtimeMs, ctimeMs: node.ctimeMs, birthtimeMs: node.birthtimeMs,
|
|
261
270
|
};
|
|
@@ -650,7 +659,10 @@ export class MemoryFileSystem {
|
|
|
650
659
|
this.integer(length, "truncate", path);
|
|
651
660
|
const node = this.file(path, "truncate");
|
|
652
661
|
this.permission(node, 2, "truncate", path);
|
|
653
|
-
|
|
662
|
+
this.resizeNode(node, length, "truncate", path);
|
|
663
|
+
}
|
|
664
|
+
resizeNode(node, length, syscall, path) {
|
|
665
|
+
const data = this.allocate(length, syscall, path);
|
|
654
666
|
try {
|
|
655
667
|
data.data.set(node.data.subarray(0, length));
|
|
656
668
|
}
|
|
@@ -661,12 +673,71 @@ export class MemoryFileSystem {
|
|
|
661
673
|
this.replaceData(node, data);
|
|
662
674
|
this.changed(node);
|
|
663
675
|
}
|
|
664
|
-
async
|
|
676
|
+
async openResizeFile(path, options = {}) {
|
|
665
677
|
options.signal?.throwIfAborted();
|
|
678
|
+
if (!stockRetainedResize(this))
|
|
679
|
+
throw new FsError("ENOTSUP", { syscall: "openResizeFile", path });
|
|
680
|
+
const location = this.resolve(path, "openResizeFile", { allowMissing: options.create === true, resizeCreate: options.create ?? false });
|
|
681
|
+
let mode = 0o666;
|
|
682
|
+
if (location.node) {
|
|
683
|
+
if (location.node.type !== "file")
|
|
684
|
+
this.fail("EISDIR", "openResizeFile", path);
|
|
685
|
+
this.permission(location.node, 2, "openResizeFile", path);
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
this.permission(location.parent, 3, "openResizeFile", path);
|
|
689
|
+
mode = this.mode(options.mode, mode, "openResizeFile", path);
|
|
690
|
+
}
|
|
691
|
+
this.ledger.reserve(path.length * 2, 1, "openResizeFile", path);
|
|
692
|
+
let node;
|
|
693
|
+
try {
|
|
694
|
+
node = this.openWrite(path, {}, "openResizeFile", { location, mode, append: false });
|
|
695
|
+
node.references++;
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
this.ledger.release(path.length * 2, 1);
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
const snapshot = this.snapshot.bind(this);
|
|
702
|
+
const integer = this.integer.bind(this);
|
|
703
|
+
const resizeNode = this.resizeNode.bind(this);
|
|
704
|
+
const releaseReference = this.releaseReference.bind(this);
|
|
705
|
+
let closing;
|
|
706
|
+
const current = (signal, syscall) => {
|
|
707
|
+
signal?.throwIfAborted();
|
|
708
|
+
if (!node)
|
|
709
|
+
throw new FsError("EBADF", { syscall, path });
|
|
710
|
+
return node;
|
|
711
|
+
};
|
|
712
|
+
return {
|
|
713
|
+
async stat(options = {}) { return snapshot(current(options.signal, "fstat")); },
|
|
714
|
+
async seekEnd(options = {}) { return BigInt(current(options.signal, "lseek").data.byteLength); },
|
|
715
|
+
async truncate(length, options = {}) {
|
|
716
|
+
const file = current(options.signal, "ftruncate");
|
|
717
|
+
integer(length, "ftruncate", path);
|
|
718
|
+
resizeNode(file, length, "ftruncate", path);
|
|
719
|
+
},
|
|
720
|
+
close() {
|
|
721
|
+
if (!closing) {
|
|
722
|
+
const retained = node;
|
|
723
|
+
node = undefined;
|
|
724
|
+
closing = Promise.resolve().then(() => { if (retained)
|
|
725
|
+
releaseReference(retained, path); });
|
|
726
|
+
}
|
|
727
|
+
return closing;
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
async openReadFile(path, options = {}) {
|
|
732
|
+
const signal = options.signal;
|
|
733
|
+
signal?.throwIfAborted();
|
|
734
|
+
const allowDirectory = options.allowDirectory === true;
|
|
735
|
+
signal?.throwIfAborted();
|
|
666
736
|
const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openReadFile", path }); };
|
|
667
737
|
const owner = ownedStores.get(this);
|
|
668
738
|
if (!owner || Object.getPrototypeOf(this) !== MemoryFileSystem.prototype
|
|
669
|
-
|| Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root
|
|
739
|
+
|| Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root
|
|
740
|
+
|| Object.getOwnPropertyDescriptor(this, "ledger")?.value !== owner.ledger)
|
|
670
741
|
unsupported();
|
|
671
742
|
for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
|
|
672
743
|
"file", "resolve", "permission", "validatePath", "fail", "snapshot", "integer", "releaseReference", "releaseNode"]) {
|
|
@@ -675,10 +746,16 @@ export class MemoryFileSystem {
|
|
|
675
746
|
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
|
|
676
747
|
unsupported();
|
|
677
748
|
}
|
|
678
|
-
|
|
749
|
+
const retainedRead = Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead;
|
|
750
|
+
signal?.throwIfAborted();
|
|
751
|
+
if (retainedRead !== true)
|
|
679
752
|
unsupported();
|
|
680
|
-
|
|
753
|
+
const selected = allowDirectory ? this.resolve(path, "openReadFile").node : this.file(path, "openReadFile");
|
|
754
|
+
if (selected.type !== "file" && selected.type !== "directory")
|
|
755
|
+
this.fail("EISDIR", "openReadFile", path);
|
|
756
|
+
let node = selected;
|
|
681
757
|
this.permission(node, 4, "openReadFile", path);
|
|
758
|
+
signal?.throwIfAborted();
|
|
682
759
|
this.ledger.reserve(path.length * 2, 1, "openReadFile", path);
|
|
683
760
|
node.references++;
|
|
684
761
|
const snapshot = this.snapshot.bind(this);
|
|
@@ -695,6 +772,10 @@ export class MemoryFileSystem {
|
|
|
695
772
|
async stat(options = {}) {
|
|
696
773
|
return snapshot(current(options.signal, "fstat"));
|
|
697
774
|
},
|
|
775
|
+
async seekEnd(options = {}) {
|
|
776
|
+
const file = current(options.signal, "lseek");
|
|
777
|
+
return file.type === "directory" ? ext4HtreeEof64 : BigInt(file.data.byteLength);
|
|
778
|
+
},
|
|
698
779
|
async read(position, maxBytes, options = {}) {
|
|
699
780
|
const file = current(options.signal, "read");
|
|
700
781
|
integer(position, "read", path);
|
|
@@ -702,6 +783,8 @@ export class MemoryFileSystem {
|
|
|
702
783
|
if (maxBytes === 0 || maxBytes > Number.MAX_SAFE_INTEGER - position) {
|
|
703
784
|
throw new FsError("EINVAL", { syscall: "read", path });
|
|
704
785
|
}
|
|
786
|
+
if (file.type === "directory")
|
|
787
|
+
throw new FsError("EISDIR", { syscall: "read", path });
|
|
705
788
|
const bytes = file.data.slice(position, position + maxBytes);
|
|
706
789
|
file.atimeMs = Date.now();
|
|
707
790
|
return bytes;
|
|
@@ -800,6 +883,20 @@ function stockDescriptorWrite(filesystem) {
|
|
|
800
883
|
}
|
|
801
884
|
return true;
|
|
802
885
|
}
|
|
886
|
+
function stockRetainedResize(filesystem) {
|
|
887
|
+
if (!stockDescriptorWrite(filesystem))
|
|
888
|
+
return false;
|
|
889
|
+
const owner = ownedStores.get(filesystem);
|
|
890
|
+
if (!owner || Object.getOwnPropertyDescriptor(filesystem, "ledger")?.value !== owner.ledger)
|
|
891
|
+
return false;
|
|
892
|
+
for (const name of ["openResizeFile", "truncate", "resizeNode", "snapshot"]) {
|
|
893
|
+
const descriptor = Object.getOwnPropertyDescriptor(filesystem, name)
|
|
894
|
+
?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
|
|
895
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
803
900
|
export function createMemoryFileSystem(options = {}) {
|
|
804
901
|
return new MemoryFileSystem(normalizeMemoryFileSystemLimits(options));
|
|
805
902
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export interface MountFileSystemOptions {
|
|
4
4
|
readonly root: FileSystem;
|
|
@@ -9,9 +9,10 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
9
9
|
private readonly mounts;
|
|
10
10
|
constructor(options: MountFileSystemOptions);
|
|
11
11
|
private select;
|
|
12
|
-
capabilitiesFor(path: string, options?:
|
|
12
|
+
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
13
13
|
private protected;
|
|
14
|
-
openReadFile(path: string, options?:
|
|
14
|
+
openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
15
|
+
openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
15
16
|
private error;
|
|
16
17
|
private operation;
|
|
17
18
|
private components;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { FsError, isFsError, toFsError } from "../../contracts/errors.js";
|
|
2
2
|
import { readBytes } from "../../contracts/io.js";
|
|
3
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
4
|
-
import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities } from "../capabilities.js";
|
|
4
|
+
import { openRetainedReadFile, openRetainedResizeFile, readOnlyCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
|
|
5
5
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
6
6
|
import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
7
|
+
import { pathNamespace } from "../path-namespace.js";
|
|
7
8
|
import { compareIdentity } from "./identity.js";
|
|
8
9
|
import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
|
|
9
10
|
const syntheticStat = Object.freeze({
|
|
@@ -20,10 +21,11 @@ function fail(code) {
|
|
|
20
21
|
throw new FsError(code);
|
|
21
22
|
}
|
|
22
23
|
function snapshotStat(stat) {
|
|
23
|
-
const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
24
|
+
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
24
25
|
return {
|
|
25
26
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
26
27
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
28
|
+
...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
|
|
27
29
|
...(birthtimeMs === undefined ? {} : { birthtimeMs }),
|
|
28
30
|
...(identityScope === undefined ? {} : { identityScope }),
|
|
29
31
|
...(ino === undefined ? {} : { ino }),
|
|
@@ -67,6 +69,10 @@ export class MountFileSystem {
|
|
|
67
69
|
add(path, backend);
|
|
68
70
|
}
|
|
69
71
|
this.mounts = mounts.sort((left, right) => right.path.length - left.path.length);
|
|
72
|
+
const selectNamespace = this.select.bind(this);
|
|
73
|
+
Object.defineProperty(this, pathNamespace, {
|
|
74
|
+
value: Object.freeze({ select: (path) => selectNamespace(path).path }),
|
|
75
|
+
});
|
|
70
76
|
registerEntryView(this, (path, options) => this.operation("compareEntry", path, options, async () => {
|
|
71
77
|
const location = await this.resolve(path, options);
|
|
72
78
|
if (location.synthetic)
|
|
@@ -85,7 +91,7 @@ export class MountFileSystem {
|
|
|
85
91
|
const common = (capability) => {
|
|
86
92
|
const optional = {
|
|
87
93
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
|
|
88
|
-
descriptorWriteStream: ["writeStream"],
|
|
94
|
+
descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"],
|
|
89
95
|
};
|
|
90
96
|
const values = mounts.map(({ backend }) => {
|
|
91
97
|
if (backend.capabilities.readOnly === true
|
|
@@ -104,7 +110,7 @@ export class MountFileSystem {
|
|
|
104
110
|
"read", "stat", "readdir", "realpath", "access",
|
|
105
111
|
"write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
|
|
106
112
|
"remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
|
|
107
|
-
"streamingAppend", "randomAccessWrite", "descriptorWriteStream", "symlinks", "hardlinks", "permissions", "timestamps",
|
|
113
|
+
"streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "symlinks", "hardlinks", "permissions", "timestamps",
|
|
108
114
|
].map(capability => [capability, common(capability)]).filter(([, value]) => value !== undefined));
|
|
109
115
|
this.capabilities = Object.freeze({
|
|
110
116
|
get snapshotRmdir() { return mounts.some(({ backend }) => backend.capabilities.snapshotRmdir === true); },
|
|
@@ -122,14 +128,17 @@ export class MountFileSystem {
|
|
|
122
128
|
}
|
|
123
129
|
async capabilitiesFor(path, options = {}) {
|
|
124
130
|
return this.operation("capabilitiesFor", path, options, async () => {
|
|
125
|
-
const location = await this.resolve(path, options, {
|
|
131
|
+
const location = await this.resolve(path, options, {
|
|
132
|
+
allowMissing: options.create ?? true,
|
|
133
|
+
...(options.create === undefined ? {} : { resizeCreate: options.create }),
|
|
134
|
+
});
|
|
126
135
|
const observed = await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
127
136
|
?? location.mount.backend.capabilities;
|
|
128
137
|
const declared = observed.descriptorWriteStream === true
|
|
129
138
|
&& (typeof location.mount.backend.writeStream !== "function" || observed.readOnly === true || observed.streamingWrite === false)
|
|
130
139
|
? { ...observed, descriptorWriteStream: false } : observed;
|
|
131
140
|
const capabilities = location.synthetic ? { ...declared, retainedRead: false }
|
|
132
|
-
: retainedReadCapabilities(location.mount.backend, declared);
|
|
141
|
+
: retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, declared));
|
|
133
142
|
if (location.synthetic)
|
|
134
143
|
return readOnlyCapabilities(capabilities);
|
|
135
144
|
if (this.mounts.length === 1 || capabilities.readOnly === true)
|
|
@@ -158,6 +167,27 @@ export class MountFileSystem {
|
|
|
158
167
|
throw error ? this.error(error, "openReadFile", path, options) : error;
|
|
159
168
|
}
|
|
160
169
|
}
|
|
170
|
+
async openResizeFile(path, options = {}) {
|
|
171
|
+
try {
|
|
172
|
+
options.signal?.throwIfAborted();
|
|
173
|
+
const resolution = this.resolve(path, options, { allowMissing: options.create === true, resizeCreate: options.create ?? false });
|
|
174
|
+
const signal = options.signal;
|
|
175
|
+
const location = await (signal ? new Promise((resolve, reject) => {
|
|
176
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
177
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
178
|
+
resolution.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
179
|
+
if (signal.aborted)
|
|
180
|
+
abort();
|
|
181
|
+
}) : resolution);
|
|
182
|
+
options.signal?.throwIfAborted();
|
|
183
|
+
if (location.synthetic)
|
|
184
|
+
fail("EROFS");
|
|
185
|
+
return await openRetainedResizeFile(location.mount.backend, location.local, options);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw error ? this.error(error, "openResizeFile", path, options) : error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
161
191
|
error(error, syscall, path, options, dest) {
|
|
162
192
|
if (options.signal?.aborted && error === options.signal.reason)
|
|
163
193
|
return error;
|
|
@@ -210,24 +240,28 @@ export class MountFileSystem {
|
|
|
210
240
|
const stack = [await this.lookup("/", options)];
|
|
211
241
|
let boundary;
|
|
212
242
|
let verification;
|
|
243
|
+
let creationVerification;
|
|
213
244
|
const verified = async (location) => {
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
const inspection = !location.stat && settings.resizeCreate === true ? creationVerification : verification;
|
|
246
|
+
if (inspection) {
|
|
247
|
+
const canonical = await inspection.mount.backend.realpath(inspection.path, options);
|
|
216
248
|
validatePath(canonical);
|
|
217
249
|
if (!canonical.startsWith("/") || normalizePath(canonical) !== canonical)
|
|
218
250
|
fail("EIO");
|
|
219
|
-
if (
|
|
251
|
+
if (inspection.finalName !== undefined) {
|
|
220
252
|
try {
|
|
221
|
-
await
|
|
253
|
+
await inspection.mount.backend.lstat(`${inspection.path}/${inspection.finalName}`, options);
|
|
254
|
+
if (!location.stat && settings.resizeCreate === true)
|
|
255
|
+
fail("ENOTSUP");
|
|
222
256
|
}
|
|
223
257
|
catch (error) {
|
|
224
258
|
if (location.stat || toFsError(error).code !== "ENOENT")
|
|
225
259
|
throw error;
|
|
226
260
|
}
|
|
227
261
|
}
|
|
228
|
-
const expected =
|
|
229
|
-
: `${canonical === "/" ? "" : canonical}/${
|
|
230
|
-
if (location.mount !==
|
|
262
|
+
const expected = inspection.finalName === undefined ? canonical
|
|
263
|
+
: `${canonical === "/" ? "" : canonical}/${inspection.finalName}`;
|
|
264
|
+
if (location.mount !== inspection.mount || location.local !== expected)
|
|
231
265
|
fail("ENOTSUP");
|
|
232
266
|
}
|
|
233
267
|
return location;
|
|
@@ -239,8 +273,10 @@ export class MountFileSystem {
|
|
|
239
273
|
const current = stack[stack.length - 1];
|
|
240
274
|
if (current.stat?.type !== "directory")
|
|
241
275
|
fail("ENOTDIR");
|
|
276
|
+
if (component.trailing && pending.length === 0 && settings.resizeCreate !== undefined)
|
|
277
|
+
fail("EISDIR");
|
|
242
278
|
if (!current.synthetic && !settings.createDirectories?.has(current.path)) {
|
|
243
|
-
const mode = current.mount.backend.capabilities.permissions === false ? 0 : 1;
|
|
279
|
+
const mode = settings.resizeCreate === undefined && current.mount.backend.capabilities.permissions === false ? 0 : 1;
|
|
244
280
|
await current.mount.backend.access(current.local, mode, options);
|
|
245
281
|
}
|
|
246
282
|
if (component.name === ".")
|
|
@@ -257,6 +293,8 @@ export class MountFileSystem {
|
|
|
257
293
|
const nextPath = `${current.path === "/" ? "" : current.path}/${component.name}`;
|
|
258
294
|
if (boundary && this.select(nextPath) !== boundary)
|
|
259
295
|
fail("EACCES");
|
|
296
|
+
if (settings.resizeCreate === true && pending.length === 1 && pending[0].trailing)
|
|
297
|
+
fail("EISDIR");
|
|
260
298
|
let next = settings.createDirectories?.get(nextPath)
|
|
261
299
|
?? await this.lookup(nextPath, options, current, settings.createDirectories?.has(current.path));
|
|
262
300
|
if (!next.stat) {
|
|
@@ -299,6 +337,20 @@ export class MountFileSystem {
|
|
|
299
337
|
const readlink = this.optional(next, "readlink", "symlinks");
|
|
300
338
|
const target = await readlink.call(next.mount.backend, next.local, options);
|
|
301
339
|
const targetParts = this.components(target);
|
|
340
|
+
if (settings.resizeCreate !== undefined && targetParts.at(-1)?.trailing && pending[0]?.trailing)
|
|
341
|
+
targetParts.pop();
|
|
342
|
+
if (settings.resizeCreate === true) {
|
|
343
|
+
const remainder = [...targetParts, ...pending];
|
|
344
|
+
const finalName = remainder.at(-1)?.name;
|
|
345
|
+
if (finalName !== undefined && finalName !== "." && finalName !== "..") {
|
|
346
|
+
const parent = target.startsWith("/") ? "" : next.local.slice(0, next.local.lastIndexOf("/"));
|
|
347
|
+
creationVerification = {
|
|
348
|
+
mount: next.mount,
|
|
349
|
+
path: `${parent}/${remainder.slice(0, -1).map(part => part.name).join("/")}`,
|
|
350
|
+
finalName,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
302
354
|
if (target.startsWith("/")) {
|
|
303
355
|
const rootIndex = stack.findIndex((entry) => entry.path === next.mount.path);
|
|
304
356
|
if (rootIndex < 0)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ByteSource } from "../../contracts/io.js";
|
|
2
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
3
|
export interface OverlayFileSystemOptions {
|
|
4
4
|
readonly upper: FileSystem;
|
|
5
5
|
readonly lower: FileSystem;
|
|
@@ -18,8 +18,8 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
18
18
|
private queue;
|
|
19
19
|
constructor(options: OverlayFileSystemOptions);
|
|
20
20
|
private run;
|
|
21
|
-
capabilitiesFor(path: string, options?:
|
|
22
|
-
openReadFile(path: string, options?:
|
|
21
|
+
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
22
|
+
openReadFile(path: string, options?: OpenReadFileOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
|
|
23
23
|
private writable;
|
|
24
24
|
private permission;
|
|
25
25
|
private maybeStat;
|
|
@@ -8,10 +8,11 @@ import { compareIdentity } from "../mount/identity.js";
|
|
|
8
8
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
9
9
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
10
10
|
function snapshotStat(stat) {
|
|
11
|
-
const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
11
|
+
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
12
12
|
return {
|
|
13
13
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
14
14
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
15
|
+
...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
|
|
15
16
|
...(birthtimeMs === undefined ? {} : { birthtimeMs }),
|
|
16
17
|
...(identityScope === undefined ? {} : { identityScope }),
|
|
17
18
|
...(ino === undefined ? {} : { ino }),
|
|
@@ -119,6 +120,7 @@ export class OverlayFileSystem {
|
|
|
119
120
|
...(effectiveAppend === undefined ? {} : { append: effectiveAppend }),
|
|
120
121
|
atomicRename: false, atomicRenameNoReplace: false,
|
|
121
122
|
descriptorWriteStream: false,
|
|
123
|
+
retainedResize: false,
|
|
122
124
|
hardlinks: false,
|
|
123
125
|
symlinks: writable && this.#upper.capabilities.symlinks === true
|
|
124
126
|
&& typeof this.#upper.symlink === "function" && typeof this.#upper.readlink === "function"
|
|
@@ -167,8 +169,12 @@ export class OverlayFileSystem {
|
|
|
167
169
|
openReadFile(path, options = {}) {
|
|
168
170
|
return this.run(options, async () => {
|
|
169
171
|
const entry = await this.required(path, options);
|
|
170
|
-
if (entry.stat.type !== "file")
|
|
171
|
-
|
|
172
|
+
if (entry.stat.type !== "file") {
|
|
173
|
+
const allowDirectory = options.allowDirectory === true;
|
|
174
|
+
options.signal?.throwIfAborted();
|
|
175
|
+
if (entry.stat.type !== "directory" || !allowDirectory)
|
|
176
|
+
fail("EISDIR", path);
|
|
177
|
+
}
|
|
172
178
|
this.permission(entry, 4);
|
|
173
179
|
return openRetainedReadFile(entry.backend, entry.path, options);
|
|
174
180
|
}, false);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
|
|
2
|
+
export declare const pathNamespace: unique symbol;
|
|
3
|
+
export declare function capturePathNamespace(filesystem: FileSystem, options: FsOptions): ((path: string) => string) | undefined;
|
|
4
|
+
export declare function readOnlyPathNamespace(projection: unknown): object | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { FsError } from "../contracts/errors.js";
|
|
2
|
+
import { normalizePath, validatePath } from "../contracts/virtual-path.js";
|
|
3
|
+
export const pathNamespace = Symbol.for("@poe-code/safe-fs/path-namespace/v1");
|
|
4
|
+
const captureNamespace = Symbol.for("@poe-code/safe-fs/capture-path-namespace/v1");
|
|
5
|
+
export function capturePathNamespace(filesystem, options) {
|
|
6
|
+
options.signal?.throwIfAborted();
|
|
7
|
+
const projection = Reflect.get(filesystem, pathNamespace);
|
|
8
|
+
options.signal?.throwIfAborted();
|
|
9
|
+
if (projection === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
return captureProjection(projection, options);
|
|
12
|
+
}
|
|
13
|
+
export function readOnlyPathNamespace(projection) {
|
|
14
|
+
if (projection === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const select = (path) => captureProjection(projection, {})(path);
|
|
17
|
+
Object.defineProperty(select, captureNamespace, {
|
|
18
|
+
value: Object.freeze((options) => captureProjection(projection, options)),
|
|
19
|
+
});
|
|
20
|
+
return Object.freeze({ select: Object.freeze(select) });
|
|
21
|
+
}
|
|
22
|
+
function captureProjection(projection, options) {
|
|
23
|
+
options.signal?.throwIfAborted();
|
|
24
|
+
if (projection === null || typeof projection !== "object")
|
|
25
|
+
throw new FsError("EIO");
|
|
26
|
+
const frozen = Object.isFrozen(projection);
|
|
27
|
+
options.signal?.throwIfAborted();
|
|
28
|
+
if (!frozen)
|
|
29
|
+
throw new FsError("EIO");
|
|
30
|
+
let select = Reflect.get(projection, "select");
|
|
31
|
+
options.signal?.throwIfAborted();
|
|
32
|
+
if (typeof select !== "function")
|
|
33
|
+
throw new FsError("EIO");
|
|
34
|
+
const capture = Reflect.get(select, captureNamespace);
|
|
35
|
+
options.signal?.throwIfAborted();
|
|
36
|
+
let receiver = projection;
|
|
37
|
+
if (capture !== undefined) {
|
|
38
|
+
if (typeof capture !== "function")
|
|
39
|
+
throw new FsError("EIO");
|
|
40
|
+
select = Reflect.apply(capture, select, [options]);
|
|
41
|
+
options.signal?.throwIfAborted();
|
|
42
|
+
if (typeof select !== "function")
|
|
43
|
+
throw new FsError("EIO");
|
|
44
|
+
receiver = undefined;
|
|
45
|
+
}
|
|
46
|
+
const selected = select;
|
|
47
|
+
return path => {
|
|
48
|
+
options.signal?.throwIfAborted();
|
|
49
|
+
const root = Reflect.apply(selected, receiver, [path]);
|
|
50
|
+
options.signal?.throwIfAborted();
|
|
51
|
+
if (typeof root !== "string" || !root.startsWith("/"))
|
|
52
|
+
throw new FsError("EIO", { path });
|
|
53
|
+
try {
|
|
54
|
+
validatePath(root);
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
throw new FsError("EIO", { path, cause });
|
|
58
|
+
}
|
|
59
|
+
if (normalizePath(root) !== root || root !== "/" && path !== root && !path.startsWith(`${root}/`))
|
|
60
|
+
throw new FsError("EIO", { path });
|
|
61
|
+
return root;
|
|
62
|
+
};
|
|
63
|
+
}
|