@poe-platform/safe-fs 0.1.154 → 0.1.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/safe-fs/contracts/filesystem.d.ts +7 -0
- package/dist/safe-fs/fs/capabilities.d.ts +3 -1
- package/dist/safe-fs/fs/capabilities.js +27 -1
- package/dist/safe-fs/fs/memory/index.d.ts +3 -1
- package/dist/safe-fs/fs/memory/index.js +50 -0
- package/dist/safe-fs/fs/mount/index.d.ts +2 -1
- package/dist/safe-fs/fs/mount/index.js +18 -2
- package/dist/safe-fs/fs/overlay/index.d.ts +2 -0
- package/dist/safe-fs/fs/overlay/index.js +25 -1
- package/dist/safe-fs/fs/quota/index.js +8 -3
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +6 -3
- package/dist/safe-fs/fs/real/index.d.ts +2 -1
- package/dist/safe-fs/fs/real/index.js +112 -1
- package/package.json +1 -1
|
@@ -52,12 +52,18 @@ export interface FileSystemCapabilities {
|
|
|
52
52
|
readonly atomicRename?: boolean;
|
|
53
53
|
readonly snapshotRmdir?: boolean;
|
|
54
54
|
readonly streamingRead?: boolean;
|
|
55
|
+
readonly retainedRead?: boolean;
|
|
55
56
|
readonly streamingWrite?: boolean;
|
|
56
57
|
readonly [capability: string]: boolean | undefined;
|
|
57
58
|
}
|
|
58
59
|
export interface FsOptions {
|
|
59
60
|
readonly signal?: AbortSignal;
|
|
60
61
|
}
|
|
62
|
+
export interface FileReadHandle {
|
|
63
|
+
stat(options?: FsOptions): Promise<FileStat>;
|
|
64
|
+
read(position: number, maxBytes: number, options?: FsOptions): Promise<Uint8Array>;
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
}
|
|
61
67
|
export interface ReadFileOptions extends FsOptions {
|
|
62
68
|
readonly maxBytes?: number;
|
|
63
69
|
}
|
|
@@ -96,6 +102,7 @@ export interface ReadStreamOptions extends FsOptions {
|
|
|
96
102
|
}
|
|
97
103
|
export interface FileSystem {
|
|
98
104
|
readonly capabilities: FileSystemCapabilities;
|
|
105
|
+
openReadFile?(path: string, options?: FsOptions): Promise<FileReadHandle>;
|
|
99
106
|
canonicalizeMissingTarget?(path: string, options?: FsOptions): string | undefined;
|
|
100
107
|
capabilitiesFor?(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
|
|
101
108
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { FileSystemCapabilities } from "../contracts/filesystem.js";
|
|
1
|
+
import type { FileReadHandle, FileSystem, FileSystemCapabilities, FsOptions } from "../contracts/filesystem.js";
|
|
2
|
+
export declare function retainedReadCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
|
|
3
|
+
export declare function openRetainedReadFile(filesystem: FileSystem, path: string, options: FsOptions): Promise<FileReadHandle>;
|
|
2
4
|
export declare function requireCapabilities(...values: readonly (boolean | undefined)[]): boolean | undefined;
|
|
3
5
|
export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
4
6
|
export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
@@ -1,10 +1,36 @@
|
|
|
1
|
+
import { FsError } from "../contracts/errors.js";
|
|
2
|
+
import { finishCleanup } from "../contracts/cleanup.js";
|
|
3
|
+
export function retainedReadCapabilities(filesystem, capabilities = filesystem.capabilities) {
|
|
4
|
+
return typeof filesystem.openReadFile === "function" ? capabilities : { ...capabilities, retainedRead: false };
|
|
5
|
+
}
|
|
6
|
+
export async function openRetainedReadFile(filesystem, path, options) {
|
|
7
|
+
let handle;
|
|
8
|
+
try {
|
|
9
|
+
options.signal?.throwIfAborted();
|
|
10
|
+
if (typeof filesystem.openReadFile !== "function")
|
|
11
|
+
throw new FsError("ENOTSUP", { syscall: "openReadFile", path });
|
|
12
|
+
const capabilities = await filesystem.capabilitiesFor?.(path, options) ?? filesystem.capabilities;
|
|
13
|
+
options.signal?.throwIfAborted();
|
|
14
|
+
if (retainedReadCapabilities(filesystem, capabilities).retainedRead !== true)
|
|
15
|
+
throw new FsError("ENOTSUP", { syscall: "openReadFile", path });
|
|
16
|
+
handle = await filesystem.openReadFile(path, options);
|
|
17
|
+
options.signal?.throwIfAborted();
|
|
18
|
+
return handle;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (handle)
|
|
22
|
+
await finishCleanup(() => handle.close(), true);
|
|
23
|
+
options.signal?.throwIfAborted();
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
1
27
|
export function requireCapabilities(...values) {
|
|
2
28
|
return values.some(value => value === false) ? false : values.every(value => value === true) ? true : undefined;
|
|
3
29
|
}
|
|
4
30
|
export function readOnlyCapabilities(capabilities) {
|
|
5
31
|
const inspection = Object.fromEntries([
|
|
6
32
|
"read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories",
|
|
7
|
-
"symlinks", "streamingRead",
|
|
33
|
+
"symlinks", "streamingRead", "retainedRead",
|
|
8
34
|
].filter(name => capabilities[name] !== undefined).map(name => [name, capabilities[name]]));
|
|
9
35
|
return Object.freeze({
|
|
10
36
|
...inspection, readOnly: true, write: false, append: false, exclusiveCreate: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export declare class MemoryFileSystem implements FileSystem {
|
|
4
4
|
readonly capabilities: Readonly<{
|
|
@@ -31,6 +31,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
31
31
|
timestamps: true;
|
|
32
32
|
atomicRename: true;
|
|
33
33
|
streamingRead: true;
|
|
34
|
+
retainedRead: true;
|
|
34
35
|
streamingWrite: true;
|
|
35
36
|
}>;
|
|
36
37
|
private readonly identityScope;
|
|
@@ -75,6 +76,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
75
76
|
chmod(path: string, mode: number, options?: FsOptions): Promise<void>;
|
|
76
77
|
utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
|
|
77
78
|
truncate(path: string, length?: number, options?: FsOptions): Promise<void>;
|
|
79
|
+
openReadFile(path: string, options?: FsOptions): Promise<FileReadHandle>;
|
|
78
80
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
79
81
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
80
82
|
}
|
|
@@ -62,6 +62,7 @@ export class MemoryFileSystem {
|
|
|
62
62
|
timestamps: true,
|
|
63
63
|
atomicRename: true,
|
|
64
64
|
streamingRead: true,
|
|
65
|
+
retainedRead: true,
|
|
65
66
|
streamingWrite: true,
|
|
66
67
|
});
|
|
67
68
|
identityScope = Symbol();
|
|
@@ -512,6 +513,55 @@ export class MemoryFileSystem {
|
|
|
512
513
|
node.data = data;
|
|
513
514
|
this.changed(node);
|
|
514
515
|
}
|
|
516
|
+
async openReadFile(path, options = {}) {
|
|
517
|
+
options.signal?.throwIfAborted();
|
|
518
|
+
const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openReadFile", path }); };
|
|
519
|
+
const owner = ownedStores.get(this);
|
|
520
|
+
if (!owner || Object.getPrototypeOf(this) !== MemoryFileSystem.prototype
|
|
521
|
+
|| Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root)
|
|
522
|
+
unsupported();
|
|
523
|
+
for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
|
|
524
|
+
"file", "resolve", "permission", "validatePath", "fail", "snapshot", "integer"]) {
|
|
525
|
+
const descriptor = Object.getOwnPropertyDescriptor(this, name)
|
|
526
|
+
?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
|
|
527
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
|
|
528
|
+
unsupported();
|
|
529
|
+
}
|
|
530
|
+
if (Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead !== true)
|
|
531
|
+
unsupported();
|
|
532
|
+
let node = this.file(path, "openReadFile");
|
|
533
|
+
this.permission(node, 4, "openReadFile", path);
|
|
534
|
+
const snapshot = this.snapshot.bind(this);
|
|
535
|
+
const integer = this.integer.bind(this);
|
|
536
|
+
let closing;
|
|
537
|
+
const current = (signal, syscall) => {
|
|
538
|
+
signal?.throwIfAborted();
|
|
539
|
+
if (!node)
|
|
540
|
+
throw new FsError("EBADF", { syscall, path });
|
|
541
|
+
return node;
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
async stat(options = {}) {
|
|
545
|
+
return snapshot(current(options.signal, "fstat"));
|
|
546
|
+
},
|
|
547
|
+
async read(position, maxBytes, options = {}) {
|
|
548
|
+
const file = current(options.signal, "read");
|
|
549
|
+
integer(position, "read", path);
|
|
550
|
+
integer(maxBytes, "read", path);
|
|
551
|
+
if (maxBytes === 0 || maxBytes > Number.MAX_SAFE_INTEGER - position) {
|
|
552
|
+
throw new FsError("EINVAL", { syscall: "read", path });
|
|
553
|
+
}
|
|
554
|
+
const bytes = file.data.slice(position, position + maxBytes);
|
|
555
|
+
file.atimeMs = Date.now();
|
|
556
|
+
return bytes;
|
|
557
|
+
},
|
|
558
|
+
close() {
|
|
559
|
+
node = undefined;
|
|
560
|
+
closing ??= Promise.resolve();
|
|
561
|
+
return closing;
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
515
565
|
async *readStream(path, options = {}) {
|
|
516
566
|
options.signal?.throwIfAborted();
|
|
517
567
|
const start = options.start ?? 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export interface MountFileSystemOptions {
|
|
4
4
|
readonly root: FileSystem;
|
|
@@ -11,6 +11,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
11
11
|
private select;
|
|
12
12
|
capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
|
|
13
13
|
private protected;
|
|
14
|
+
openReadFile(path: string, options?: FsOptions): Promise<FileReadHandle>;
|
|
14
15
|
private error;
|
|
15
16
|
private operation;
|
|
16
17
|
private components;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FsError, isFsError, toFsError } from "../../contracts/errors.js";
|
|
2
2
|
import { readBytes } from "../../contracts/io.js";
|
|
3
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
4
|
-
import { readOnlyCapabilities } from "../capabilities.js";
|
|
4
|
+
import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities } from "../capabilities.js";
|
|
5
5
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
6
6
|
import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
7
7
|
import { compareIdentity } from "./identity.js";
|
|
@@ -79,6 +79,7 @@ export class MountFileSystem {
|
|
|
79
79
|
const streaming = (capability, method) => all(capability, [method]) ? true : mounts.some(({ backend }) => backend.capabilities[capability] !== false && typeof backend[method] === "function") ? undefined : false;
|
|
80
80
|
const streamingRead = streaming("streamingRead", "readStream");
|
|
81
81
|
const streamingWrite = streaming("streamingWrite", "writeStream");
|
|
82
|
+
const retainedRead = streaming("retainedRead", "openReadFile");
|
|
82
83
|
const append = all("append") ? true
|
|
83
84
|
: mounts.every(({ backend }) => backend.capabilities.append === false) ? false : undefined;
|
|
84
85
|
const common = (capability) => {
|
|
@@ -110,6 +111,7 @@ export class MountFileSystem {
|
|
|
110
111
|
atomicRename: mounts.length === 1 && all("atomicRename"),
|
|
111
112
|
...(streamingRead === undefined ? {} : { streamingRead }),
|
|
112
113
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
114
|
+
...(retainedRead === undefined ? {} : { retainedRead }),
|
|
113
115
|
});
|
|
114
116
|
}
|
|
115
117
|
select(path) {
|
|
@@ -118,8 +120,10 @@ export class MountFileSystem {
|
|
|
118
120
|
async capabilitiesFor(path, options = {}) {
|
|
119
121
|
return this.operation("capabilitiesFor", path, options, async () => {
|
|
120
122
|
const location = await this.resolve(path, options, { allowMissing: true });
|
|
121
|
-
const
|
|
123
|
+
const declared = await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
122
124
|
?? location.mount.backend.capabilities;
|
|
125
|
+
const capabilities = location.synthetic ? { ...declared, retainedRead: false }
|
|
126
|
+
: retainedReadCapabilities(location.mount.backend, declared);
|
|
123
127
|
if (location.synthetic)
|
|
124
128
|
return readOnlyCapabilities(capabilities);
|
|
125
129
|
if (this.mounts.length === 1 || capabilities.readOnly === true)
|
|
@@ -136,6 +140,18 @@ export class MountFileSystem {
|
|
|
136
140
|
protected(path) {
|
|
137
141
|
return path === "/" || this.mounts.some((mount) => within(path, mount.path));
|
|
138
142
|
}
|
|
143
|
+
async openReadFile(path, options = {}) {
|
|
144
|
+
try {
|
|
145
|
+
options.signal?.throwIfAborted();
|
|
146
|
+
const location = await this.resolve(path, options);
|
|
147
|
+
if (location.synthetic)
|
|
148
|
+
fail("EISDIR");
|
|
149
|
+
return await openRetainedReadFile(location.mount.backend, location.local, options);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw error ? this.error(error, "openReadFile", path, options) : error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
139
155
|
error(error, syscall, path, options, dest) {
|
|
140
156
|
if (options.signal?.aborted && error === options.signal.reason)
|
|
141
157
|
return error;
|
|
@@ -18,6 +18,8 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
18
18
|
private queue;
|
|
19
19
|
constructor(options: OverlayFileSystemOptions);
|
|
20
20
|
private run;
|
|
21
|
+
capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
|
|
22
|
+
openReadFile(path: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
|
|
21
23
|
private writable;
|
|
22
24
|
private permission;
|
|
23
25
|
private maybeStat;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { platform } from "#safe-fs-platform";
|
|
2
|
-
import { requireCapabilities } from "../capabilities.js";
|
|
2
|
+
import { openRetainedReadFile, requireCapabilities, retainedReadCapabilities } from "../capabilities.js";
|
|
3
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
4
4
|
import { collectBytes, readBytes } from "../../contracts/io.js";
|
|
5
5
|
import { FsError, toFsError } from "../../contracts/errors.js";
|
|
@@ -79,6 +79,9 @@ export class OverlayFileSystem {
|
|
|
79
79
|
const readable = [this.#upper, this.#lower].map((backend) => typeof backend.readStream === "function" ? backend.capabilities.streamingRead : false);
|
|
80
80
|
const streamingRead = readable.every((capability) => capability === true) ? true
|
|
81
81
|
: readable.every((capability) => capability === false) ? false : undefined;
|
|
82
|
+
const retained = [this.#upper, this.#lower].map((backend) => retainedReadCapabilities(backend).retainedRead);
|
|
83
|
+
const retainedRead = retained.every((capability) => capability === true) ? true
|
|
84
|
+
: retained.every((capability) => capability === false) ? false : undefined;
|
|
82
85
|
const streamingWrite = writable && this.#upper.capabilities.streamingWrite === true
|
|
83
86
|
&& this.#upper.capabilities.streamingRead === true
|
|
84
87
|
&& typeof this.#upper.writeStream === "function" && typeof this.#upper.readStream === "function"
|
|
@@ -122,6 +125,7 @@ export class OverlayFileSystem {
|
|
|
122
125
|
permissions: writable && this.#upper.capabilities.permissions === true && typeof this.#upper.chmod === "function",
|
|
123
126
|
timestamps: writable && this.#upper.capabilities.timestamps === true && typeof this.#upper.utimes === "function",
|
|
124
127
|
...(streamingRead === undefined ? {} : { streamingRead }),
|
|
128
|
+
...(retainedRead === undefined ? {} : { retainedRead }),
|
|
125
129
|
...(effectiveStreamingWrite === undefined ? {} : { streamingWrite: effectiveStreamingWrite }),
|
|
126
130
|
});
|
|
127
131
|
Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
|
|
@@ -148,6 +152,26 @@ export class OverlayFileSystem {
|
|
|
148
152
|
release();
|
|
149
153
|
}
|
|
150
154
|
}
|
|
155
|
+
capabilitiesFor(path, options = {}) {
|
|
156
|
+
return this.run(options, async () => {
|
|
157
|
+
const location = await this.resolve(path, options, true, true);
|
|
158
|
+
const backend = location.entry?.backend ?? this.#upper;
|
|
159
|
+
const capabilities = await backend.capabilitiesFor?.(location.path, options) ?? backend.capabilities;
|
|
160
|
+
options.signal?.throwIfAborted();
|
|
161
|
+
const { retainedRead: ignoredRetainedRead, ...composed } = this.capabilities;
|
|
162
|
+
const retainedRead = retainedReadCapabilities(backend, capabilities).retainedRead;
|
|
163
|
+
return Object.freeze({ ...composed, ...(retainedRead === undefined ? {} : { retainedRead }) });
|
|
164
|
+
}, false);
|
|
165
|
+
}
|
|
166
|
+
openReadFile(path, options = {}) {
|
|
167
|
+
return this.run(options, async () => {
|
|
168
|
+
const entry = await this.required(path, options);
|
|
169
|
+
if (entry.stat.type !== "file")
|
|
170
|
+
fail("EISDIR", path);
|
|
171
|
+
this.permission(entry, 4);
|
|
172
|
+
return openRetainedReadFile(entry.backend, entry.path, options);
|
|
173
|
+
}, false);
|
|
174
|
+
}
|
|
151
175
|
writable(path) {
|
|
152
176
|
if (this.#upper.capabilities.readOnly)
|
|
153
177
|
fail("EROFS", path);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FsError } from "../../contracts/errors.js";
|
|
2
|
-
import { quotaCapabilities } from "../capabilities.js";
|
|
2
|
+
import { openRetainedReadFile, quotaCapabilities, retainedReadCapabilities } from "../capabilities.js";
|
|
3
3
|
import { admitDirectoryEntries } from "../directory-admission.js";
|
|
4
4
|
export class FileSystemQuotaError extends Error {
|
|
5
5
|
maxBytes;
|
|
@@ -173,9 +173,14 @@ export function withFileSystemQuota(fs, options) {
|
|
|
173
173
|
if (property === "canonicalizeMissingTarget")
|
|
174
174
|
return undefined;
|
|
175
175
|
if (property === "capabilities")
|
|
176
|
-
return quotaCapabilities(fs
|
|
176
|
+
return quotaCapabilities(retainedReadCapabilities(fs));
|
|
177
177
|
if (property === "capabilitiesFor")
|
|
178
|
-
return async (path, fsOptions) =>
|
|
178
|
+
return async (path, fsOptions) => {
|
|
179
|
+
const capabilities = await fs.capabilitiesFor?.(path, fsOptions) ?? fs.capabilities;
|
|
180
|
+
return quotaCapabilities(retainedReadCapabilities(fs, capabilities));
|
|
181
|
+
};
|
|
182
|
+
if (property === "openReadFile")
|
|
183
|
+
return (path, fsOptions = {}) => openRetainedReadFile(fs, path, fsOptions);
|
|
179
184
|
const replacement = Reflect.get(mutations, property);
|
|
180
185
|
if (typeof replacement === "function")
|
|
181
186
|
return replacement;
|
|
@@ -5,6 +5,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
|
|
|
5
5
|
constructor(filesystem: FileSystem);
|
|
6
6
|
get capabilities(): FileSystemCapabilities;
|
|
7
7
|
capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
|
|
8
|
+
openReadFile(path: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
|
|
8
9
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
9
10
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
10
11
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
@@ -2,7 +2,7 @@ import { ACCESS_MODES } from "../../contracts/filesystem.js";
|
|
|
2
2
|
import { FsError } from "../../contracts/errors.js";
|
|
3
3
|
import { readBytes } from "../../contracts/io.js";
|
|
4
4
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
5
|
-
import { readOnlyCapabilities } from "../capabilities.js";
|
|
5
|
+
import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities } from "../capabilities.js";
|
|
6
6
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
7
7
|
function readOnly(syscall, path, dest) {
|
|
8
8
|
throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
@@ -29,7 +29,7 @@ export class ReadOnlyFileSystem {
|
|
|
29
29
|
registerEntryView(this, async (path) => ({ filesystem: this.#filesystem, path, readOnly: true }));
|
|
30
30
|
const streamingRead = typeof filesystem.readStream === "function" ? filesystem.capabilities.streamingRead : false;
|
|
31
31
|
this.#capabilities = readOnlyCapabilities({
|
|
32
|
-
...filesystem
|
|
32
|
+
...retainedReadCapabilities(filesystem),
|
|
33
33
|
readOnly: true,
|
|
34
34
|
append: false,
|
|
35
35
|
symlinks: filesystem.capabilities.symlinks === true && typeof filesystem.readlink === "function",
|
|
@@ -46,7 +46,10 @@ export class ReadOnlyFileSystem {
|
|
|
46
46
|
}
|
|
47
47
|
async capabilitiesFor(path, options) {
|
|
48
48
|
const capabilities = await this.#filesystem.capabilitiesFor?.(path, options) ?? this.#filesystem.capabilities;
|
|
49
|
-
return readOnlyCapabilities(capabilities);
|
|
49
|
+
return readOnlyCapabilities(retainedReadCapabilities(this.#filesystem, capabilities));
|
|
50
|
+
}
|
|
51
|
+
openReadFile(path, options = {}) {
|
|
52
|
+
return openRetainedReadFile(this.#filesystem, path, options);
|
|
50
53
|
}
|
|
51
54
|
async readFile(path, options) {
|
|
52
55
|
return new Uint8Array(await this.#filesystem.readFile(path, options));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
1
|
+
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
2
2
|
export interface RealFileSystemOptions {
|
|
3
3
|
/** An existing, absolute host directory. It is never created implicitly. */
|
|
4
4
|
readonly root: string;
|
|
@@ -69,6 +69,7 @@ export declare class RealFileSystem implements FileSystem {
|
|
|
69
69
|
chmod(path: string, mode: number, options?: FsOptions): Promise<void>;
|
|
70
70
|
utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
|
|
71
71
|
truncate(path: string, length?: number, options?: FsOptions): Promise<void>;
|
|
72
|
+
openReadFile(path: string, options?: FsOptions): Promise<FileReadHandle>;
|
|
72
73
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
73
74
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
74
75
|
}
|
|
@@ -87,7 +87,7 @@ export class RealFileSystem {
|
|
|
87
87
|
rename: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
|
|
88
88
|
streamingAppend: true, randomAccessWrite: true,
|
|
89
89
|
readOnly: false, symlinks: true, hardlinks: true, permissions: true,
|
|
90
|
-
timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true,
|
|
90
|
+
timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true,
|
|
91
91
|
});
|
|
92
92
|
configuredRoot;
|
|
93
93
|
rootPromise;
|
|
@@ -474,6 +474,116 @@ export class RealFileSystem {
|
|
|
474
474
|
}
|
|
475
475
|
});
|
|
476
476
|
}
|
|
477
|
+
async openReadFile(path, options = {}) {
|
|
478
|
+
options.signal?.throwIfAborted();
|
|
479
|
+
const assertStock = () => {
|
|
480
|
+
const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openReadFile", path }); };
|
|
481
|
+
if (Object.getPrototypeOf(this) !== RealFileSystem.prototype)
|
|
482
|
+
unsupported();
|
|
483
|
+
for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
|
|
484
|
+
"root", "absoluteTarget", "walk", "path"]) {
|
|
485
|
+
const descriptor = Object.getOwnPropertyDescriptor(this, name)
|
|
486
|
+
?? Object.getOwnPropertyDescriptor(RealFileSystem.prototype, name);
|
|
487
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== realImplementation[name]?.value)
|
|
488
|
+
unsupported();
|
|
489
|
+
}
|
|
490
|
+
if (Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead !== true)
|
|
491
|
+
unsupported();
|
|
492
|
+
};
|
|
493
|
+
assertStock();
|
|
494
|
+
const failure = (error, syscall) => error
|
|
495
|
+
? new FsError(nativeError(error).code, { syscall, path }) : error;
|
|
496
|
+
let resource;
|
|
497
|
+
try {
|
|
498
|
+
const target = await this.path(path, options);
|
|
499
|
+
options.signal?.throwIfAborted();
|
|
500
|
+
assertStock();
|
|
501
|
+
resource = await native.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
502
|
+
options.signal?.throwIfAborted();
|
|
503
|
+
const stats = await resource.stat();
|
|
504
|
+
options.signal?.throwIfAborted();
|
|
505
|
+
assertStock();
|
|
506
|
+
if (stats.isDirectory())
|
|
507
|
+
throw new FsError("EISDIR");
|
|
508
|
+
if (!stats.isFile())
|
|
509
|
+
throw new FsError("ENOTSUP");
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
await finishCleanup(async () => { await resource?.close(); }, true);
|
|
513
|
+
options.signal?.throwIfAborted();
|
|
514
|
+
throw failure(error, "openReadFile");
|
|
515
|
+
}
|
|
516
|
+
const handle = resource;
|
|
517
|
+
const pending = new Set();
|
|
518
|
+
let accepting = true;
|
|
519
|
+
let closing;
|
|
520
|
+
const assertOpen = (options, syscall) => {
|
|
521
|
+
options.signal?.throwIfAborted();
|
|
522
|
+
if (!accepting)
|
|
523
|
+
throw new FsError("EBADF", { syscall, path });
|
|
524
|
+
};
|
|
525
|
+
const perform = async (options, syscall, action) => {
|
|
526
|
+
assertOpen(options, syscall);
|
|
527
|
+
let settled;
|
|
528
|
+
const admitted = new Promise(resolve => { settled = resolve; });
|
|
529
|
+
pending.add(admitted);
|
|
530
|
+
try {
|
|
531
|
+
const value = await action();
|
|
532
|
+
options.signal?.throwIfAborted();
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
options.signal?.throwIfAborted();
|
|
537
|
+
throw failure(error, syscall);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
pending.delete(admitted);
|
|
541
|
+
settled();
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
return {
|
|
545
|
+
stat(options = {}) {
|
|
546
|
+
return perform(options, "fstat", async () => fileStat(await handle.stat()));
|
|
547
|
+
},
|
|
548
|
+
async read(position, maxBytes, options = {}) {
|
|
549
|
+
assertOpen(options, "read");
|
|
550
|
+
try {
|
|
551
|
+
integer(position);
|
|
552
|
+
integer(maxBytes, 1);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
throw failure(error, "read");
|
|
556
|
+
}
|
|
557
|
+
if (maxBytes > Number.MAX_SAFE_INTEGER - position)
|
|
558
|
+
throw new FsError("EINVAL", { syscall: "read", path });
|
|
559
|
+
return perform(options, "read", async () => {
|
|
560
|
+
let bytes;
|
|
561
|
+
try {
|
|
562
|
+
bytes = new Uint8Array(maxBytes);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
throw new FsError("EFBIG");
|
|
566
|
+
}
|
|
567
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.byteLength, position);
|
|
568
|
+
if (!Number.isSafeInteger(bytesRead) || bytesRead < 0 || bytesRead > maxBytes)
|
|
569
|
+
throw new FsError("EIO");
|
|
570
|
+
return bytes.slice(0, bytesRead);
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
close() {
|
|
574
|
+
accepting = false;
|
|
575
|
+
closing ??= Promise.all([...pending]).then(async () => {
|
|
576
|
+
try {
|
|
577
|
+
await handle.close();
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
throw failure(error, "close");
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
return closing;
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
477
587
|
async *readStream(path, options = {}) {
|
|
478
588
|
const syscall = "readStream";
|
|
479
589
|
let handle;
|
|
@@ -565,6 +675,7 @@ export class RealFileSystem {
|
|
|
565
675
|
});
|
|
566
676
|
}
|
|
567
677
|
}
|
|
678
|
+
const realImplementation = Object.getOwnPropertyDescriptors(RealFileSystem.prototype);
|
|
568
679
|
/** Construct and validate an existing root before returning the backend. */
|
|
569
680
|
export async function createRealFileSystem(options) {
|
|
570
681
|
const filesystem = new RealFileSystem(options);
|