@poe-platform/safe-fs 0.1.804 → 0.1.806
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 +5 -5
- package/dist/safe-fs/bridge/filesystem.d.ts +1 -0
- package/dist/safe-fs/bridge/filesystem.js +3 -0
- package/dist/safe-fs/bridge/index.d.ts +1 -0
- package/dist/safe-fs/bridge/index.js +1 -0
- package/dist/safe-fs/config/registry.d.ts +2 -0
- package/dist/safe-fs/config/registry.js +15 -0
- package/dist/safe-fs/config.node.js +3 -14
- package/dist/safe-fs/config.portable.d.ts +3 -0
- package/dist/safe-fs/config.portable.js +9 -0
- package/dist/safe-fs/contracts/errors.d.ts +1 -0
- package/dist/safe-fs/contracts/errors.js +12 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +12 -0
- package/dist/safe-fs/contracts/index.d.ts +1 -0
- package/dist/safe-fs/contracts/index.js +1 -0
- package/dist/safe-fs/contracts/io.js +1 -1
- package/dist/safe-fs/core.d.ts +5 -14
- package/dist/safe-fs/core.js +4 -11
- package/dist/safe-fs/fs/devices/index.d.ts +8 -2
- package/dist/safe-fs/fs/devices/index.js +79 -23
- package/dist/safe-fs/fs/devices/path.js +1 -1
- package/dist/safe-fs/fs/memory/atomic-view.d.ts +3 -0
- package/dist/safe-fs/fs/memory/atomic-view.js +5 -0
- package/dist/safe-fs/fs/memory/index.d.ts +7 -4
- package/dist/safe-fs/fs/memory/index.js +211 -160
- package/dist/safe-fs/fs/memory/ledger.d.ts +0 -4
- package/dist/safe-fs/fs/memory/ledger.js +0 -15
- package/dist/safe-fs/fs/memory/limits.js +3 -0
- package/dist/safe-fs/fs/mount/comparison.js +3 -3
- package/dist/safe-fs/fs/mount/identity.d.ts +4 -0
- package/dist/safe-fs/fs/mount/identity.js +8 -0
- package/dist/safe-fs/fs/mount/index.d.ts +1 -0
- package/dist/safe-fs/fs/mount/index.js +31 -0
- package/dist/safe-fs/fs/object-publication/index.d.ts +3 -0
- package/dist/safe-fs/fs/object-publication/index.js +3 -2
- package/dist/safe-fs/fs/quota/index.js +1 -1
- package/dist/safe-fs/fs/readonly/index.js +1 -1
- package/dist/safe-fs/fs/s3/filesystem.js +1 -1
- package/dist/safe-fs/fs/s3/http/transport.js +1 -1
- package/dist/safe-fs/fs/s3/http/xml.js +2 -2
- package/dist/safe-fs/fs/scoped.js +32 -2
- package/dist/safe-fs/fs/webdav/webdav.js +1 -1
- package/dist/safe-fs/index.d.ts +2 -0
- package/dist/safe-fs/index.js +2 -0
- package/dist/safe-fs/node/filesystem.d.ts +2 -1
- package/dist/safe-fs/node/filesystem.js +1 -5
- package/dist/safe-fs/runtime-core.d.ts +27 -0
- package/dist/safe-fs/runtime-core.js +57 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ rename nor inode/version identity from placeholder native metadata. Retained
|
|
|
35
35
|
reads remain available, with native seek reporting `ENOTSUP`. Ordinary browser
|
|
36
36
|
bundles cannot import this backend. Node keeps its POSIX host profile.
|
|
37
37
|
|
|
38
|
-
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.
|
|
38
|
+
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. Archive extraction requires atomic ancestry verification. MemoryFileSystem supports it, including a writable root composed with read-only mounts such as `/skills/.system`; the real adapter refuses it. Mount extraction delegates confinement to the root backend and keeps other mounts read-only in the extraction view. Extraction roots inside a separate mount are rejected because directory and metadata mutations cannot atomically guard ancestors on another backend. Staged file publication supports nested writable mounts when every ancestor provides authoritative validation. 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.
|
|
39
39
|
|
|
40
40
|
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).
|
|
41
41
|
|
|
@@ -148,7 +148,7 @@ Omit `maxOperations` or pass `Infinity` for unlimited cleanup operations.
|
|
|
148
148
|
| `createOverlayFileSystem({ upper, lower })` | Reading through to a lower layer and writing changes to an upper layer |
|
|
149
149
|
| `withFileSystemQuota(filesystem, { maxBytes })` | Enforcing a cumulative logical-byte ceiling across every write path |
|
|
150
150
|
|
|
151
|
-
`createNodeFsBridge` offers a promises-shaped subset, including recursive `cp` and `mkdtemp`. A trusted `reserveReadFile` callback can reserve host resources and return a release function; cancellation retains the reservation until the backend read settles. The portable `createFsBridge` from `@poe-platform/safe-fs/
|
|
151
|
+
`createNodeFsBridge` offers a promises-shaped subset, including recursive `cp` and `mkdtemp`. A trusted `reserveReadFile` callback can reserve host resources and return a release function; cancellation retains the reservation until the backend read settles. The portable `createFsBridge` from `@poe-platform/safe-fs/bridge` instead requires a caller-supplied text codec and returns `Uint8Array` values.
|
|
152
152
|
|
|
153
153
|
## Write an adapter
|
|
154
154
|
|
|
@@ -253,7 +253,7 @@ console.log(new TextDecoder().decode(await catalog.readFile("/welcome.txt")));
|
|
|
253
253
|
|
|
254
254
|
Output: `Hello from the catalog`. `readFile` returns a copy so callers cannot mutate the stored snapshot. Reads check cancellation and `maxBytes`; required mutations always reject with `EROFS`. `stat` and `lstat` share an implementation because this adapter has no symlinks.
|
|
255
255
|
|
|
256
|
-
`createFileSystem` validates options before calling the factory. The Node registry includes `memory` and `real`, accepts new names, and rejects attempts to replace those defaults. For an explicit allowlist without either default, pass `new Map([["catalog", catalogAdapter]])` directly as `registry`. Names select registered factories; they do not load modules from config.
|
|
256
|
+
`createFileSystem` validates options before calling the factory. For Workers, import `createPortableFileSystemAdapterRegistry` and `createFileSystem` from `@poe-code/safe-fs/core`; the portable registry includes `memory` and caller-supplied adapters, without ambient filesystem access. The Node registry includes `memory` and `real`, accepts new names, and rejects attempts to replace those defaults. For an explicit allowlist without either default, pass `new Map([["catalog", catalogAdapter]])` directly as `registry`. Names select registered factories; they do not load modules from config.
|
|
257
257
|
|
|
258
258
|
For a mutable backend, preserve exclusive-create behavior, return independent byte buffers, handle cancellation and size limits, and declare only capabilities you actually provide. Use `FsError` for filesystem failures. Do not invent entry identity from matching paths or metadata; omit `compareEntry` when identity is unknown.
|
|
259
259
|
|
|
@@ -357,8 +357,8 @@ Each batch lists from the beginning because previous keys have been deleted; it
|
|
|
357
357
|
| `addressingStyle` | `path`; alternative `virtual-hosted` requires a DNS endpoint |
|
|
358
358
|
| `listUrlEncoding` | `percent`; alternative `form` |
|
|
359
359
|
| `allowInsecureHttp` | `false`; HTTPS required unless explicitly enabled |
|
|
360
|
-
| `maxPutBytes`, `maxGetBytes` | Unlimited
|
|
361
|
-
| `maxXmlBytes` | Unlimited
|
|
360
|
+
| `maxPutBytes`, `maxGetBytes` | Unlimited when omitted or `Infinity`; positive safe integers set byte caps |
|
|
361
|
+
| `maxXmlBytes` | Unlimited when omitted or `Infinity`; positive safe integers set a byte cap |
|
|
362
362
|
| `requestTimeoutMs` | Unlimited unless configured |
|
|
363
363
|
| `enableCopy` | `true`; disabling uses a buffered GET/PUT fallback |
|
|
364
364
|
| `verifiedConditionalOperations` | Optional `put`, `copy`, `delete` booleans, each defaulting to false; enable only after verifying the server's semantics |
|
|
@@ -12,6 +12,7 @@ type DirectoryOptions = {
|
|
|
12
12
|
};
|
|
13
13
|
export declare class FileSystemBridge<Binary extends Uint8Array> {
|
|
14
14
|
#private;
|
|
15
|
+
static getProvider(bridge: object): FileSystem | undefined;
|
|
15
16
|
constructor(fs: FileSystem, options: {
|
|
16
17
|
readonly cwd?: string;
|
|
17
18
|
readonly root?: string;
|
|
@@ -4,3 +4,4 @@ import type { FsBridgeOptions } from "./types.js";
|
|
|
4
4
|
export type { FsBridgeCodec, FsBridgeDirent, FsBridgeEncoding, FsBridgeFileSystem, FsBridgeOptions, FsBridgeStats } from "./types.js";
|
|
5
5
|
export type FsBridge = FileSystemBridge<Uint8Array<ArrayBuffer>>;
|
|
6
6
|
export declare function createFsBridge(fs: FileSystem, options: FsBridgeOptions): FsBridge;
|
|
7
|
+
export declare const getFsBridgeProvider: typeof FileSystemBridge.getProvider;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function extendAdapterRegistry(defaults, extensions) {
|
|
2
|
+
if (extensions !== undefined &&
|
|
3
|
+
(extensions === null ||
|
|
4
|
+
typeof extensions.get !== "function" ||
|
|
5
|
+
typeof extensions[Symbol.iterator] !== "function")) {
|
|
6
|
+
throw new TypeError("registry must be a filesystem adapter map.");
|
|
7
|
+
}
|
|
8
|
+
const registry = new Map(defaults);
|
|
9
|
+
for (const [name, descriptor] of extensions ?? []) {
|
|
10
|
+
if (registry.has(name))
|
|
11
|
+
throw new TypeError(`Filesystem adapter already registered: ${name}`);
|
|
12
|
+
registry.set(name, descriptor);
|
|
13
|
+
}
|
|
14
|
+
return registry;
|
|
15
|
+
}
|
|
@@ -1,22 +1,11 @@
|
|
|
1
|
+
import { extendAdapterRegistry } from "./config/registry.js";
|
|
1
2
|
import { createMemoryFileSystemAdapter } from "./config/memory.js";
|
|
2
3
|
import { createRealFileSystemAdapter } from "./config/real.js";
|
|
3
4
|
import { createMemoryFileSystem } from "./fs/memory/index.js";
|
|
4
5
|
import { createRealFileSystem } from "./fs/real/index.js";
|
|
5
6
|
export function createNodeFileSystemAdapterRegistry(extensions) {
|
|
6
|
-
|
|
7
|
-
(extensions === null ||
|
|
8
|
-
typeof extensions.get !== "function" ||
|
|
9
|
-
typeof extensions[Symbol.iterator] !== "function")) {
|
|
10
|
-
throw new TypeError("registry must be a filesystem adapter map.");
|
|
11
|
-
}
|
|
12
|
-
const registry = new Map([
|
|
7
|
+
return extendAdapterRegistry(new Map([
|
|
13
8
|
["memory", createMemoryFileSystemAdapter(createMemoryFileSystem)],
|
|
14
9
|
["real", createRealFileSystemAdapter(createRealFileSystem)]
|
|
15
|
-
]);
|
|
16
|
-
for (const [name, descriptor] of extensions ?? []) {
|
|
17
|
-
if (registry.has(name))
|
|
18
|
-
throw new TypeError(`Filesystem adapter already registered: ${name}`);
|
|
19
|
-
registry.set(name, descriptor);
|
|
20
|
-
}
|
|
21
|
-
return registry;
|
|
10
|
+
]), extensions);
|
|
22
11
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createMemoryFileSystemAdapter } from "./config/memory.js";
|
|
2
|
+
import { extendAdapterRegistry } from "./config/registry.js";
|
|
3
|
+
import { createMemoryFileSystem } from "./fs/memory/index.js";
|
|
4
|
+
/** Portable defaults never grant access to the host filesystem. */
|
|
5
|
+
export function createPortableFileSystemAdapterRegistry(extensions) {
|
|
6
|
+
return extendAdapterRegistry(new Map([
|
|
7
|
+
["memory", createMemoryFileSystemAdapter(createMemoryFileSystem)]
|
|
8
|
+
]), extensions);
|
|
9
|
+
}
|
|
@@ -40,6 +40,7 @@ export interface FsErrorOptions extends ErrorOptions {
|
|
|
40
40
|
readonly message?: string;
|
|
41
41
|
}
|
|
42
42
|
export declare class FsError extends Error {
|
|
43
|
+
static [Symbol.hasInstance](value: unknown): boolean;
|
|
43
44
|
readonly code: ErrnoCode;
|
|
44
45
|
readonly errno: PlatformErrno;
|
|
45
46
|
readonly syscall?: string;
|
|
@@ -31,7 +31,18 @@ const descriptions = {
|
|
|
31
31
|
ETIMEDOUT: "operation timed out",
|
|
32
32
|
EXDEV: "cross-device link not permitted",
|
|
33
33
|
};
|
|
34
|
+
const FS_ERROR_BRAND = Symbol.for("@poe-code/safe-fs.FsError");
|
|
34
35
|
export class FsError extends Error {
|
|
36
|
+
static [Symbol.hasInstance](value) {
|
|
37
|
+
if (typeof value !== "object" || value === null)
|
|
38
|
+
return false;
|
|
39
|
+
if (this !== FsError)
|
|
40
|
+
return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
41
|
+
if (Function.prototype[Symbol.hasInstance].call(FsError, value))
|
|
42
|
+
return true;
|
|
43
|
+
const record = value;
|
|
44
|
+
return record[FS_ERROR_BRAND] === true && isErrnoCode(record.code);
|
|
45
|
+
}
|
|
35
46
|
code;
|
|
36
47
|
errno;
|
|
37
48
|
syscall;
|
|
@@ -53,6 +64,7 @@ export class FsError extends Error {
|
|
|
53
64
|
finally {
|
|
54
65
|
errorConstructor.stackTraceLimit = prevStackLimit;
|
|
55
66
|
}
|
|
67
|
+
Object.defineProperty(this, FS_ERROR_BRAND, { value: true, enumerable: false, configurable: false, writable: false });
|
|
56
68
|
this.name = "FsError";
|
|
57
69
|
this.code = code;
|
|
58
70
|
this.errno = errno;
|
|
@@ -284,6 +284,17 @@ export interface PublishStagedFileOptions extends FsOptions {
|
|
|
284
284
|
readonly parent: FileStat;
|
|
285
285
|
readonly destination: FileStat | null;
|
|
286
286
|
}
|
|
287
|
+
/** Publish one staged regular file and validate/retire sibling source entries in
|
|
288
|
+
* the same indivisible commit. Every expected version (including absence) must
|
|
289
|
+
* match before any mutation. Hosts must not emulate this with sequential calls.
|
|
290
|
+
* Unavailable hosts omit publishStagedFileSet. Sources remain caller-owned. */
|
|
291
|
+
export interface PublishStagedFileSetOptions extends PublishStagedFileOptions {
|
|
292
|
+
readonly companions: readonly {
|
|
293
|
+
readonly path: string;
|
|
294
|
+
readonly expected: FileStat | null;
|
|
295
|
+
readonly remove: boolean;
|
|
296
|
+
}[];
|
|
297
|
+
}
|
|
287
298
|
export interface PrepareDirectoryOptions extends FsOptions {
|
|
288
299
|
readonly expected: FileStat | null;
|
|
289
300
|
readonly parent: FileStat;
|
|
@@ -321,6 +332,7 @@ export interface FileSystem {
|
|
|
321
332
|
prepareDirectory?(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
322
333
|
createStagedFile?(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
323
334
|
publishStagedFile?(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
335
|
+
publishStagedFileSet?(staging: FileStaging, destination: string, options: PublishStagedFileSetOptions): Promise<FileStat>;
|
|
324
336
|
/** Admit a complete canonical root-to-directory prefix and prepare a guard.
|
|
325
337
|
* The returned guard checks current identities/search access synchronously;
|
|
326
338
|
* preparation is not validation, and the guard's result is not an async lease. */
|
|
@@ -211,7 +211,7 @@ class ReadBytesGenerator {
|
|
|
211
211
|
if (!this.finished && this.iterator?.return) {
|
|
212
212
|
this.finished = true;
|
|
213
213
|
const it = this.iterator;
|
|
214
|
-
const cleanup = Promise
|
|
214
|
+
const cleanup = new Promise(resolve => { resolve(it.return()); });
|
|
215
215
|
if (signal?.aborted)
|
|
216
216
|
void cleanup.catch(noop);
|
|
217
217
|
else
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export
|
|
3
|
-
export * from "./contracts/io.js";
|
|
4
|
-
export { openFileDescriptor } from "./fs/descriptor.js";
|
|
5
|
-
export type { DescriptorBackend, DescriptorOpenOptions } from "./fs/descriptor.js";
|
|
6
|
-
export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
|
|
7
|
-
export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
|
|
8
|
-
export * from "./fs/memory/index.js";
|
|
9
|
-
export * from "./fs/devices/index.js";
|
|
1
|
+
export * from "./runtime-core.js";
|
|
2
|
+
export { MemoryFileSystem, MemoryRedirectHandle, createMemoryFileSystem, defaultMemoryFileSystemLimits, bindConditionalMutation, getLastReadMemoryFileSourceRef, isCleanAbsolutePath, tryGetMemoryDirectoryEntryNamesSync, tryMkdirMemorySync, tryOpenMemoryRedirectHandleSync, tryReadMemoryFileViewSync, tryResolveMemoryDevicePath, tryRmRfMemorySync, tryWriteMemoryFileInDirSync, tryWriteMemoryFileSync, utf8ByteLength } from "./fs/memory/index.js";
|
|
10
3
|
export * from "./fs/readonly/index.js";
|
|
11
4
|
export * from "./fs/mount/index.js";
|
|
12
5
|
export * from "./fs/overlay/index.js";
|
|
13
6
|
export * from "./fs/quota/index.js";
|
|
14
7
|
export * from "./fs/object-publication/index.js";
|
|
15
|
-
export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
16
|
-
export type { RetainedFileSystemCleanupView, RetainedFileSystemCleanupOptions } from "./fs/scoped.js";
|
|
17
8
|
export * from "./fs/webdav/index.js";
|
|
18
9
|
export * from "./bridge/index.js";
|
|
19
10
|
export * from "./python/index.js";
|
|
20
|
-
export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
|
|
21
|
-
export type { EntryViewResolver } from "./fs/mount/comparison.js";
|
|
22
11
|
export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
|
|
23
12
|
export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
|
|
24
13
|
export * from "./contracts/object.js";
|
|
25
14
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
26
|
-
export { bindConditionalMutation, type ConditionalMutationBinding } from "./fs/memory/index.js";
|
|
27
15
|
export * from "./fs/s3/index.js";
|
|
28
16
|
export * from "./fs/s3/http/index.js";
|
|
17
|
+
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
18
|
+
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
|
19
|
+
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export
|
|
3
|
-
export * from "./contracts/io.js";
|
|
4
|
-
export { openFileDescriptor } from "./fs/descriptor.js";
|
|
5
|
-
export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
|
|
6
|
-
export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
|
|
7
|
-
export * from "./fs/memory/index.js";
|
|
8
|
-
export * from "./fs/devices/index.js";
|
|
1
|
+
export * from "./runtime-core.js";
|
|
2
|
+
export { MemoryFileSystem, MemoryRedirectHandle, createMemoryFileSystem, defaultMemoryFileSystemLimits, bindConditionalMutation, getLastReadMemoryFileSourceRef, isCleanAbsolutePath, tryGetMemoryDirectoryEntryNamesSync, tryMkdirMemorySync, tryOpenMemoryRedirectHandleSync, tryReadMemoryFileViewSync, tryResolveMemoryDevicePath, tryRmRfMemorySync, tryWriteMemoryFileInDirSync, tryWriteMemoryFileSync, utf8ByteLength } from "./fs/memory/index.js";
|
|
9
3
|
export * from "./fs/readonly/index.js";
|
|
10
4
|
export * from "./fs/mount/index.js";
|
|
11
5
|
export * from "./fs/overlay/index.js";
|
|
12
6
|
export * from "./fs/quota/index.js";
|
|
13
7
|
export * from "./fs/object-publication/index.js";
|
|
14
|
-
export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
15
8
|
export * from "./fs/webdav/index.js";
|
|
16
9
|
export * from "./bridge/index.js";
|
|
17
10
|
export * from "./python/index.js";
|
|
18
|
-
export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
|
|
19
11
|
export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
|
|
20
12
|
export * from "./contracts/object.js";
|
|
21
13
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
22
|
-
export { bindConditionalMutation } from "./fs/memory/index.js";
|
|
23
14
|
export * from "./fs/s3/index.js";
|
|
24
15
|
export * from "./fs/s3/http/index.js";
|
|
16
|
+
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
17
|
+
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
2
|
-
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
3
3
|
import type { ByteSource } from "../../contracts/io.js";
|
|
4
|
+
import { type EntryLocation } from "../mount/comparison.js";
|
|
5
|
+
import { pathNamespace } from "../path-namespace.js";
|
|
4
6
|
export declare class DeviceFileSystem implements FileSystem {
|
|
5
7
|
#private;
|
|
6
8
|
readonly capabilities: FileSystemCapabilities;
|
|
9
|
+
get [pathNamespace](): any;
|
|
10
|
+
_resolveEntryView: (path: string, options: FsOptions) => Promise<EntryLocation>;
|
|
11
|
+
_entryAuthority: () => Promise<"unknown">;
|
|
7
12
|
constructor(filesystem: FileSystem);
|
|
8
13
|
canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
|
|
9
14
|
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
10
15
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
11
16
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
12
|
-
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../
|
|
17
|
+
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../runtime-core.js").EntryComparison>;
|
|
13
18
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
14
19
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
15
20
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
@@ -35,6 +40,7 @@ export declare class DeviceFileSystem implements FileSystem {
|
|
|
35
40
|
removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
|
|
36
41
|
createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
37
42
|
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
43
|
+
publishStagedFileSet(receipt: FileStaging, destination: string, supplied: PublishStagedFileSetOptions): Promise<FileStat>;
|
|
38
44
|
prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
|
|
39
45
|
prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
|
|
40
46
|
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
@@ -2,9 +2,9 @@ import { snapshotStagingCreation } from "../staging-cleanup.js";
|
|
|
2
2
|
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
3
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
4
4
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
5
|
-
import { compareEntries
|
|
5
|
+
import { compareEntries } from "../mount/comparison.js";
|
|
6
6
|
import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
|
|
7
|
-
import { tryResolveMemoryDevicePath } from "
|
|
7
|
+
import { tryResolveMemoryDevicePath } from "../../runtime-core.js";
|
|
8
8
|
import { createDeviceYield, deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
|
|
9
9
|
import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
|
|
10
10
|
import { pathNamespace } from "../path-namespace.js";
|
|
@@ -71,21 +71,46 @@ function nonnegative(value, path) {
|
|
|
71
71
|
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
72
72
|
throw new FsError("EINVAL", { path });
|
|
73
73
|
}
|
|
74
|
+
const sharedUnknownEntryAuthority = async () => "unknown";
|
|
74
75
|
export class DeviceFileSystem {
|
|
75
76
|
#filesystem;
|
|
76
77
|
capabilities;
|
|
77
|
-
#
|
|
78
|
-
#
|
|
78
|
+
#identityScopeObj;
|
|
79
|
+
#nullStatObj;
|
|
80
|
+
#directoryStatObj;
|
|
81
|
+
get [pathNamespace]() {
|
|
82
|
+
return Reflect.get(this.#filesystem, pathNamespace);
|
|
83
|
+
}
|
|
84
|
+
get #nullStat() {
|
|
85
|
+
if (this.#nullStatObj)
|
|
86
|
+
return this.#nullStatObj;
|
|
87
|
+
const identityScope = (this.#identityScopeObj ??= Object.freeze({}));
|
|
88
|
+
return (this.#nullStatObj = Object.freeze({
|
|
89
|
+
type: "character", mode: 0o020666, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
|
|
90
|
+
mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1, preferredIoBlockSize: 4096,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
get #directoryStat() {
|
|
94
|
+
if (this.#directoryStatObj)
|
|
95
|
+
return this.#directoryStatObj;
|
|
96
|
+
const identityScope = (this.#identityScopeObj ??= Object.freeze({}));
|
|
97
|
+
return (this.#directoryStatObj = Object.freeze({
|
|
98
|
+
type: "directory", mode: 0o040755, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
|
|
99
|
+
mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 2, dev: 0, nlink: 1,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
_resolveEntryView = async (path, options) => {
|
|
103
|
+
const resolved = await this.#resolve(path, options);
|
|
104
|
+
if (resolved === nullPath || resolved === deviceDirectory) {
|
|
105
|
+
return { filesystem: this, path: resolved, stat: resolved === nullPath ? this.#nullStat : this.#directoryStat, readOnly: false };
|
|
106
|
+
}
|
|
107
|
+
return { filesystem: this.#filesystem, path };
|
|
108
|
+
};
|
|
109
|
+
_entryAuthority = sharedUnknownEntryAuthority;
|
|
79
110
|
constructor(filesystem) {
|
|
80
111
|
this.#filesystem = filesystem;
|
|
81
|
-
Object.defineProperty(this, pathNamespace, { get: () => Reflect.get(filesystem, pathNamespace) });
|
|
82
112
|
this.capabilities = globalCapabilities(filesystem);
|
|
83
|
-
|
|
84
|
-
const stat = { mode: 0o020666, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
|
|
85
|
-
mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1 };
|
|
86
|
-
this.#nullStat = Object.freeze({ ...stat, type: "character", preferredIoBlockSize: 4096 });
|
|
87
|
-
this.#directoryStat = Object.freeze({ ...stat, type: "directory", mode: 0o040755, ino: 2 });
|
|
88
|
-
const methods = new Map();
|
|
113
|
+
let methods;
|
|
89
114
|
const view = new Proxy(this, {
|
|
90
115
|
set: (_target, property, value) => Reflect.set(filesystem, property, value, filesystem),
|
|
91
116
|
get(target, property) {
|
|
@@ -93,24 +118,14 @@ export class DeviceFileSystem {
|
|
|
93
118
|
if (typeof value !== "function" || property === "constructor")
|
|
94
119
|
return value;
|
|
95
120
|
const backing = Reflect.get(filesystem, property, filesystem);
|
|
96
|
-
const cached = methods
|
|
121
|
+
const cached = methods?.get(property);
|
|
97
122
|
if (cached && cached.backing === backing)
|
|
98
123
|
return cached.bound;
|
|
99
124
|
const bound = value.bind(target);
|
|
100
|
-
methods.set(property, { backing, bound });
|
|
125
|
+
(methods ??= new Map()).set(property, { backing, bound });
|
|
101
126
|
return bound;
|
|
102
127
|
},
|
|
103
128
|
});
|
|
104
|
-
for (const identity of [this, view])
|
|
105
|
-
registerEntryView(identity, async (path, options) => {
|
|
106
|
-
const resolved = await this.#resolve(path, options);
|
|
107
|
-
if (resolved === nullPath || resolved === deviceDirectory)
|
|
108
|
-
return { filesystem: this, path: resolved,
|
|
109
|
-
stat: resolved === nullPath ? this.#nullStat : this.#directoryStat, readOnly: false };
|
|
110
|
-
return { filesystem, path };
|
|
111
|
-
});
|
|
112
|
-
// This internal view has no identity proof beyond its synthetic stats.
|
|
113
|
-
registerEntryAuthority(this, async () => "unknown");
|
|
114
129
|
return view;
|
|
115
130
|
}
|
|
116
131
|
#resolve(path, options, followFinal = true, resizeCreate, traversal) {
|
|
@@ -624,6 +639,47 @@ export class DeviceFileSystem {
|
|
|
624
639
|
await requireOwnedMutation(this.#filesystem, destination, "guardedStagingPublication", options, options.destination === null);
|
|
625
640
|
await this.#filesystem.publishStagedFile(staging, destination, options);
|
|
626
641
|
}
|
|
642
|
+
async publishStagedFileSet(receipt, destination, supplied) {
|
|
643
|
+
const publish = this.#filesystem.publishStagedFileSet;
|
|
644
|
+
if (!publish)
|
|
645
|
+
throw new FsError("ENOTSUP", { path: destination });
|
|
646
|
+
const entry = (value) => ({ path: value.path, stat: { ...value.stat } });
|
|
647
|
+
const staging = { ...receipt, parent: entry(receipt.parent), directory: entry(receipt.directory), file: entry(receipt.file) };
|
|
648
|
+
const options = {
|
|
649
|
+
...supplied,
|
|
650
|
+
parent: { ...supplied.parent },
|
|
651
|
+
destination: supplied.destination === null ? null : { ...supplied.destination },
|
|
652
|
+
companions: supplied.companions.map(value => ({ ...value, expected: value.expected === null ? null : { ...value.expected } })),
|
|
653
|
+
...(supplied.ancestors === undefined ? {} : { ancestors: snapshotDirectoryAncestry(supplied.ancestors) }),
|
|
654
|
+
};
|
|
655
|
+
options.signal?.throwIfAborted();
|
|
656
|
+
const capabilities = await this.#filesystem.capabilitiesFor?.(destination, { ...options, create: options.destination === null }) ?? this.#filesystem.capabilities;
|
|
657
|
+
if (capabilities.readOnly === true)
|
|
658
|
+
throw new FsError("EROFS", { path: destination });
|
|
659
|
+
for (const path of [staging.parent.path, staging.directory.path])
|
|
660
|
+
await this.#mutable(path, options, false);
|
|
661
|
+
const guards = [];
|
|
662
|
+
if (options.ancestors)
|
|
663
|
+
guards.push(await this.prepareDirectoryAncestry(options.ancestors, options));
|
|
664
|
+
for (const path of [staging.file.path, destination, ...options.companions.map(value => value.path)]) {
|
|
665
|
+
await this.#mutable(path, options, false);
|
|
666
|
+
const resolution = await this.prepareStagingResolution(path, options);
|
|
667
|
+
guards.push(resolution.validate);
|
|
668
|
+
}
|
|
669
|
+
const callerGuard = options.commitGuard;
|
|
670
|
+
options.signal?.throwIfAborted();
|
|
671
|
+
return publish.call(this.#filesystem, staging, destination, { ...options, commitGuard: () => {
|
|
672
|
+
options.signal?.throwIfAborted();
|
|
673
|
+
if (callerGuard !== undefined)
|
|
674
|
+
runStagingGuard(callerGuard);
|
|
675
|
+
// Recheck every route in the backend's atomic commit, so a source cannot
|
|
676
|
+
// cross into the virtual device namespace after asynchronous admission.
|
|
677
|
+
for (const guard of guards)
|
|
678
|
+
runStagingGuard(guard);
|
|
679
|
+
options.signal?.throwIfAborted();
|
|
680
|
+
return true;
|
|
681
|
+
} });
|
|
682
|
+
}
|
|
627
683
|
async prepareDirectoryAncestry(ancestors, options = {}) {
|
|
628
684
|
options.signal?.throwIfAborted();
|
|
629
685
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
2
2
|
import { pathByteLength, validatePath } from "../../contracts/virtual-path.js";
|
|
3
3
|
import { capturePathNamespace, pathNamespace } from "../path-namespace.js";
|
|
4
|
-
import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "
|
|
4
|
+
import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../../runtime-core.js";
|
|
5
5
|
export const nullPath = "/dev/null";
|
|
6
6
|
export const deviceDirectory = "/dev";
|
|
7
7
|
function isNullParts(parts) {
|
|
@@ -4,5 +4,8 @@ export interface MemoryAtomicView {
|
|
|
4
4
|
stat(path: string): FileStat | undefined;
|
|
5
5
|
names(path: string): readonly string[];
|
|
6
6
|
}
|
|
7
|
+
type LazyMemoryAtomicView = (this: FileSystem) => MemoryAtomicView | undefined;
|
|
7
8
|
export declare function registerMemoryAtomicView(fs: FileSystem, view: MemoryAtomicView, intact: () => boolean): void;
|
|
9
|
+
export declare function registerLazyMemoryAtomicView(fs: FileSystem, create: LazyMemoryAtomicView): void;
|
|
8
10
|
export declare function memoryAtomicView(fs: FileSystem): MemoryAtomicView | undefined;
|
|
11
|
+
export {};
|
|
@@ -2,7 +2,12 @@ const views = new WeakMap();
|
|
|
2
2
|
export function registerMemoryAtomicView(fs, view, intact) {
|
|
3
3
|
views.set(fs, { view, intact });
|
|
4
4
|
}
|
|
5
|
+
export function registerLazyMemoryAtomicView(fs, create) {
|
|
6
|
+
views.set(fs, create);
|
|
7
|
+
}
|
|
5
8
|
export function memoryAtomicView(fs) {
|
|
6
9
|
const registered = views.get(fs);
|
|
10
|
+
if (typeof registered === "function")
|
|
11
|
+
return registered.call(fs);
|
|
7
12
|
return registered?.intact() ? registered.view : undefined;
|
|
8
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
|
+
import { type MemoryAtomicView } from "./atomic-view.js";
|
|
3
4
|
import { MemoryAllocation, MemoryLedger } from "./ledger.js";
|
|
4
5
|
import { type MemoryFileSystemOptions } from "./limits.js";
|
|
5
6
|
export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
|
|
@@ -34,7 +35,6 @@ interface SymlinkNode extends Metadata {
|
|
|
34
35
|
}
|
|
35
36
|
type MemoryNode = FileNode | DirectoryNode | SymlinkNode;
|
|
36
37
|
declare class MemoryCache {
|
|
37
|
-
readonly allocations: MemoryAllocation[];
|
|
38
38
|
readonly files: FileNode[];
|
|
39
39
|
readonly directories: DirectoryNode[];
|
|
40
40
|
readonly removedScratch: MemoryNode[];
|
|
@@ -59,9 +59,10 @@ export interface ConditionalMutationBinding {
|
|
|
59
59
|
}
|
|
60
60
|
export declare function bindConditionalMutation<Result>(identityScope: object | symbol | undefined, binding: ConditionalMutationBinding, action: () => Promise<Result>): Promise<Result>;
|
|
61
61
|
export declare class MemoryFileSystem implements FileSystem {
|
|
62
|
+
#private;
|
|
62
63
|
capabilitiesFor?: NonNullable<FileSystem["capabilitiesFor"]>;
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
private get identityScope();
|
|
65
|
+
get capabilities(): FileSystemCapabilities;
|
|
65
66
|
private nextInode;
|
|
66
67
|
private readonly ledger;
|
|
67
68
|
readonly _cache: MemoryCache;
|
|
@@ -70,6 +71,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
70
71
|
private readonly root;
|
|
71
72
|
private totalBytes;
|
|
72
73
|
symlinkCount: number;
|
|
74
|
+
_getAtomicView(): MemoryAtomicView | undefined;
|
|
73
75
|
constructor(options?: MemoryFileSystemOptions);
|
|
74
76
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
|
|
75
77
|
canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
|
|
@@ -115,6 +117,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
115
117
|
prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
|
|
116
118
|
private verifyDirectoryAncestry;
|
|
117
119
|
publishStagedFile(receipt: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
120
|
+
publishStagedFileSet(receipt: FileStaging, destination: string, options: PublishStagedFileSetOptions): Promise<FileStat>;
|
|
118
121
|
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
119
122
|
private removeStagingLocations;
|
|
120
123
|
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|