@poe-platform/safe-fs 0.1.803 → 0.1.805
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 +22 -4
- package/dist/safe-fs/bridge/filesystem.js +1 -1
- package/dist/safe-fs/contracts/errors.d.ts +1 -0
- package/dist/safe-fs/contracts/errors.js +16 -3
- package/dist/safe-fs/contracts/filesystem.d.ts +3 -2
- package/dist/safe-fs/contracts/io.d.ts +2 -0
- package/dist/safe-fs/contracts/io.js +32 -7
- package/dist/safe-fs/contracts/portable-path.d.ts +8 -0
- package/dist/safe-fs/contracts/portable-path.js +22 -1
- package/dist/safe-fs/core.d.ts +3 -14
- package/dist/safe-fs/core.js +3 -11
- package/dist/safe-fs/fs/devices/index.d.ts +6 -1
- package/dist/safe-fs/fs/devices/index.js +40 -24
- package/dist/safe-fs/fs/devices/path.js +1 -1
- package/dist/safe-fs/fs/directory-admission.js +2 -2
- 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 +6 -3
- package/dist/safe-fs/fs/memory/index.js +147 -131
- package/dist/safe-fs/fs/memory/ledger.js +3 -1
- 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/object-publication/index.js +2 -1
- package/dist/safe-fs/fs/readonly/index.js +1 -1
- package/dist/safe-fs/fs/real/index.js +3 -2
- package/dist/safe-fs/fs/s3/filesystem.js +1 -1
- package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +14 -3
- package/dist/safe-fs/fs/s3/http/request-node.d.ts +2 -2
- package/dist/safe-fs/fs/s3/http/types.d.ts +2 -2
- package/dist/safe-fs/fs/s3/manifest-admission.js +0 -4
- package/dist/safe-fs/fs/s3/namespace.js +2 -4
- package/dist/safe-fs/fs/webdav/webdav.js +5 -29
- package/dist/safe-fs/fs/webdav/xml.js +2 -2
- package/dist/safe-fs/python/emscripten.js +5 -2
- package/dist/safe-fs/python/filesystem.js +2 -2
- package/dist/safe-fs/python/native.js +1 -1
- package/dist/safe-fs/python/stat.js +5 -2
- package/dist/safe-fs/runtime-core.d.ts +26 -0
- package/dist/safe-fs/runtime-core.js +56 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# safe-fs
|
|
2
2
|
|
|
3
|
-
An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges. Portable filesystem types and POSIX helpers compile without Node types.
|
|
3
|
+
An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges. Portable filesystem types and POSIX helpers compile without Node types. The `posixPath` export from `@poe-code/safe-fs/core` provides `basename`, `dirname`, `extname`, `join`, `isAbsolute`, `normalize`, `resolve`, `relative`, `sep`, and `delimiter`; `resolve` and `relative` use `/` as their working directory in every runtime.
|
|
4
4
|
|
|
5
5
|
Under `workerd` and `browser` conditions, the root export includes portable filesystems, `S3FileSystem`, and `MockS3Client`. Supply an S3 transport to use object storage without Node globals.
|
|
6
6
|
|
|
@@ -86,14 +86,26 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
|
|
|
86
86
|
|
|
87
87
|
All required methods must exist, but a backend may reject an operation with `FsError`, such as `EROFS` for a write or `ENOTSUP` for unsupported semantics. Check optional methods and `capabilities` rather than assuming every backend behaves like a local disk. Errors expose `code` and may include `syscall`, `path`, `dest`, and `cause`.
|
|
88
88
|
|
|
89
|
+
`readBytes` supports frozen host abort signals. The exported
|
|
90
|
+
`listenForAbort(signal, listener)` returns a cleanup function for custom
|
|
91
|
+
operations; if frozen host bookkeeping prevents registration, callers must
|
|
92
|
+
check cancellation at operation boundaries, as `readBytes` does.
|
|
93
|
+
|
|
89
94
|
`createDeviceFileSystem(fs)` adds portable `/dev/null` whole-file, stream, and descriptor I/O. Descriptor reads return EOF and writes discard bytes; stat remains a zero-size character device and descriptor position remains zero. Truncating opens are accepted, exclusive creation fails with `EEXIST`, and descriptor resizing and synchronization are unsupported. Access modes, cancellation, and closed handles use the normal descriptor checks. With an authoritative object store, use `createDeviceFileSystem(withObjectFileDescriptors(fs, store))` so null-device I/O never acquires or publishes an object version; ordinary files retain conditional publication. The device wrapper must be outermost for this composition.
|
|
90
95
|
|
|
96
|
+
`PythonStatTranslator` and `createPythonRuntimeStatMapper` allocate stable guest device
|
|
97
|
+
identities without a default device-count budget. Their optional `maxDevices` accepts
|
|
98
|
+
`Infinity` or a positive safe integer. Explicit budgets remain enforced; device numbers
|
|
99
|
+
must still fit the disjoint application/runtime ranges of the uint32 Python stat ABI.
|
|
100
|
+
|
|
91
101
|
`withObjectFileDescriptors(fs, store)` supports large shell and Python descriptor
|
|
92
102
|
writes when the host supplies `store.createStaging`: private externally backed
|
|
93
103
|
pages keep working memory bounded without publishing the growing file after
|
|
94
104
|
every write. Conditional publication still occurs at sync/close, and retained
|
|
95
105
|
readers keep their old versions. Without that optional backend primitive, the
|
|
96
|
-
optional dirty-page budget limits unflushed output only when configured.
|
|
106
|
+
optional dirty-page budget limits unflushed output only when configured. Descriptor
|
|
107
|
+
resource limits accept `Infinity` and are unlimited when omitted; `chunkBytes`
|
|
108
|
+
remains a positive finite integer. See the
|
|
97
109
|
[object descriptor and spill contract](src/contracts/object-publication.md) for
|
|
98
110
|
backend methods, failure semantics and qualification; no provider storage is
|
|
99
111
|
configured automatically.
|
|
@@ -270,7 +282,8 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
270
282
|
|
|
271
283
|
| Operation | Options |
|
|
272
284
|
| --- | --- |
|
|
273
|
-
| `readFile` | `maxBytes`:
|
|
285
|
+
| `readFile` | `maxBytes`: nonnegative safe-integer byte cap; `Infinity` adds no per-read cap |
|
|
286
|
+
| `readdir` | `maxEntries`: nonnegative safe-integer entry cap; omission or `Infinity` preserves backend limits and ordering |
|
|
274
287
|
| `writeFile`, `writeStream` | `flag`: `w` (default, replace), `wx` (exclusive create), `a` (append), or `ax` (exclusive append/create); optional `mode` |
|
|
275
288
|
| `appendFile` | Optional `mode` |
|
|
276
289
|
| `mkdir` | `recursive` (default false); optional `mode` |
|
|
@@ -278,7 +291,7 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
278
291
|
| `copyFile` | `exclusive` (default false) |
|
|
279
292
|
| `readStream` | `start` (default 0), `endExclusive` (default end of file), `chunkSize` (built-in default 64 KiB) |
|
|
280
293
|
|
|
281
|
-
`validatePath(path, { maxPathBytes, maxPathComponents })` accepts optional UTF-8 byte and component quotas; both default to `Infinity`. Memory accepts the same constructor options and counts cumulative symlink expansion. Device operations accept these quotas under `pathLimits`. WebDAV accepts `maxTimestampPropertyBytes` for timestamp metadata, also unlimited by default.
|
|
294
|
+
`validatePath(path, { maxPathBytes, maxPathComponents })` accepts optional UTF-8 byte and component quotas; both default to `Infinity`. Memory accepts the same constructor options and counts cumulative symlink expansion. Device operations and WebDAV stat, access, and writes accept these quotas under `pathLimits`. WebDAV accepts `maxTimestampPropertyBytes` for timestamp metadata, also unlimited by default.
|
|
282
295
|
|
|
283
296
|
`access` takes a separate mode bitmask from `ACCESS_MODES`. `chmod` takes a mode, `utimes` takes millisecond timestamps, and `truncate` takes a byte length (default 0). For conditional chmod, check `capabilitiesFor(path, { conditionalChmod: true })` (or `capabilities`) and require `conditionalChmod: true`; supply `parent`, `expected`, and complete root-to-parent `ancestors` together. An optional mutation-free `commitGuard` must return literal `true` synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the [bridge signatures](src/bridge/filesystem.ts).
|
|
284
297
|
|
|
@@ -289,6 +302,11 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
289
302
|
|
|
290
303
|
`S3FileSystem` requires `transport` and `bucket`. A transport supplies `headObject`, `getObject`, `putObject`, `deleteObject`, `copyObject`, and `listObjectsV2`; see the [transport interface](src/fs/s3/transport.ts).
|
|
291
304
|
|
|
305
|
+
`createS3NamespaceFileSystem` resource limits (`maxBytes`, `maxEntries`,
|
|
306
|
+
`maxManifestBytes`, `maxAttempts`, and inherited descriptor limits) are unlimited
|
|
307
|
+
when omitted or set to `Infinity`; positive safe integers enable finite limits.
|
|
308
|
+
Namespace paths have no implicit length cap; configured manifest budgets still apply.
|
|
309
|
+
|
|
292
310
|
`createS3Transport(client, capabilities?)` wraps a compatible client with explicit `streamingRead`, `streamingWrite`, `conditionalPut`, `conditionalCopy`, and `conditionalDelete` flags (all absent by default). Direct transport requests accept an optional `abortSignal`.
|
|
293
311
|
|
|
294
312
|
For in-memory S3 simulations, `new MockS3Client({ buckets, pageSize?, now?, authorize? })` requires bucket names. `pageSize` defaults to 1,000 (range 1–1,000), `now` defaults to the current date/time, and optional `authorize(request)` can reject requests. It does not verify a real provider.
|
|
@@ -70,7 +70,7 @@ export class FileSystemBridge {
|
|
|
70
70
|
assertBridgePath(this.#root, this.#cwd);
|
|
71
71
|
this.#signal = options.signal;
|
|
72
72
|
const maximum = options.readFileMaxBytes;
|
|
73
|
-
if (maximum !== undefined && (!Number.isSafeInteger(maximum) || maximum < 0))
|
|
73
|
+
if (maximum !== undefined && maximum !== Infinity && (!Number.isSafeInteger(maximum) || maximum < 0))
|
|
74
74
|
throw new TypeError("Invalid readFileMaxBytes");
|
|
75
75
|
this.#readFileMaxBytes = maximum;
|
|
76
76
|
if (options.reserveReadFile !== undefined && typeof options.reserveReadFile !== "function")
|
|
@@ -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;
|
|
@@ -44,14 +55,16 @@ export class FsError extends Error {
|
|
|
44
55
|
const operation = options.syscall ? `, ${options.syscall}` : "";
|
|
45
56
|
const path = options.path === undefined ? "" : ` '${options.path}'`;
|
|
46
57
|
const destination = options.dest === undefined ? "" : ` -> '${options.dest}'`;
|
|
47
|
-
const
|
|
48
|
-
|
|
58
|
+
const errorConstructor = Error;
|
|
59
|
+
const prevStackLimit = errorConstructor.stackTraceLimit;
|
|
60
|
+
errorConstructor.stackTraceLimit = 0;
|
|
49
61
|
try {
|
|
50
62
|
super(`${code}: ${options.message ?? descriptions[code]}${operation}${path}${destination}`, options);
|
|
51
63
|
}
|
|
52
64
|
finally {
|
|
53
|
-
|
|
65
|
+
errorConstructor.stackTraceLimit = prevStackLimit;
|
|
54
66
|
}
|
|
67
|
+
Object.defineProperty(this, FS_ERROR_BRAND, { value: true, enumerable: false, configurable: false, writable: false });
|
|
55
68
|
this.name = "FsError";
|
|
56
69
|
this.code = code;
|
|
57
70
|
this.errno = errno;
|
|
@@ -170,13 +170,14 @@ export interface RenameOptions extends FsOptions {
|
|
|
170
170
|
readonly noReplace?: boolean;
|
|
171
171
|
}
|
|
172
172
|
export interface ReadFileOptions extends FsOptions {
|
|
173
|
+
/** Nonnegative safe-integer byte cap; Infinity adds no per-read cap. */
|
|
173
174
|
readonly maxBytes?: number;
|
|
174
175
|
}
|
|
175
176
|
export interface ReadDirectoryOptions extends FsOptions {
|
|
176
177
|
/**
|
|
177
|
-
* Optional per-listing entry admission limit, a nonnegative safe integer.
|
|
178
|
+
* Optional per-listing entry admission limit, a nonnegative safe integer or Infinity.
|
|
178
179
|
* Zero permits an empty listing; overflow rejects with EFBIG, never truncates.
|
|
179
|
-
* Omission preserves the adapter's existing limits and ordering. Composed
|
|
180
|
+
* Omission or Infinity preserves the adapter's existing limits and ordering. Composed
|
|
180
181
|
* adapters may conservatively count distinct candidates before visibility
|
|
181
182
|
* filtering. This is not a global traversal, host-allocation or work quota.
|
|
182
183
|
*/
|
|
@@ -7,4 +7,6 @@ export interface CollectOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function toByteSource(input: string | Uint8Array): ByteSource;
|
|
9
9
|
export declare function collectBytes(source: ByteSource, options: CollectOptions): Promise<Uint8Array>;
|
|
10
|
+
/** Subscribe when supported; callers must still check cancellation at operation boundaries. */
|
|
11
|
+
export declare function listenForAbort(signal: AbortSignal, listener: () => void): () => void;
|
|
10
12
|
export declare function readBytes(source: ByteSource, signal?: AbortSignal): AsyncGenerator<Uint8Array>;
|
|
@@ -108,27 +108,52 @@ export async function collectBytes(source, options) {
|
|
|
108
108
|
options.signal?.throwIfAborted();
|
|
109
109
|
return buffer.subarray(0, size);
|
|
110
110
|
}
|
|
111
|
+
/** Subscribe when supported; callers must still check cancellation at operation boundaries. */
|
|
112
|
+
export function listenForAbort(signal, listener) {
|
|
113
|
+
let callback = listener;
|
|
114
|
+
const onAbort = () => callback?.();
|
|
115
|
+
const dispose = () => {
|
|
116
|
+
// Release operation state even when a frozen host refuses removal.
|
|
117
|
+
callback = undefined;
|
|
118
|
+
try {
|
|
119
|
+
signal.removeEventListener("abort", onAbort);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!(error instanceof TypeError) || Object.isExtensible(signal))
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Frozen hosts may register before failing to update their bookkeeping.
|
|
131
|
+
dispose();
|
|
132
|
+
if (!(error instanceof TypeError) || Object.isExtensible(signal))
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
return dispose;
|
|
136
|
+
}
|
|
111
137
|
async function abortable(operation, signal) {
|
|
112
138
|
signal?.throwIfAborted();
|
|
113
139
|
if (!signal)
|
|
114
140
|
return operation();
|
|
115
141
|
return new Promise((resolve, reject) => {
|
|
116
|
-
const
|
|
117
|
-
|
|
142
|
+
const dispose = listenForAbort(signal, () => {
|
|
143
|
+
dispose();
|
|
118
144
|
reject(signal.reason);
|
|
119
|
-
};
|
|
120
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
});
|
|
121
146
|
try {
|
|
122
147
|
Promise.resolve(operation()).then((result) => {
|
|
123
|
-
|
|
148
|
+
dispose();
|
|
124
149
|
resolve(result);
|
|
125
150
|
}, (error) => {
|
|
126
|
-
|
|
151
|
+
dispose();
|
|
127
152
|
reject(error);
|
|
128
153
|
});
|
|
129
154
|
}
|
|
130
155
|
catch (error) {
|
|
131
|
-
|
|
156
|
+
dispose();
|
|
132
157
|
reject(error);
|
|
133
158
|
}
|
|
134
159
|
});
|
|
@@ -4,10 +4,18 @@ export declare const dirname: typeof virtualDirname;
|
|
|
4
4
|
export declare function extname(path: string): string;
|
|
5
5
|
export declare function isAbsolutePath(path: string): boolean;
|
|
6
6
|
export declare function joinPath(...paths: string[]): string;
|
|
7
|
+
declare function resolve(...paths: string[]): string;
|
|
8
|
+
declare function relative(from: string, to: string): string;
|
|
7
9
|
export declare const posixPath: Readonly<{
|
|
8
10
|
basename: typeof basename;
|
|
9
11
|
dirname: typeof virtualDirname;
|
|
10
12
|
extname: typeof extname;
|
|
11
13
|
join: typeof joinPath;
|
|
12
14
|
isAbsolute: typeof isAbsolutePath;
|
|
15
|
+
normalize: typeof joinPath;
|
|
16
|
+
resolve: typeof resolve;
|
|
17
|
+
relative: typeof relative;
|
|
18
|
+
sep: "/";
|
|
19
|
+
delimiter: ":";
|
|
13
20
|
}>;
|
|
21
|
+
export {};
|
|
@@ -54,4 +54,25 @@ export function joinPath(...paths) {
|
|
|
54
54
|
const result = `${absolute ? "/" : ""}${components.join("/")}` || ".";
|
|
55
55
|
return joined.endsWith("/") && result !== "/" ? `${result}/` : result;
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
// Portable paths have a stable root instead of a process working directory.
|
|
58
|
+
function resolve(...paths) {
|
|
59
|
+
let joined = "/";
|
|
60
|
+
for (const path of paths) {
|
|
61
|
+
assertString(path);
|
|
62
|
+
joined = path.startsWith("/") ? path : `${joined}/${path}`;
|
|
63
|
+
}
|
|
64
|
+
const normalized = joinPath(joined);
|
|
65
|
+
return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
66
|
+
}
|
|
67
|
+
function relative(from, to) {
|
|
68
|
+
const source = resolve(from).split("/").filter(Boolean);
|
|
69
|
+
const target = resolve(to).split("/").filter(Boolean);
|
|
70
|
+
let common = 0;
|
|
71
|
+
while (common < source.length && common < target.length && source[common] === target[common])
|
|
72
|
+
common++;
|
|
73
|
+
return [...Array(source.length - common).fill(".."), ...target.slice(common)].join("/");
|
|
74
|
+
}
|
|
75
|
+
export const posixPath = Object.freeze({
|
|
76
|
+
basename, dirname, extname, join: joinPath, isAbsolute: isAbsolutePath,
|
|
77
|
+
normalize: joinPath, resolve, relative, sep: "/", delimiter: ":"
|
|
78
|
+
});
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
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";
|
|
16
|
+
export * from "./fs/s3/http/index.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
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";
|
|
15
|
+
export * from "./fs/s3/http/index.js";
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
2
2
|
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
3
3
|
import type { ByteSource } from "../../contracts/io.js";
|
|
4
|
+
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;
|
|
@@ -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) {
|
|
@@ -309,7 +324,8 @@ export class DeviceFileSystem {
|
|
|
309
324
|
async #readFileSlow(path, options) {
|
|
310
325
|
const resolved = await this.#resolve(path, options);
|
|
311
326
|
if (resolved === nullPath) {
|
|
312
|
-
|
|
327
|
+
if (options.maxBytes !== Infinity)
|
|
328
|
+
nonnegative(options.maxBytes, path);
|
|
313
329
|
return new Uint8Array();
|
|
314
330
|
}
|
|
315
331
|
if (resolved === deviceDirectory)
|
|
@@ -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) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FsError } from "../contracts/errors.js";
|
|
2
2
|
export function directoryEntryLimit(options, path) {
|
|
3
3
|
options.signal?.throwIfAborted();
|
|
4
|
-
const limit = options.maxEntries;
|
|
4
|
+
const limit = options.maxEntries === Infinity ? undefined : options.maxEntries;
|
|
5
5
|
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
6
|
-
throw new FsError("EINVAL", { syscall: "readdir", path, message: "directory entry limit must be a nonnegative safe integer" });
|
|
6
|
+
throw new FsError("EINVAL", { syscall: "readdir", path, message: "directory entry limit must be a nonnegative safe integer or Infinity" });
|
|
7
7
|
}
|
|
8
8
|
return limit;
|
|
9
9
|
}
|
|
@@ -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
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";
|
|
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";
|
|
@@ -59,9 +60,10 @@ export interface ConditionalMutationBinding {
|
|
|
59
60
|
}
|
|
60
61
|
export declare function bindConditionalMutation<Result>(identityScope: object | symbol | undefined, binding: ConditionalMutationBinding, action: () => Promise<Result>): Promise<Result>;
|
|
61
62
|
export declare class MemoryFileSystem implements FileSystem {
|
|
63
|
+
#private;
|
|
62
64
|
capabilitiesFor?: NonNullable<FileSystem["capabilitiesFor"]>;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
private get identityScope();
|
|
66
|
+
get capabilities(): FileSystemCapabilities;
|
|
65
67
|
private nextInode;
|
|
66
68
|
private readonly ledger;
|
|
67
69
|
readonly _cache: MemoryCache;
|
|
@@ -70,6 +72,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
70
72
|
private readonly root;
|
|
71
73
|
private totalBytes;
|
|
72
74
|
symlinkCount: number;
|
|
75
|
+
_getAtomicView(): MemoryAtomicView | undefined;
|
|
73
76
|
constructor(options?: MemoryFileSystemOptions);
|
|
74
77
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
|
|
75
78
|
canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
|
|
@@ -173,7 +176,7 @@ export declare function utf8ByteLength(value: string): number;
|
|
|
173
176
|
export declare function isCleanAbsolutePath(path: string): boolean;
|
|
174
177
|
export declare function tryResolveMemoryDevicePath(filesystem: FileSystem, path: string, resizeCreate?: boolean): string | undefined;
|
|
175
178
|
export declare function getLastReadMemoryFileSourceRef(view: Uint8Array): Uint8Array | undefined;
|
|
176
|
-
export declare function tryReadMemoryFileViewSync(filesystem: FileSystem, path: string, maxBytes?: number, signal?: AbortSignal, captureSourceRef?: boolean): Uint8Array | undefined;
|
|
179
|
+
export declare function tryReadMemoryFileViewSync(filesystem: FileSystem, path: string, maxBytes?: number, signal?: AbortSignal, captureSourceRef?: boolean, beforeRead?: () => void): Uint8Array | undefined;
|
|
177
180
|
export declare function tryGetMemoryDirectoryEntryNamesSync(filesystem: FileSystem, path: string): ReadonlyMap<string, {
|
|
178
181
|
readonly type: "file" | "directory" | "symlink";
|
|
179
182
|
}> | undefined;
|