@poe-platform/safe-fs 0.1.729 → 0.1.730
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 +9 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +8 -0
- package/dist/safe-fs/core.d.ts +1 -1
- package/dist/safe-fs/core.js +1 -1
- package/dist/safe-fs/fs/capabilities.js +4 -2
- package/dist/safe-fs/fs/memory/index.js +161 -49
- package/dist/safe-fs/fs/memory/ledger.d.ts +3 -2
- package/dist/safe-fs/fs/memory/ledger.js +6 -1
- package/dist/safe-fs/fs/mount/index.js +1 -1
- package/dist/safe-fs/fs/real/index.js +28 -9
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +34 -3
- package/dist/safe-fs/node/native-seek-unavailable.d.ts +1 -0
- package/dist/safe-fs/node/native-seek-unavailable.js +5 -0
- package/dist/safe-fs/platform/browser.d.ts +8 -0
- package/dist/safe-fs/platform/browser.js +1 -0
- package/dist/safe-fs/platform/node.d.ts +8 -0
- package/dist/safe-fs/platform/node.js +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -25,6 +25,14 @@ console.log(await fs.readFile("note.txt", "utf8"));
|
|
|
25
25
|
|
|
26
26
|
Output: `hello`, then `hello world`. Nothing touches the host filesystem. Raw adapters exchange `Uint8Array` values; the Node bridge adds strings, encodings, `Buffer` results, and stat predicates. Its `cwd` is the relative-path base and, by default, the confinement boundary. Set an explicit `root` to use a different boundary, for example `{ cwd: "/work", root: "/" }` to address a complete provider namespace. The bridge supports `unlink` when the provider offers atomic file removal; unsupported methods fail without host fallback. `createHostFileSystem()` provides trusted, unrestricted native host access when a rooted adapter is not desired.
|
|
27
27
|
|
|
28
|
+
Under the `workerd` export condition, `@poe-platform/safe-fs/fs/real` reads the
|
|
29
|
+
native request-local filesystem, including retained `/tmp` file bytes.
|
|
30
|
+
The restricted profile refuses generic descriptors, permission/timestamp
|
|
31
|
+
changes, exact creation modes, and owned staging; it promises neither atomic
|
|
32
|
+
rename nor inode/version identity from placeholder native metadata. Retained
|
|
33
|
+
reads remain available, with native seek reporting `ENOTSUP`. Ordinary browser
|
|
34
|
+
bundles cannot import this backend. Node keeps its POSIX host profile.
|
|
35
|
+
|
|
28
36
|
For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Unzip extraction requires atomic ancestry verification, currently supported by MemoryFileSystem; the real adapter and mount views refuse it. Its `trustedOwnedStaging` capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.
|
|
29
37
|
|
|
30
38
|
To create new outputs with Safe Bash's `dos2unix`, `unix2dos`, or compression commands, host adapters need atomic no-replace publication. Supply `createRealFileSystem({ root, renameNoReplace })` only when that callback binds a qualified native primitive such as Linux `renameat2` with `RENAME_NOREPLACE`. It receives resolved absolute host paths and an optional signal in its third argument. Existing destinations must reject `EEXIST` atomically; existence checks followed by rename and copy/delete are insufficient. Without this binding the adapter explicitly refuses those operations. See the [no-replace contract](src/contracts/filesystem.md#atomic-no-replace-rename).
|
|
@@ -108,7 +116,7 @@ See the [staging contract](src/contracts/filesystem.md#atomic-owned-staging).
|
|
|
108
116
|
| Backend or wrapper | Use it for |
|
|
109
117
|
| --- | --- |
|
|
110
118
|
| `createMemoryFileSystem()` | Isolated, nonpersistent storage with links, permissions, timestamps, and streams; each path resolution admits at most 65,536 cumulative UTF-16 code units across the input and followed symlink targets, rejecting excess with `ENAMETOOLONG` before component allocation |
|
|
111
|
-
| `createRealFileSystem({ root })` | An existing host directory, with virtual paths rooted inside it; Node
|
|
119
|
+
| `createRealFileSystem({ root })` | An existing host directory, with virtual paths rooted inside it; Node or qualified Workerd operations |
|
|
112
120
|
| `new S3FileSystem({ transport, bucket, … })` | Bucket/prefix storage through an explicitly supplied transport; Node only |
|
|
113
121
|
| `new WebDavFileSystem({ baseUrl, fetch, … })` | A WebDAV namespace through an explicitly supplied Fetch implementation |
|
|
114
122
|
| `createReadOnlyFileSystem(filesystem)` | Rejecting writes through one view of an existing filesystem |
|
|
@@ -78,6 +78,7 @@ export interface FileSystemCapabilities {
|
|
|
78
78
|
readonly atomicFileStaging?: boolean;
|
|
79
79
|
/** Creates a cleanup handle bound to owned staging entries, independent of ancestor paths. */
|
|
80
80
|
readonly retainedStagingCleanup?: boolean;
|
|
81
|
+
readonly retainedStagingWrite?: boolean;
|
|
81
82
|
/** Atomically verifies every supplied root-to-parent directory identity at publication. */
|
|
82
83
|
readonly atomicStagingAncestry?: boolean;
|
|
83
84
|
/** Prepares directory guards that validate without yielding or mutating state. */
|
|
@@ -249,6 +250,13 @@ export interface FileStaging {
|
|
|
249
250
|
readonly directory: FileStagingEntry;
|
|
250
251
|
readonly file: FileStagingEntry;
|
|
251
252
|
readonly cleanup?: FileStagingCleanup;
|
|
253
|
+
/** Available for retained regular-file staging on qualified backends. Writes
|
|
254
|
+
* append to the owned entry; finish seals it and returns its publication stat.
|
|
255
|
+
* Cleanup owns the lifetime and checks the last successfully written revision. */
|
|
256
|
+
readonly writer?: {
|
|
257
|
+
write(bytes: Uint8Array, options?: FsOptions): Promise<void>;
|
|
258
|
+
finish(options?: FsOptions): Promise<FileStat>;
|
|
259
|
+
};
|
|
252
260
|
}
|
|
253
261
|
export type StagedFileContent = {
|
|
254
262
|
readonly type: "file";
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export * from "./fs/mount/index.js";
|
|
|
12
12
|
export * from "./fs/overlay/index.js";
|
|
13
13
|
export * from "./fs/quota/index.js";
|
|
14
14
|
export * from "./fs/object-publication/index.js";
|
|
15
|
-
export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
15
|
+
export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
16
16
|
export type { RetainedFileSystemCleanupView, RetainedFileSystemCleanupOptions } from "./fs/scoped.js";
|
|
17
17
|
export * from "./fs/webdav/index.js";
|
|
18
18
|
export * from "./bridge/index.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -11,7 +11,7 @@ export * from "./fs/mount/index.js";
|
|
|
11
11
|
export * from "./fs/overlay/index.js";
|
|
12
12
|
export * from "./fs/quota/index.js";
|
|
13
13
|
export * from "./fs/object-publication/index.js";
|
|
14
|
-
export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
14
|
+
export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
15
15
|
export * from "./fs/webdav/index.js";
|
|
16
16
|
export * from "./bridge/index.js";
|
|
17
17
|
export * from "./python/index.js";
|
|
@@ -105,7 +105,7 @@ export function readOnlyCapabilities(capabilities) {
|
|
|
105
105
|
"symlinks", "streamingRead", "open", "retainedRead", "versionedDescriptors",
|
|
106
106
|
].filter(name => capabilities[name] !== undefined).map(name => [name, capabilities[name]]));
|
|
107
107
|
return Object.freeze({
|
|
108
|
-
...inspection, retainedStagingCleanup: false, readOnly: true, write: false, append: false, exclusiveCreate: false,
|
|
108
|
+
...inspection, retainedStagingCleanup: false, retainedStagingWrite: false, readOnly: true, write: false, append: false, exclusiveCreate: false,
|
|
109
109
|
synchronousStagingResolution: false,
|
|
110
110
|
mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
|
|
111
111
|
rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
|
|
@@ -118,7 +118,7 @@ export function quotaCapabilities(capabilities) {
|
|
|
118
118
|
const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
|
|
119
119
|
const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
|
|
120
120
|
const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
|
|
121
|
-
return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
121
|
+
return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
122
122
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
123
123
|
...(streamingAppend === undefined ? {} : { streamingAppend }),
|
|
124
124
|
});
|
|
@@ -143,6 +143,8 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
|
|
|
143
143
|
(unavailable ??= {}).atomicFileStaging = false;
|
|
144
144
|
if (capabilities.retainedStagingCleanup === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
|
|
145
145
|
(unavailable ??= {}).retainedStagingCleanup = false;
|
|
146
|
+
if (capabilities.retainedStagingWrite === true && (capabilities.retainedStagingCleanup !== true || unavailable?.retainedStagingCleanup === false))
|
|
147
|
+
(unavailable ??= {}).retainedStagingWrite = false;
|
|
146
148
|
if (capabilities.atomicStagingAncestry === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
|
|
147
149
|
(unavailable ??= {}).atomicStagingAncestry = false;
|
|
148
150
|
if (capabilities.synchronousDirectoryValidation === true && typeof filesystem.prepareDirectoryAncestry !== "function")
|
|
@@ -44,6 +44,39 @@ const EMPTY_ALLOC_BYTES = new Uint8Array(0);
|
|
|
44
44
|
const SMALL_ALLOC_SLAB_SIZE = 8192;
|
|
45
45
|
let smallAllocSlab = new Uint8Array(SMALL_ALLOC_SLAB_SIZE);
|
|
46
46
|
let smallAllocOffset = 0;
|
|
47
|
+
const DUMMY_POOL_LEDGER = new MemoryLedger(normalizeMemoryFileSystemLimits({}));
|
|
48
|
+
const DUMMY_POOL_ALLOCATION = new MemoryAllocation(EMPTY_ALLOC_BYTES, DUMMY_POOL_LEDGER);
|
|
49
|
+
const sharedAllocationPool = [];
|
|
50
|
+
const sharedFileNodePool = [];
|
|
51
|
+
const sharedDirectoryNodePool = [];
|
|
52
|
+
const sharedRemovedScratch = [];
|
|
53
|
+
let sharedRemovedKeyChars = 0;
|
|
54
|
+
let sharedRemovedKeyEntries = 0;
|
|
55
|
+
function collectRemovedChild(child, name) {
|
|
56
|
+
sharedRemovedScratch.push(child);
|
|
57
|
+
sharedRemovedKeyChars += name.length;
|
|
58
|
+
sharedRemovedKeyEntries++;
|
|
59
|
+
}
|
|
60
|
+
function replenishSharedMemoryPools() {
|
|
61
|
+
while (sharedAllocationPool.length < 112) {
|
|
62
|
+
if (smallAllocOffset + 64 > SMALL_ALLOC_SLAB_SIZE) {
|
|
63
|
+
smallAllocSlab = new Uint8Array(SMALL_ALLOC_SLAB_SIZE);
|
|
64
|
+
smallAllocOffset = 0;
|
|
65
|
+
}
|
|
66
|
+
const slice = smallAllocSlab.subarray(smallAllocOffset, smallAllocOffset + 64);
|
|
67
|
+
smallAllocOffset += 64;
|
|
68
|
+
DUMMY_POOL_LEDGER.reserve(64, 0, "init", "/");
|
|
69
|
+
const alloc = new MemoryAllocation(slice, DUMMY_POOL_LEDGER);
|
|
70
|
+
alloc.release();
|
|
71
|
+
sharedAllocationPool.push(alloc);
|
|
72
|
+
}
|
|
73
|
+
while (sharedFileNodePool.length < 112) {
|
|
74
|
+
sharedFileNodePool.push(new MemoryFileNode(0, 0, 0, 0, DUMMY_POOL_ALLOCATION, undefined));
|
|
75
|
+
}
|
|
76
|
+
while (sharedDirectoryNodePool.length < 16) {
|
|
77
|
+
sharedDirectoryNodePool.push(new MemoryDirectoryNode(0, 0, 0));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
47
80
|
class MemoryDirectoryNode {
|
|
48
81
|
constructor(mode, ino, now) {
|
|
49
82
|
this.mode = mode;
|
|
@@ -271,7 +304,7 @@ export class MemoryFileSystem {
|
|
|
271
304
|
permissions: true,
|
|
272
305
|
timestamps: true,
|
|
273
306
|
atomicRename: true,
|
|
274
|
-
atomicFileStaging: true, retainedStagingCleanup: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
|
|
307
|
+
atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
|
|
275
308
|
synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
|
|
276
309
|
atomicDirectoryMetadata: true,
|
|
277
310
|
streamingRead: true,
|
|
@@ -289,6 +322,9 @@ export class MemoryFileSystem {
|
|
|
289
322
|
symlinkCount = 0;
|
|
290
323
|
constructor(options = {}) {
|
|
291
324
|
this.ledger = new MemoryLedger(normalizeMemoryFileSystemLimits(options));
|
|
325
|
+
if (this.ledger.hasInfiniteRetained && this.ledger.limits.maxFileBytes === Infinity) {
|
|
326
|
+
replenishSharedMemoryPools();
|
|
327
|
+
}
|
|
292
328
|
memoryCaches.set(this.ledger, new MemoryCache());
|
|
293
329
|
this.ledger.reserve(0, 1, "mkdir", "/");
|
|
294
330
|
this.root = this.directory(0o755);
|
|
@@ -380,7 +416,7 @@ export class MemoryFileSystem {
|
|
|
380
416
|
retained.set("/", this.root);
|
|
381
417
|
}
|
|
382
418
|
const capabilities = this.capabilities;
|
|
383
|
-
const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream", "writeFileConditional", "removeFileConditional", "prepareDirectory", "createStagedFile", "publishStagedFile", "removeStagedFile"]);
|
|
419
|
+
const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream", "writeFileConditional", "removeFileConditional", "removeEntryConditional", "prepareDirectory", "createStagedFile", "publishStagedFile", "removeStagedFile"]);
|
|
384
420
|
const reads = new Set(["access", "capabilitiesFor", "compareEntry", "lstat", "stat", "readFile", "readStream", "readdir", "readlink", "realpath"]);
|
|
385
421
|
const check = (path, followFinal) => {
|
|
386
422
|
if (!roots.some(root => root === "/" || path === root || path.startsWith(`${root}/`)))
|
|
@@ -430,6 +466,24 @@ export class MemoryFileSystem {
|
|
|
430
466
|
this.fail("EPERM", "writeStream", path);
|
|
431
467
|
} };
|
|
432
468
|
}
|
|
469
|
+
if (property === "createStagedFile")
|
|
470
|
+
return Reflect.apply(value, target, args).then(staging => {
|
|
471
|
+
const writer = staging.writer;
|
|
472
|
+
if (!writer)
|
|
473
|
+
return staging;
|
|
474
|
+
return Object.freeze({ ...staging, writer: Object.freeze({
|
|
475
|
+
write: async (bytes, controls = {}) => {
|
|
476
|
+
controls.signal?.throwIfAborted();
|
|
477
|
+
check(staging.file.path, true);
|
|
478
|
+
await writer.write(bytes, controls);
|
|
479
|
+
},
|
|
480
|
+
finish: async (controls = {}) => {
|
|
481
|
+
controls.signal?.throwIfAborted();
|
|
482
|
+
check(staging.file.path, true);
|
|
483
|
+
return writer.finish(controls);
|
|
484
|
+
},
|
|
485
|
+
}) });
|
|
486
|
+
});
|
|
433
487
|
return Reflect.apply(value, target, args);
|
|
434
488
|
};
|
|
435
489
|
},
|
|
@@ -446,7 +500,7 @@ export class MemoryFileSystem {
|
|
|
446
500
|
const now = Date.now();
|
|
447
501
|
const fullMode = typeModes.directory | mode;
|
|
448
502
|
const ino = this.nextInode++;
|
|
449
|
-
const pooled = memoryCaches.get(this.ledger)?.directories.pop();
|
|
503
|
+
const pooled = sharedDirectoryNodePool.pop() ?? memoryCaches.get(this.ledger)?.directories.pop();
|
|
450
504
|
if (pooled) {
|
|
451
505
|
pooled.mode = fullMode;
|
|
452
506
|
pooled.ino = ino;
|
|
@@ -511,11 +565,21 @@ export class MemoryFileSystem {
|
|
|
511
565
|
this.totalBytes -= node.byteLength;
|
|
512
566
|
const alloc = node.allocation;
|
|
513
567
|
node.view = undefined;
|
|
568
|
+
node.allocation = DUMMY_POOL_ALLOCATION;
|
|
514
569
|
alloc.release();
|
|
515
|
-
if (alloc.isReleased64()
|
|
516
|
-
|
|
570
|
+
if (alloc.isReleased64()) {
|
|
571
|
+
alloc.detachLedger(DUMMY_POOL_LEDGER);
|
|
572
|
+
if (this.ledger.hasInfiniteRetained && this.ledger.limits.maxFileBytes === Infinity && sharedAllocationPool.length < 128) {
|
|
573
|
+
sharedAllocationPool.push(alloc);
|
|
574
|
+
}
|
|
575
|
+
else if (cache.allocations.length < 128) {
|
|
576
|
+
cache.allocations.push(alloc);
|
|
577
|
+
}
|
|
517
578
|
}
|
|
518
|
-
if (
|
|
579
|
+
if (sharedFileNodePool.length < 128) {
|
|
580
|
+
sharedFileNodePool.push(node);
|
|
581
|
+
}
|
|
582
|
+
else if (cache.files.length < 128) {
|
|
519
583
|
cache.files.push(node);
|
|
520
584
|
}
|
|
521
585
|
}
|
|
@@ -527,8 +591,13 @@ export class MemoryFileSystem {
|
|
|
527
591
|
cache.lastFastDirPrefix = "";
|
|
528
592
|
cache.lastFastDirNode = undefined;
|
|
529
593
|
}
|
|
530
|
-
if (node.entries.size === 0
|
|
531
|
-
|
|
594
|
+
if (node.entries.size === 0) {
|
|
595
|
+
if (sharedDirectoryNodePool.length < 32) {
|
|
596
|
+
sharedDirectoryNodePool.push(node);
|
|
597
|
+
}
|
|
598
|
+
else if (cache.directories.length < 32) {
|
|
599
|
+
cache.directories.push(node);
|
|
600
|
+
}
|
|
532
601
|
}
|
|
533
602
|
}
|
|
534
603
|
}
|
|
@@ -900,11 +969,14 @@ export class MemoryFileSystem {
|
|
|
900
969
|
if (length === 0) {
|
|
901
970
|
return new MemoryAllocation(EMPTY_ALLOC_BYTES, this.ledger);
|
|
902
971
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
972
|
+
if (length === 64) {
|
|
973
|
+
const pooled = (this.ledger.hasInfiniteRetained && this.ledger.limits.maxFileBytes === Infinity
|
|
974
|
+
? sharedAllocationPool.pop()
|
|
975
|
+
: undefined) ?? memoryCaches.get(this.ledger).allocations.pop();
|
|
976
|
+
if (pooled) {
|
|
977
|
+
pooled.reuse(this.ledger);
|
|
978
|
+
return pooled;
|
|
979
|
+
}
|
|
908
980
|
}
|
|
909
981
|
try {
|
|
910
982
|
// A shared slab's backing buffer exceeds the admitted slice capacity.
|
|
@@ -1020,7 +1092,7 @@ export class MemoryFileSystem {
|
|
|
1020
1092
|
const now = Date.now();
|
|
1021
1093
|
const fileMode = typeModes.file | target.mode;
|
|
1022
1094
|
const view = capacity === length ? allocation.data : undefined;
|
|
1023
|
-
const pooled = memoryCaches.get(this.ledger).files.pop();
|
|
1095
|
+
const pooled = sharedFileNodePool.pop() ?? memoryCaches.get(this.ledger).files.pop();
|
|
1024
1096
|
if (pooled) {
|
|
1025
1097
|
pooled.mode = fileMode;
|
|
1026
1098
|
pooled.ino = this.nextInode++;
|
|
@@ -1183,7 +1255,7 @@ export class MemoryFileSystem {
|
|
|
1183
1255
|
return;
|
|
1184
1256
|
}
|
|
1185
1257
|
if (!current) {
|
|
1186
|
-
const capacity = length;
|
|
1258
|
+
const capacity = length > 0 && length < 64 && this.ledger.hasInfiniteRetained && this.ledger.limits.maxFileBytes === Infinity ? 64 : length;
|
|
1187
1259
|
const allocation = this.allocate(capacity, syscall, path);
|
|
1188
1260
|
try {
|
|
1189
1261
|
allocation.data.set(data);
|
|
@@ -1191,8 +1263,8 @@ export class MemoryFileSystem {
|
|
|
1191
1263
|
try {
|
|
1192
1264
|
const now = Date.now();
|
|
1193
1265
|
const fileMode = typeModes.file | mode;
|
|
1194
|
-
const view = allocation.data;
|
|
1195
|
-
let node = cache.files.pop();
|
|
1266
|
+
const view = capacity === length ? allocation.data : undefined;
|
|
1267
|
+
let node = sharedFileNodePool.pop() ?? cache.files.pop();
|
|
1196
1268
|
if (node) {
|
|
1197
1269
|
node.mode = fileMode;
|
|
1198
1270
|
node.ino = this.nextInode++;
|
|
@@ -1326,7 +1398,7 @@ export class MemoryFileSystem {
|
|
|
1326
1398
|
return;
|
|
1327
1399
|
}
|
|
1328
1400
|
if (!current) {
|
|
1329
|
-
const capacity = length;
|
|
1401
|
+
const capacity = length > 0 && length < 64 && this.ledger.hasInfiniteRetained && this.ledger.limits.maxFileBytes === Infinity ? 64 : length;
|
|
1330
1402
|
const allocation = this.allocate(capacity, syscall, name);
|
|
1331
1403
|
try {
|
|
1332
1404
|
allocation.data.set(data);
|
|
@@ -1334,8 +1406,8 @@ export class MemoryFileSystem {
|
|
|
1334
1406
|
try {
|
|
1335
1407
|
const now = Date.now();
|
|
1336
1408
|
const fileMode = typeModes.file | mode;
|
|
1337
|
-
const view = allocation.data;
|
|
1338
|
-
let node = cache.files.pop();
|
|
1409
|
+
const view = capacity === length ? allocation.data : undefined;
|
|
1410
|
+
let node = sharedFileNodePool.pop() ?? cache.files.pop();
|
|
1339
1411
|
if (node) {
|
|
1340
1412
|
node.mode = fileMode;
|
|
1341
1413
|
node.ino = this.nextInode++;
|
|
@@ -1517,13 +1589,16 @@ export class MemoryFileSystem {
|
|
|
1517
1589
|
location.parent.references++;
|
|
1518
1590
|
directory.references++;
|
|
1519
1591
|
file.references++;
|
|
1520
|
-
return Object.freeze({ ...staging,
|
|
1592
|
+
return Object.freeze({ ...staging, ...this.retainStagingCleanup(staging, location.parent, directory, file, location.name, name) });
|
|
1521
1593
|
}
|
|
1522
1594
|
retainStagingCleanup(staging, parent, directory, file, directoryName, fileName) {
|
|
1523
1595
|
// Clear this state on release, so keeping the closed handle does not keep
|
|
1524
1596
|
// detached nodes alive outside the Memory ledger.
|
|
1525
1597
|
let retained = { parent, directory, file };
|
|
1526
|
-
|
|
1598
|
+
let expected = staging.file.stat;
|
|
1599
|
+
let writing = true;
|
|
1600
|
+
const receipt = () => ({ ...staging, file: { ...staging.file, stat: expected } });
|
|
1601
|
+
const ownedCleanup = createStagingCleanup(staging.directory.path, options => {
|
|
1527
1602
|
options.signal?.throwIfAborted();
|
|
1528
1603
|
const state = retained;
|
|
1529
1604
|
if (state.parent.nlink === 0 || state.parent.entries.get(directoryName) !== state.directory)
|
|
@@ -1535,7 +1610,7 @@ export class MemoryFileSystem {
|
|
|
1535
1610
|
const child = state.directory.entries.get(fileName);
|
|
1536
1611
|
if (child && child !== state.file)
|
|
1537
1612
|
this.fail("EAGAIN", "removeStagedFile", staging.file.path);
|
|
1538
|
-
this.removeStagingLocations(
|
|
1613
|
+
this.removeStagingLocations(receipt(), {
|
|
1539
1614
|
node: state.directory, parent: state.parent, name: directoryName, path: staging.directory.path,
|
|
1540
1615
|
}, { node: child, parent: state.directory, name: fileName, path: staging.file.path });
|
|
1541
1616
|
}, () => {
|
|
@@ -1545,6 +1620,43 @@ export class MemoryFileSystem {
|
|
|
1545
1620
|
this.releaseReference(state.directory, staging.directory.path);
|
|
1546
1621
|
this.releaseReference(state.parent, staging.parent.path);
|
|
1547
1622
|
});
|
|
1623
|
+
const cleanup = Object.freeze({
|
|
1624
|
+
remove: (options) => { writing = false; return ownedCleanup.remove(options); },
|
|
1625
|
+
close: () => { writing = false; return ownedCleanup.close(); },
|
|
1626
|
+
});
|
|
1627
|
+
if (file.type !== "file")
|
|
1628
|
+
return { cleanup };
|
|
1629
|
+
const inspect = (options) => {
|
|
1630
|
+
options.signal?.throwIfAborted();
|
|
1631
|
+
if (!retained || !writing)
|
|
1632
|
+
this.fail("EBADF", "stagedWrite", staging.file.path);
|
|
1633
|
+
const location = this.stagingLocations(receipt());
|
|
1634
|
+
if (location.file.node !== retained.file)
|
|
1635
|
+
this.fail("EAGAIN", "stagedWrite", staging.file.path);
|
|
1636
|
+
this.expectEntry(retained.file, expected, staging.file.path);
|
|
1637
|
+
this.permission(retained.parent, 3, "stagedWrite", staging.parent.path);
|
|
1638
|
+
this.permission(retained.directory, 3, "stagedWrite", staging.directory.path);
|
|
1639
|
+
return retained.file;
|
|
1640
|
+
};
|
|
1641
|
+
return { cleanup, writer: Object.freeze({
|
|
1642
|
+
write: async (bytes, options = {}) => {
|
|
1643
|
+
const node = inspect(options);
|
|
1644
|
+
if (!(bytes instanceof Uint8Array))
|
|
1645
|
+
throw new TypeError("Staged writes require Uint8Array data");
|
|
1646
|
+
this.writeAt(node, bytes, node.byteLength, "stagedWrite", staging.file.path);
|
|
1647
|
+
expected = Object.freeze(this.snapshot(node));
|
|
1648
|
+
},
|
|
1649
|
+
finish: async (options = {}) => {
|
|
1650
|
+
const node = inspect(options);
|
|
1651
|
+
this.changed(node);
|
|
1652
|
+
// changed updates modification time; archive creation metadata is final.
|
|
1653
|
+
node.atimeMs = staging.file.stat.atimeMs;
|
|
1654
|
+
node.mtimeMs = staging.file.stat.mtimeMs;
|
|
1655
|
+
writing = false;
|
|
1656
|
+
expected = Object.freeze(this.snapshot(node));
|
|
1657
|
+
return expected;
|
|
1658
|
+
},
|
|
1659
|
+
}) };
|
|
1548
1660
|
}
|
|
1549
1661
|
async prepareDirectoryAncestry(ancestors, options = {}) {
|
|
1550
1662
|
const entries = snapshotDirectoryAncestry(ancestors);
|
|
@@ -1890,38 +2002,38 @@ export class MemoryFileSystem {
|
|
|
1890
2002
|
this.fail("EISDIR", syscall, path);
|
|
1891
2003
|
const cache = memoryCaches.get(this.ledger);
|
|
1892
2004
|
cache.clearWrites();
|
|
1893
|
-
const removed =
|
|
2005
|
+
const removed = sharedRemovedScratch;
|
|
1894
2006
|
removed.length = 0;
|
|
1895
2007
|
removed.push(node);
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
keyChars += name.length;
|
|
1905
|
-
keyEntries++;
|
|
2008
|
+
sharedRemovedKeyChars = 0;
|
|
2009
|
+
sharedRemovedKeyEntries = 0;
|
|
2010
|
+
try {
|
|
2011
|
+
for (let index = 0; index < removed.length; index++) {
|
|
2012
|
+
const entry = removed[index];
|
|
2013
|
+
if (entry.type === "directory" && entry.entries.size > 0) {
|
|
2014
|
+
this.permission(entry, 7, syscall, path);
|
|
2015
|
+
entry.entries.forEach(collectRemovedChild);
|
|
1906
2016
|
}
|
|
1907
2017
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2018
|
+
location.parent.entries.delete(location.name);
|
|
2019
|
+
this.ledger.release((location.name.length + sharedRemovedKeyChars) * 2, 1 + sharedRemovedKeyEntries);
|
|
2020
|
+
const now = Date.now();
|
|
2021
|
+
for (let i = 0; i < removed.length; i++) {
|
|
2022
|
+
const entry = removed[i];
|
|
2023
|
+
if (entry.type === "directory") {
|
|
2024
|
+
entry.entries.clear();
|
|
2025
|
+
}
|
|
2026
|
+
entry.nlink--;
|
|
2027
|
+
if (entry.nlink !== 0 || entry.references !== 0) {
|
|
2028
|
+
entry.ctimeMs = now;
|
|
2029
|
+
entry.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, entry.revision + 1);
|
|
2030
|
+
}
|
|
2031
|
+
this.releaseNode(entry);
|
|
1921
2032
|
}
|
|
1922
|
-
this.releaseNode(entry);
|
|
1923
2033
|
}
|
|
1924
|
-
|
|
2034
|
+
finally {
|
|
2035
|
+
removed.length = 0;
|
|
2036
|
+
}
|
|
1925
2037
|
this.changed(location.parent);
|
|
1926
2038
|
}
|
|
1927
2039
|
unlink(path, options = {}) {
|
|
@@ -18,10 +18,11 @@ export declare class MemoryLedger {
|
|
|
18
18
|
export declare class MemoryAllocation {
|
|
19
19
|
private references;
|
|
20
20
|
readonly data: Uint8Array;
|
|
21
|
-
private
|
|
21
|
+
private ledger;
|
|
22
22
|
constructor(data: Uint8Array, ledger: MemoryLedger);
|
|
23
23
|
retain(): void;
|
|
24
24
|
release(): void;
|
|
25
25
|
isReleased64(): boolean;
|
|
26
|
-
|
|
26
|
+
detachLedger(dummyLedger: MemoryLedger): void;
|
|
27
|
+
reuse(ledger?: MemoryLedger): void;
|
|
27
28
|
}
|
|
@@ -83,8 +83,13 @@ export class MemoryAllocation {
|
|
|
83
83
|
isReleased64() {
|
|
84
84
|
return this.references === 0 && this.data.byteLength === 64;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
detachLedger(dummyLedger) {
|
|
87
|
+
this.ledger = dummyLedger;
|
|
88
|
+
}
|
|
89
|
+
reuse(ledger) {
|
|
87
90
|
this.references = 1;
|
|
91
|
+
if (ledger !== undefined)
|
|
92
|
+
this.ledger = ledger;
|
|
88
93
|
this.data.fill(0);
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -188,7 +188,7 @@ export class MountFileSystem {
|
|
|
188
188
|
&& await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
|
|
189
189
|
: this.capabilities.conditionalChmod;
|
|
190
190
|
const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, ...ordinary } = resize;
|
|
191
|
-
const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
|
|
191
|
+
const withOpen = { ...ordinary, retainedStagingWrite: false, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
|
|
192
192
|
const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
|
|
193
193
|
: retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
|
|
194
194
|
if (location.synthetic)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { platform } from "#safe-fs-platform";
|
|
1
2
|
import { snapshotStagingCreation } from "../staging-cleanup.js";
|
|
2
3
|
import { snapshotConditionalChmod } from "../conditional-chmod.js";
|
|
3
4
|
import { runStagingGuard, snapshotDirectoryAncestry } from "../staging-ancestry.js";
|
|
@@ -7,6 +8,7 @@ import * as native from "node:fs/promises";
|
|
|
7
8
|
import { dirname, basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
9
|
import { nativeAllocatedBytes } from "./allocation.js";
|
|
9
10
|
import { openFileDescriptor } from "../descriptor.js";
|
|
11
|
+
import { compareIdentity } from "../mount/identity.js";
|
|
10
12
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
11
13
|
import { callNativeSeekEnd, loadNativeSeekBinding } from "../../node/native-seek.js";
|
|
12
14
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
@@ -32,11 +34,11 @@ function fileStat(stats) {
|
|
|
32
34
|
mtimeMs: "mtimeNs" in stats ? Number(stats.mtimeNs / 1000000n) + Number(stats.mtimeNs % 1000000n) / 1e6 : stats.mtimeMs,
|
|
33
35
|
ctimeMs: "ctimeNs" in stats ? Number(stats.ctimeNs / 1000000n) + Number(stats.ctimeNs % 1000000n) / 1e6 : stats.ctimeMs,
|
|
34
36
|
birthtimeMs: "birthtimeNs" in stats ? Number(stats.birthtimeNs / 1000000n) + Number(stats.birthtimeNs % 1000000n) / 1e6 : stats.birthtimeMs, ino, dev,
|
|
35
|
-
...(Number.isSafeInteger(dev) && dev >= 0 && Number.isSafeInteger(ino) && ino
|
|
37
|
+
...(Number.isSafeInteger(dev) && dev >= 0 && Number.isSafeInteger(ino) && ino > 0
|
|
36
38
|
? { identityScope: Symbol.for("virtual-bash.fs.native") } : {}),
|
|
37
39
|
nlink: Number(stats.nlink), uid: Number(stats.uid), gid: Number(stats.gid),
|
|
38
40
|
};
|
|
39
|
-
if ("ctimeNs" in stats && !stats.isDirectory())
|
|
41
|
+
if (snapshot.identityScope !== undefined && "ctimeNs" in stats && (stats.ctimeNs !== 0n || stats.mtimeNs !== 0n) && !stats.isDirectory())
|
|
40
42
|
Object.defineProperty(snapshot, "opaqueVersion", { value: `${stats.ctimeNs}:${stats.mtimeNs}`, enumerable: true });
|
|
41
43
|
return snapshot;
|
|
42
44
|
}
|
|
@@ -98,14 +100,13 @@ function nativeError(error) {
|
|
|
98
100
|
*/
|
|
99
101
|
export class RealFileSystem {
|
|
100
102
|
capabilities = Object.freeze({
|
|
101
|
-
read: true, stat: true, readdir: true, realpath: true, access: true,
|
|
103
|
+
read: true, stat: true, readdir: true, realpath: true, access: true,
|
|
102
104
|
write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
|
|
103
105
|
mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
|
|
104
106
|
rename: true, atomicRenameNoReplace: false, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
|
|
105
107
|
streamingAppend: true, randomAccessWrite: true,
|
|
106
|
-
readOnly: false, symlinks: true, hardlinks: true,
|
|
107
|
-
|
|
108
|
-
timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true, retainedResize: true, trustedOwnedStaging: true,
|
|
108
|
+
readOnly: false, symlinks: true, hardlinks: true, streamingRead: true, streamingWrite: true, retainedRead: true, retainedResize: true,
|
|
109
|
+
...platform.nativeFileSystem,
|
|
109
110
|
});
|
|
110
111
|
configuredRoot;
|
|
111
112
|
renameNoReplace;
|
|
@@ -122,6 +123,8 @@ export class RealFileSystem {
|
|
|
122
123
|
this.configuredRoot = root;
|
|
123
124
|
this.renameNoReplace = typeof options === "string" ? undefined : options.renameNoReplace;
|
|
124
125
|
if (this.renameNoReplace !== undefined) {
|
|
126
|
+
if (!platform.nativeFileSystem.atomicRename)
|
|
127
|
+
throw new FsError("ENOTSUP", { syscall: "root", message: "atomic rename is unavailable on this host" });
|
|
125
128
|
if (typeof this.renameNoReplace !== "function")
|
|
126
129
|
throw new FsError("EINVAL", { syscall: "root", message: "renameNoReplace must be a function" });
|
|
127
130
|
this.capabilities = Object.freeze({ ...this.capabilities, atomicRenameNoReplace: true });
|
|
@@ -291,10 +294,15 @@ export class RealFileSystem {
|
|
|
291
294
|
}
|
|
292
295
|
}
|
|
293
296
|
expectStaging(path, expected, identityOnly = false) {
|
|
297
|
+
if (!platform.nativeFileSystem.trustedOwnedStaging)
|
|
298
|
+
throw new FsError("ENOTSUP");
|
|
294
299
|
if (expected && (expected.identityScope !== Symbol.for("virtual-bash.fs.native")
|
|
295
300
|
|| expected.dev === undefined || expected.ino === undefined || !identityOnly && expected.opaqueVersion === undefined))
|
|
296
301
|
throw new FsError("ENOTSUP");
|
|
297
302
|
const current = this.stagingSnapshot(path);
|
|
303
|
+
if (expected && current && (current.identityScope !== Symbol.for("virtual-bash.fs.native")
|
|
304
|
+
|| current.dev === undefined || current.ino === undefined || !identityOnly && current.opaqueVersion === undefined))
|
|
305
|
+
throw new FsError("ENOTSUP");
|
|
298
306
|
if (expected === null ? current !== null : !current || current.dev !== expected.dev || current.ino !== expected.ino || current.type !== expected.type
|
|
299
307
|
|| !identityOnly && (current.opaqueVersion !== expected.opaqueVersion || current.size !== expected.size || current.mode !== expected.mode || current.nlink !== expected.nlink))
|
|
300
308
|
throw new FsError("EAGAIN");
|
|
@@ -521,6 +529,8 @@ export class RealFileSystem {
|
|
|
521
529
|
}
|
|
522
530
|
}
|
|
523
531
|
open(path, options) {
|
|
532
|
+
if (!platform.nativeFileSystem.open)
|
|
533
|
+
return this.operation("open", path, options, async () => { throw new FsError("ENOTSUP"); });
|
|
524
534
|
return openFileDescriptor(path, options, {
|
|
525
535
|
noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
526
536
|
}, async (admitted) => {
|
|
@@ -663,7 +673,7 @@ export class RealFileSystem {
|
|
|
663
673
|
return this.operation("mkdir", path, options, async () => {
|
|
664
674
|
if (options.mode !== undefined)
|
|
665
675
|
integer(options.mode);
|
|
666
|
-
if (options.exactMode && options.recursive)
|
|
676
|
+
if (options.exactMode && (!platform.nativeFileSystem.permissions || options.recursive))
|
|
667
677
|
throw new FsError("ENOTSUP");
|
|
668
678
|
const target = await this.path(path, {
|
|
669
679
|
...options,
|
|
@@ -746,8 +756,13 @@ export class RealFileSystem {
|
|
|
746
756
|
}
|
|
747
757
|
if (target && options.exclusive)
|
|
748
758
|
throw new FsError("EEXIST");
|
|
749
|
-
if (target && origin.isFile()
|
|
750
|
-
|
|
759
|
+
if (target && origin.isFile()) {
|
|
760
|
+
const identity = compareIdentity(fileStat(origin), fileStat(target));
|
|
761
|
+
if (identity === "same")
|
|
762
|
+
throw new FsError("EINVAL");
|
|
763
|
+
if (identity === "unknown")
|
|
764
|
+
throw new FsError("ENOTSUP");
|
|
765
|
+
}
|
|
751
766
|
if (!target)
|
|
752
767
|
flags |= constants.COPYFILE_EXCL;
|
|
753
768
|
}
|
|
@@ -813,6 +828,8 @@ export class RealFileSystem {
|
|
|
813
828
|
}
|
|
814
829
|
async chmod(path, mode, options = {}) {
|
|
815
830
|
return this.operation("chmod", path, options, async () => {
|
|
831
|
+
if (!platform.nativeFileSystem.permissions)
|
|
832
|
+
throw new FsError("ENOTSUP");
|
|
816
833
|
integer(mode);
|
|
817
834
|
const conditional = snapshotConditionalChmod(path, options);
|
|
818
835
|
if (conditional) {
|
|
@@ -846,6 +863,8 @@ export class RealFileSystem {
|
|
|
846
863
|
}
|
|
847
864
|
async utimes(path, atimeMs, mtimeMs, options = {}) {
|
|
848
865
|
return this.operation("utimes", path, options, async () => {
|
|
866
|
+
if (!platform.nativeFileSystem.timestamps)
|
|
867
|
+
throw new FsError("ENOTSUP");
|
|
849
868
|
if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs)
|
|
850
869
|
|| Math.abs(atimeMs) > 8.64e15 || Math.abs(mtimeMs) > 8.64e15)
|
|
851
870
|
throw new FsError("EINVAL");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
|
|
2
|
+
export declare function retargetScopedFileSystem(scoped: FileSystem, charge: () => void, signal: AbortSignal, cleanupCharge?: () => void, maxPathComponents?: number): boolean;
|
|
2
3
|
export declare function scopeFileSystem(filesystem: FileSystem, charge: () => void, signal: AbortSignal, cleanupCharge?: () => void, options?: {
|
|
3
4
|
readonly preserveDescriptorWriteReceipt?: boolean;
|
|
4
5
|
readonly creationMask?: number;
|
|
@@ -8,6 +8,14 @@ import { openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabi
|
|
|
8
8
|
import { createStagingCleanup, snapshotStagingCreation } from "./staging-cleanup.js";
|
|
9
9
|
import { inspectStagingBindings, runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "./staging-ancestry.js";
|
|
10
10
|
const originals = new WeakMap();
|
|
11
|
+
const retargeters = new WeakMap();
|
|
12
|
+
export function retargetScopedFileSystem(scoped, charge, signal, cleanupCharge = charge, maxPathComponents) {
|
|
13
|
+
const fn = retargeters.get(scoped);
|
|
14
|
+
if (!fn)
|
|
15
|
+
return false;
|
|
16
|
+
fn(charge, signal, cleanupCharge, maxPathComponents);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
11
19
|
const operations = new Set([
|
|
12
20
|
"prepareDirectoryAncestry", "prepareStagingResolution",
|
|
13
21
|
"publishFileConditional", "removeEntryConditional", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
|
|
@@ -19,7 +27,7 @@ const operations = new Set([
|
|
|
19
27
|
export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = charge, options = {}) {
|
|
20
28
|
const original = originals.get(filesystem)?.filesystem ?? filesystem;
|
|
21
29
|
const creationMask = options.creationMask ?? originals.get(filesystem)?.creationMask;
|
|
22
|
-
|
|
30
|
+
let maxPathComponents = options.maxPathComponents ?? originals.get(filesystem)?.maxPathComponents;
|
|
23
31
|
if (creationMask !== undefined && (!Number.isInteger(creationMask) || creationMask < 0 || creationMask > 0o777))
|
|
24
32
|
throw new RangeError("creationMask must be a permission mask between 0 and 0777");
|
|
25
33
|
const creationOptions = (settings, directory = false) => {
|
|
@@ -343,7 +351,20 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
343
351
|
controls.signal?.throwIfAborted();
|
|
344
352
|
return backendCleanup.remove(controls);
|
|
345
353
|
}, () => backendCleanup.close());
|
|
346
|
-
|
|
354
|
+
const backendWriter = staging.writer;
|
|
355
|
+
const writer = backendWriter && Object.freeze({
|
|
356
|
+
write: async (bytes, settings = {}) => {
|
|
357
|
+
const controls = resizeOptions(settings);
|
|
358
|
+
admit(controls);
|
|
359
|
+
await backendWriter.write(bytes, controls);
|
|
360
|
+
},
|
|
361
|
+
finish: async (settings = {}) => {
|
|
362
|
+
const controls = resizeOptions(settings);
|
|
363
|
+
admit(controls);
|
|
364
|
+
return backendWriter.finish(controls);
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
return Object.freeze({ ...staging, cleanup, ...(writer ? { writer } : {}) });
|
|
347
368
|
}
|
|
348
369
|
catch (error) {
|
|
349
370
|
await finishCleanup(() => backendCleanup.close(), true);
|
|
@@ -471,7 +492,17 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
471
492
|
return scoped;
|
|
472
493
|
},
|
|
473
494
|
});
|
|
474
|
-
|
|
495
|
+
const originalRecord = { filesystem: original, signal, cleanupCharge, creationMask, maxPathComponents };
|
|
496
|
+
originals.set(view, originalRecord);
|
|
497
|
+
retargeters.set(view, (nextCharge, nextSignal, nextCleanupCharge, nextMaxPathComponents) => {
|
|
498
|
+
charge = nextCharge;
|
|
499
|
+
signal = nextSignal;
|
|
500
|
+
cleanupCharge = nextCleanupCharge;
|
|
501
|
+
maxPathComponents = nextMaxPathComponents ?? originals.get(filesystem)?.maxPathComponents;
|
|
502
|
+
originalRecord.signal = nextSignal;
|
|
503
|
+
originalRecord.cleanupCharge = nextCleanupCharge;
|
|
504
|
+
originalRecord.maxPathComponents = maxPathComponents;
|
|
505
|
+
});
|
|
475
506
|
registerEntryView(view, async (path, options) => {
|
|
476
507
|
assertOpen(options);
|
|
477
508
|
return { filesystem: original, path };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadBinding(): Promise<never>;
|
|
@@ -3,6 +3,14 @@ import type { ScopedTransportBudgetFrame } from "./transport-budget.js";
|
|
|
3
3
|
export type PlatformErrno = number | undefined;
|
|
4
4
|
export type PlatformComparisonCallback<Callback> = Callback & never;
|
|
5
5
|
export declare const platform: Readonly<{
|
|
6
|
+
nativeFileSystem: Readonly<{
|
|
7
|
+
open: false;
|
|
8
|
+
permissions: false;
|
|
9
|
+
timestamps: false;
|
|
10
|
+
conditionalChmod: false;
|
|
11
|
+
trustedOwnedStaging: false;
|
|
12
|
+
atomicRename: false;
|
|
13
|
+
}>;
|
|
6
14
|
maxCollectionBytes: number;
|
|
7
15
|
errno(_code: string): PlatformErrno;
|
|
8
16
|
callbackAuthorities: false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FsError } from "../contracts/errors.js";
|
|
2
2
|
export const platform = Object.freeze({
|
|
3
|
+
nativeFileSystem: Object.freeze({ open: false, permissions: false, timestamps: false, conditionalChmod: false, trustedOwnedStaging: false, atomicRename: false }),
|
|
3
4
|
maxCollectionBytes: 32 * 1024 * 1024,
|
|
4
5
|
errno(_code) {
|
|
5
6
|
return undefined;
|
|
@@ -4,6 +4,14 @@ import { type ScopedTransportBudgetFrame } from "./transport-budget.js";
|
|
|
4
4
|
export type PlatformErrno = number;
|
|
5
5
|
export type PlatformComparisonCallback<Callback> = Callback;
|
|
6
6
|
export declare const platform: Readonly<{
|
|
7
|
+
nativeFileSystem: Readonly<{
|
|
8
|
+
open: true;
|
|
9
|
+
permissions: true;
|
|
10
|
+
timestamps: true;
|
|
11
|
+
conditionalChmod: true;
|
|
12
|
+
trustedOwnedStaging: true;
|
|
13
|
+
atomicRename: true;
|
|
14
|
+
}>;
|
|
7
15
|
maxCollectionBytes: number;
|
|
8
16
|
errno(code: string): PlatformErrno;
|
|
9
17
|
callbackAuthorities: true;
|
|
@@ -16,6 +16,7 @@ const systemErrnos = (() => {
|
|
|
16
16
|
})();
|
|
17
17
|
const negotiating = new AsyncLocalStorage();
|
|
18
18
|
export const platform = Object.freeze({
|
|
19
|
+
nativeFileSystem: Object.freeze({ open: true, permissions: true, timestamps: true, conditionalChmod: true, trustedOwnedStaging: true, atomicRename: true }),
|
|
19
20
|
maxCollectionBytes: Infinity,
|
|
20
21
|
errno(code) {
|
|
21
22
|
const errno = systemErrnos.get(code === "EOPNOTSUPP" ? "ENOTSUP" : code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poe-platform/safe-fs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.730",
|
|
4
4
|
"description": "Composable filesystem with a portable core and explicit Node adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,9 +69,11 @@
|
|
|
69
69
|
},
|
|
70
70
|
"./fs/real": {
|
|
71
71
|
"types": {
|
|
72
|
+
"workerd": "./dist/safe-fs/fs/real/index.d.ts",
|
|
72
73
|
"browser": "./dist/safe-fs/node-unavailable.d.ts",
|
|
73
74
|
"default": "./dist/safe-fs/fs/real/index.d.ts"
|
|
74
75
|
},
|
|
76
|
+
"workerd": "./dist/safe-fs/fs/real/index.js",
|
|
75
77
|
"browser": null,
|
|
76
78
|
"import": "./dist/safe-fs/fs/real/index.js"
|
|
77
79
|
},
|
|
@@ -134,15 +136,17 @@
|
|
|
134
136
|
"imports": {
|
|
135
137
|
"#safe-fs-platform": {
|
|
136
138
|
"types": {
|
|
139
|
+
"workerd": "./dist/safe-fs/platform/browser.d.ts",
|
|
137
140
|
"browser": "./dist/safe-fs/platform/browser.d.ts",
|
|
138
141
|
"default": "./dist/safe-fs/platform/node.d.ts"
|
|
139
142
|
},
|
|
143
|
+
"workerd": "./dist/safe-fs/platform/browser.js",
|
|
140
144
|
"browser": "./dist/safe-fs/platform/browser.js",
|
|
141
145
|
"default": "./dist/safe-fs/platform/node.js"
|
|
142
146
|
},
|
|
143
147
|
"#safe-fs-native-seek": {
|
|
144
148
|
"types": "./dist/safe-fs/native/fs-seek/loader.d.ts",
|
|
145
|
-
"workerd":
|
|
149
|
+
"workerd": "./dist/safe-fs/node/native-seek-unavailable.js",
|
|
146
150
|
"browser": null,
|
|
147
151
|
"default": "./dist/safe-fs/native/fs-seek/loader.mjs"
|
|
148
152
|
}
|