@poe-platform/safe-fs 0.1.511 → 0.1.513
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/safe-fs/bridge/stats.js +1 -1
- package/dist/safe-fs/contracts/errors.d.ts +1 -0
- package/dist/safe-fs/contracts/errors.js +1 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +22 -2
- package/dist/safe-fs/fs/capabilities.d.ts +4 -2
- package/dist/safe-fs/fs/capabilities.js +80 -9
- package/dist/safe-fs/fs/devices/index.d.ts +4 -3
- package/dist/safe-fs/fs/devices/index.js +75 -16
- package/dist/safe-fs/fs/devices/path.d.ts +1 -1
- package/dist/safe-fs/fs/devices/path.js +111 -7
- package/dist/safe-fs/fs/memory/index.d.ts +5 -2
- package/dist/safe-fs/fs/memory/index.js +107 -10
- package/dist/safe-fs/fs/mount/index.d.ts +4 -3
- package/dist/safe-fs/fs/mount/index.js +66 -14
- package/dist/safe-fs/fs/overlay/index.d.ts +3 -3
- package/dist/safe-fs/fs/overlay/index.js +9 -3
- package/dist/safe-fs/fs/path-namespace.d.ts +4 -0
- package/dist/safe-fs/fs/path-namespace.js +63 -0
- package/dist/safe-fs/fs/quota/index.js +142 -6
- package/dist/safe-fs/fs/readonly/index.d.ts +4 -3
- package/dist/safe-fs/fs/readonly/index.js +14 -1
- package/dist/safe-fs/fs/real/index.d.ts +3 -2
- package/dist/safe-fs/fs/real/index.js +177 -8
- package/dist/safe-fs/fs/scoped.js +111 -13
- package/dist/safe-fs/native/fs-seek/linux-x64-glibc.node +0 -0
- package/dist/safe-fs/native/fs-seek/loader.d.ts +5 -0
- package/dist/safe-fs/native/fs-seek/loader.mjs +120 -0
- package/dist/safe-fs/native/fs-seek/manifest.json +46 -0
- package/dist/safe-fs/node/native-seek.d.ts +3 -0
- package/dist/safe-fs/node/native-seek.js +54 -0
- package/package.json +7 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FsError } from "../../contracts/errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
3
|
+
import { openRetainedReadFile, openRetainedResizeFile, quotaCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
|
|
3
4
|
import { admitDirectoryEntries } from "../directory-admission.js";
|
|
4
5
|
export class FileSystemQuotaError extends Error {
|
|
5
6
|
maxBytes;
|
|
@@ -9,8 +10,25 @@ export class FileSystemQuotaError extends Error {
|
|
|
9
10
|
this.name = "FileSystemQuotaError";
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
function completeIdentity(stat) {
|
|
14
|
+
return (typeof stat.identityScope === "symbol" || typeof stat.identityScope === "object" && stat.identityScope !== null)
|
|
15
|
+
&& [stat.dev, stat.ino].every(value => typeof value === "number" && Number.isSafeInteger(value) && value >= 0);
|
|
16
|
+
}
|
|
17
|
+
function namespaceMetadata(metadata, signal) {
|
|
18
|
+
if (!signal)
|
|
19
|
+
return metadata;
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
22
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
23
|
+
metadata.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
24
|
+
if (signal.aborted)
|
|
25
|
+
abort();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function usedBytes(fs, limits, options, change, retained = change?.retained === true) {
|
|
13
29
|
let total = 0;
|
|
30
|
+
let retainedTotal = 0n;
|
|
31
|
+
let shrinkCredited = false;
|
|
14
32
|
let possibleAliases = 0;
|
|
15
33
|
let remaining = limits.maxScanEntries;
|
|
16
34
|
const pending = [{ path: "/", depth: 0 }];
|
|
@@ -20,7 +38,8 @@ async function usedBytes(fs, limits, options, change) {
|
|
|
20
38
|
if (!directory)
|
|
21
39
|
break;
|
|
22
40
|
const signal = options?.signal;
|
|
23
|
-
const
|
|
41
|
+
const listing = fs.readdir(directory.path, { ...options, ...(signal ? { signal } : {}), maxEntries: remaining });
|
|
42
|
+
const entries = await (retained ? namespaceMetadata(listing, signal) : listing);
|
|
24
43
|
options?.signal?.throwIfAborted();
|
|
25
44
|
const count = entries.length;
|
|
26
45
|
admitDirectoryEntries(count, remaining, directory.path);
|
|
@@ -41,7 +60,32 @@ async function usedBytes(fs, limits, options, change) {
|
|
|
41
60
|
pending.push({ path, depth });
|
|
42
61
|
}
|
|
43
62
|
else {
|
|
44
|
-
const
|
|
63
|
+
const metadata = fs.lstat(path, options);
|
|
64
|
+
const stat = await (retained ? namespaceMetadata(metadata, signal) : metadata);
|
|
65
|
+
if (retained) {
|
|
66
|
+
options?.signal?.throwIfAborted();
|
|
67
|
+
if (!Number.isSafeInteger(stat.size) || stat.size < 0)
|
|
68
|
+
throw new FsError("EIO", { syscall: "lstat", path, message: "invalid quota entry size" });
|
|
69
|
+
retainedTotal += BigInt(stat.size);
|
|
70
|
+
if (!change || stat.type !== "file")
|
|
71
|
+
continue;
|
|
72
|
+
const known = completeIdentity(stat);
|
|
73
|
+
const same = known && change.stat.identityScope === stat.identityScope && change.stat.dev === stat.dev && change.stat.ino === stat.ino;
|
|
74
|
+
const nextBytes = change.stat.size + change.delta;
|
|
75
|
+
if (same) {
|
|
76
|
+
possibleAliases++;
|
|
77
|
+
retainedTotal += BigInt(Math.max(0, nextBytes - stat.size));
|
|
78
|
+
if (!shrinkCredited && change.delta < 0) {
|
|
79
|
+
retainedTotal -= BigInt(Math.max(0, Math.min(stat.size, change.stat.size) - nextBytes));
|
|
80
|
+
shrinkCredited = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (!known) {
|
|
84
|
+
possibleAliases++;
|
|
85
|
+
retainedTotal += BigInt(Math.max(0, change.delta, nextBytes - stat.size));
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
45
89
|
total += stat.size;
|
|
46
90
|
if (!change || stat.type !== "file")
|
|
47
91
|
continue;
|
|
@@ -66,6 +110,11 @@ async function usedBytes(fs, limits, options, change) {
|
|
|
66
110
|
}
|
|
67
111
|
}
|
|
68
112
|
}
|
|
113
|
+
if (retained) {
|
|
114
|
+
if (change && possibleAliases === 0)
|
|
115
|
+
retainedTotal += BigInt(Math.max(0, change.delta));
|
|
116
|
+
return Number(retainedTotal);
|
|
117
|
+
}
|
|
69
118
|
if (change && possibleAliases === 0)
|
|
70
119
|
total += Math.max(0, change.delta);
|
|
71
120
|
return total;
|
|
@@ -98,6 +147,24 @@ export function withFileSystemQuota(fs, options) {
|
|
|
98
147
|
queue = result.catch(() => undefined);
|
|
99
148
|
return result;
|
|
100
149
|
};
|
|
150
|
+
const resizeBackend = new Proxy(Object.create(fs), {
|
|
151
|
+
get(_target, property) {
|
|
152
|
+
const original = Reflect.get(fs, property);
|
|
153
|
+
if (property === "openResizeFile" && typeof original === "function")
|
|
154
|
+
return async (path, resizeOptions) => {
|
|
155
|
+
resizeOptions.signal?.throwIfAborted();
|
|
156
|
+
if (resizeOptions.create) {
|
|
157
|
+
const total = await usedBytes(fs, scanLimits, resizeOptions, undefined, true);
|
|
158
|
+
resizeOptions.signal?.throwIfAborted();
|
|
159
|
+
if (total > options.maxBytes)
|
|
160
|
+
throw new FileSystemQuotaError(options.maxBytes);
|
|
161
|
+
}
|
|
162
|
+
resizeOptions.signal?.throwIfAborted();
|
|
163
|
+
return Reflect.apply(original, fs, [path, resizeOptions]);
|
|
164
|
+
};
|
|
165
|
+
return typeof original === "function" ? original.bind(fs) : original;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
101
168
|
const assertDelta = async (path, nextBytes, fsOptions) => {
|
|
102
169
|
fsOptions?.signal?.throwIfAborted();
|
|
103
170
|
let current;
|
|
@@ -116,6 +183,75 @@ export function withFileSystemQuota(fs, options) {
|
|
|
116
183
|
throw new FileSystemQuotaError(options.maxBytes);
|
|
117
184
|
};
|
|
118
185
|
const mutations = {
|
|
186
|
+
openResizeFile(path, resizeOptions = {}) {
|
|
187
|
+
return mutate(async () => {
|
|
188
|
+
let retained;
|
|
189
|
+
try {
|
|
190
|
+
retained = await openRetainedResizeFile(resizeBackend, path, resizeOptions);
|
|
191
|
+
const pinned = { ...await retained.stat(resizeOptions) };
|
|
192
|
+
resizeOptions.signal?.throwIfAborted();
|
|
193
|
+
if (pinned.type !== "file" || !completeIdentity(pinned))
|
|
194
|
+
throw new FsError("ENOTSUP", { syscall: "openResizeFile", path, message: "quota requires complete retained file identity" });
|
|
195
|
+
if (!Number.isSafeInteger(pinned.size) || pinned.size < 0)
|
|
196
|
+
throw new FsError("EIO", { syscall: "stat", path, message: "invalid retained file size" });
|
|
197
|
+
const handle = retained;
|
|
198
|
+
const seekEnd = handle.seekEnd;
|
|
199
|
+
resizeOptions.signal?.throwIfAborted();
|
|
200
|
+
let pending = Promise.resolve();
|
|
201
|
+
let closing;
|
|
202
|
+
const admit = (operation, operationOptions) => {
|
|
203
|
+
if (closing)
|
|
204
|
+
return Promise.reject(new FsError("EBADF", { path }));
|
|
205
|
+
const result = mutate(async () => {
|
|
206
|
+
operationOptions?.signal?.throwIfAborted();
|
|
207
|
+
const value = await operation();
|
|
208
|
+
operationOptions?.signal?.throwIfAborted();
|
|
209
|
+
return value;
|
|
210
|
+
});
|
|
211
|
+
pending = result.catch(() => undefined);
|
|
212
|
+
return result;
|
|
213
|
+
};
|
|
214
|
+
const current = async (operationOptions) => {
|
|
215
|
+
const stat = { ...await handle.stat(operationOptions) };
|
|
216
|
+
operationOptions?.signal?.throwIfAborted();
|
|
217
|
+
if (stat.type !== "file" || !completeIdentity(stat) || stat.identityScope !== pinned.identityScope || stat.dev !== pinned.dev || stat.ino !== pinned.ino
|
|
218
|
+
|| !Number.isSafeInteger(stat.size) || stat.size < 0)
|
|
219
|
+
throw new FsError("EIO", { syscall: "stat", path, message: "invalid retained quota identity or size" });
|
|
220
|
+
return stat;
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
stat(operationOptions) { return admit(() => current(operationOptions), operationOptions); },
|
|
224
|
+
...(typeof seekEnd === "function" ? {
|
|
225
|
+
seekEnd(operationOptions) {
|
|
226
|
+
return admit(() => Reflect.apply(seekEnd, handle, [operationOptions]), operationOptions);
|
|
227
|
+
},
|
|
228
|
+
} : {}),
|
|
229
|
+
truncate(length, operationOptions) {
|
|
230
|
+
return admit(async () => {
|
|
231
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
232
|
+
throw new FsError("EINVAL", { syscall: "truncate", path });
|
|
233
|
+
const stat = await current(operationOptions);
|
|
234
|
+
const projected = await usedBytes(fs, scanLimits, operationOptions, { path, stat, delta: length - stat.size, retained: true });
|
|
235
|
+
operationOptions?.signal?.throwIfAborted();
|
|
236
|
+
if (projected > options.maxBytes)
|
|
237
|
+
throw new FileSystemQuotaError(options.maxBytes);
|
|
238
|
+
await handle.truncate(length, operationOptions);
|
|
239
|
+
}, operationOptions);
|
|
240
|
+
},
|
|
241
|
+
close() {
|
|
242
|
+
closing ??= pending.then(() => handle.close());
|
|
243
|
+
return closing;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (retained)
|
|
249
|
+
await finishCleanup(() => retained.close(), true);
|
|
250
|
+
resizeOptions.signal?.throwIfAborted();
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
},
|
|
119
255
|
writeFile(path, data, writeOptions) {
|
|
120
256
|
return mutate(async () => {
|
|
121
257
|
const append = writeOptions?.flag === "a" || writeOptions?.flag === "ax";
|
|
@@ -183,11 +319,11 @@ export function withFileSystemQuota(fs, options) {
|
|
|
183
319
|
if (property === "canonicalizeMissingTarget")
|
|
184
320
|
return undefined;
|
|
185
321
|
if (property === "capabilities")
|
|
186
|
-
return quotaCapabilities(retainedReadCapabilities(fs));
|
|
322
|
+
return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs)));
|
|
187
323
|
if (property === "capabilitiesFor")
|
|
188
324
|
return async (path, fsOptions) => {
|
|
189
325
|
const capabilities = await fs.capabilitiesFor?.(path, fsOptions) ?? fs.capabilities;
|
|
190
|
-
return quotaCapabilities(retainedReadCapabilities(fs, capabilities));
|
|
326
|
+
return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)));
|
|
191
327
|
};
|
|
192
328
|
if (property === "openReadFile")
|
|
193
329
|
return (path, fsOptions = {}) => openRetainedReadFile(fs, path, fsOptions);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ByteSource } from "../../contracts/io.js";
|
|
2
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileResizeHandle, FileStat, OpenReadFileOptions, OpenResizeFileOptions, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
3
|
export declare class ReadOnlyFileSystem implements FileSystem {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(filesystem: FileSystem);
|
|
6
6
|
get capabilities(): FileSystemCapabilities;
|
|
7
|
-
capabilitiesFor(path: string, options?:
|
|
8
|
-
openReadFile(path: string, options?:
|
|
7
|
+
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
8
|
+
openReadFile(path: string, options?: OpenReadFileOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
|
|
9
9
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
10
10
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
11
11
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
@@ -29,5 +29,6 @@ export declare class ReadOnlyFileSystem implements FileSystem {
|
|
|
29
29
|
chmod(path: string, _mode: number, _options?: FsOptions): Promise<void>;
|
|
30
30
|
utimes(path: string, _atimeMs: number, _mtimeMs: number, _options?: FsOptions): Promise<void>;
|
|
31
31
|
truncate(path: string, _length?: number, _options?: FsOptions): Promise<void>;
|
|
32
|
+
openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
32
33
|
}
|
|
33
34
|
export declare function createReadOnlyFileSystem(filesystem: FileSystem): ReadOnlyFileSystem;
|
|
@@ -4,14 +4,16 @@ import { readBytes } from "../../contracts/io.js";
|
|
|
4
4
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
5
5
|
import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities } from "../capabilities.js";
|
|
6
6
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
7
|
+
import { pathNamespace, readOnlyPathNamespace } from "../path-namespace.js";
|
|
7
8
|
function readOnly(syscall, path, dest) {
|
|
8
9
|
throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
9
10
|
}
|
|
10
11
|
function snapshotStat(stat) {
|
|
11
|
-
const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
12
|
+
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
12
13
|
return {
|
|
13
14
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
14
15
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
16
|
+
...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
|
|
15
17
|
...(birthtimeMs === undefined ? {} : { birthtimeMs }),
|
|
16
18
|
...(identityScope === undefined ? {} : { identityScope }),
|
|
17
19
|
...(ino === undefined ? {} : { ino }),
|
|
@@ -26,6 +28,10 @@ export class ReadOnlyFileSystem {
|
|
|
26
28
|
#capabilities;
|
|
27
29
|
constructor(filesystem) {
|
|
28
30
|
this.#filesystem = filesystem;
|
|
31
|
+
if (Reflect.has(filesystem, pathNamespace))
|
|
32
|
+
Object.defineProperty(this, pathNamespace, {
|
|
33
|
+
get: () => readOnlyPathNamespace(Reflect.get(filesystem, pathNamespace)),
|
|
34
|
+
});
|
|
29
35
|
registerEntryView(this, async (path) => ({ filesystem: this.#filesystem, path, readOnly: true }));
|
|
30
36
|
const streamingRead = typeof filesystem.readStream === "function" ? filesystem.capabilities.streamingRead : false;
|
|
31
37
|
this.#capabilities = readOnlyCapabilities({
|
|
@@ -45,6 +51,9 @@ export class ReadOnlyFileSystem {
|
|
|
45
51
|
return this.#capabilities;
|
|
46
52
|
}
|
|
47
53
|
async capabilitiesFor(path, options) {
|
|
54
|
+
options?.signal?.throwIfAborted();
|
|
55
|
+
if (options?.create !== undefined)
|
|
56
|
+
readOnly("capabilitiesFor", path);
|
|
48
57
|
const capabilities = await this.#filesystem.capabilitiesFor?.(path, options) ?? this.#filesystem.capabilities;
|
|
49
58
|
return readOnlyCapabilities(retainedReadCapabilities(this.#filesystem, capabilities));
|
|
50
59
|
}
|
|
@@ -138,6 +147,10 @@ export class ReadOnlyFileSystem {
|
|
|
138
147
|
async truncate(path, _length, _options) {
|
|
139
148
|
readOnly("truncate", path);
|
|
140
149
|
}
|
|
150
|
+
async openResizeFile(path, options = {}) {
|
|
151
|
+
options.signal?.throwIfAborted();
|
|
152
|
+
readOnly("openResizeFile", path);
|
|
153
|
+
}
|
|
141
154
|
}
|
|
142
155
|
export function createReadOnlyFileSystem(filesystem) {
|
|
143
156
|
return new ReadOnlyFileSystem(filesystem);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
1
|
+
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, OpenReadFileOptions, OpenResizeFileOptions, 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,7 +69,8 @@ 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
|
-
|
|
72
|
+
openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
73
|
+
openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
73
74
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
74
75
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
75
76
|
}
|
|
@@ -3,6 +3,7 @@ import * as native from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { nativeAllocatedBytes } from "./allocation.js";
|
|
5
5
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
6
|
+
import { callNativeSeekEnd, loadNativeSeekBinding } from "../../node/native-seek.js";
|
|
6
7
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
7
8
|
import { FsError, collectBytes, isErrnoCode, toByteSource, toFsError, validatePath, } from "../../contracts/index.js";
|
|
8
9
|
function fileType(stats) {
|
|
@@ -16,9 +17,11 @@ function fileType(stats) {
|
|
|
16
17
|
}
|
|
17
18
|
function fileStat(stats) {
|
|
18
19
|
const allocatedBytes = nativeAllocatedBytes(stats.blocks, process.platform);
|
|
20
|
+
const preferredIoBlockSize = stats.blksize;
|
|
19
21
|
return {
|
|
20
22
|
type: fileType(stats), size: stats.size, mode: stats.mode,
|
|
21
23
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
24
|
+
...(Number.isSafeInteger(preferredIoBlockSize) && preferredIoBlockSize > 0 ? { preferredIoBlockSize } : {}),
|
|
22
25
|
atimeMs: stats.atimeMs, mtimeMs: stats.mtimeMs, ctimeMs: stats.ctimeMs,
|
|
23
26
|
birthtimeMs: stats.birthtimeMs, ino: stats.ino, dev: stats.dev,
|
|
24
27
|
...(Number.isSafeInteger(stats.dev) && stats.dev >= 0 && Number.isSafeInteger(stats.ino) && stats.ino >= 0
|
|
@@ -87,7 +90,7 @@ export class RealFileSystem {
|
|
|
87
90
|
rename: true, atomicRenameNoReplace: false, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
|
|
88
91
|
streamingAppend: true, randomAccessWrite: true,
|
|
89
92
|
readOnly: false, symlinks: true, hardlinks: true, permissions: true,
|
|
90
|
-
timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true,
|
|
93
|
+
timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true, retainedResize: true,
|
|
91
94
|
});
|
|
92
95
|
configuredRoot;
|
|
93
96
|
rootPromise;
|
|
@@ -136,7 +139,13 @@ export class RealFileSystem {
|
|
|
136
139
|
while (pending.length > 0) {
|
|
137
140
|
options.signal?.throwIfAborted();
|
|
138
141
|
const { name: component, fromLink } = pending.shift();
|
|
139
|
-
if (component === ""
|
|
142
|
+
if (component === "")
|
|
143
|
+
continue;
|
|
144
|
+
if ((component === "." || component === "..") && !options.checkTarget) {
|
|
145
|
+
await native.lstat(`${current}/.`);
|
|
146
|
+
options.signal?.throwIfAborted();
|
|
147
|
+
}
|
|
148
|
+
if (component === ".")
|
|
140
149
|
continue;
|
|
141
150
|
if (component === "..") {
|
|
142
151
|
if (current === root) {
|
|
@@ -148,6 +157,11 @@ export class RealFileSystem {
|
|
|
148
157
|
}
|
|
149
158
|
continue;
|
|
150
159
|
}
|
|
160
|
+
if (options.createFile && pending.length > 0 && pending.every((part) => part.name === "")) {
|
|
161
|
+
await native.lstat(`${current}/.`);
|
|
162
|
+
options.signal?.throwIfAborted();
|
|
163
|
+
throw new FsError("EISDIR");
|
|
164
|
+
}
|
|
151
165
|
const candidate = join(current, component);
|
|
152
166
|
let stats;
|
|
153
167
|
try {
|
|
@@ -476,20 +490,23 @@ export class RealFileSystem {
|
|
|
476
490
|
}
|
|
477
491
|
});
|
|
478
492
|
}
|
|
479
|
-
async
|
|
493
|
+
async openResizeFile(path, options = {}) {
|
|
480
494
|
options.signal?.throwIfAborted();
|
|
481
495
|
const assertStock = () => {
|
|
482
|
-
const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "
|
|
496
|
+
const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openResizeFile", path }); };
|
|
483
497
|
if (Object.getPrototypeOf(this) !== RealFileSystem.prototype)
|
|
484
498
|
unsupported();
|
|
485
|
-
for (const name of ["
|
|
499
|
+
for (const name of ["openResizeFile", "truncate", "writeFile", "writeStream", "appendFile", "stat", "lstat", "realpath", "access",
|
|
486
500
|
"root", "absoluteTarget", "walk", "path"]) {
|
|
487
501
|
const descriptor = Object.getOwnPropertyDescriptor(this, name)
|
|
488
502
|
?? Object.getOwnPropertyDescriptor(RealFileSystem.prototype, name);
|
|
489
503
|
if (!descriptor || !("value" in descriptor) || descriptor.value !== realImplementation[name]?.value)
|
|
490
504
|
unsupported();
|
|
491
505
|
}
|
|
492
|
-
|
|
506
|
+
const capabilities = Object.getOwnPropertyDescriptor(this, "capabilities")?.value;
|
|
507
|
+
if (capabilities?.readOnly === true)
|
|
508
|
+
throw new FsError("EROFS", { syscall: "openResizeFile", path });
|
|
509
|
+
if (capabilities?.retainedResize !== true)
|
|
493
510
|
unsupported();
|
|
494
511
|
};
|
|
495
512
|
assertStock();
|
|
@@ -497,10 +514,19 @@ export class RealFileSystem {
|
|
|
497
514
|
? new FsError(nativeError(error).code, { syscall, path }) : error;
|
|
498
515
|
let resource;
|
|
499
516
|
try {
|
|
500
|
-
|
|
517
|
+
if (options.mode !== undefined)
|
|
518
|
+
integer(options.mode);
|
|
519
|
+
if (options.create !== undefined && typeof options.create !== "boolean")
|
|
520
|
+
throw new FsError("EINVAL");
|
|
521
|
+
const target = await this.path(path, {
|
|
522
|
+
...options, ...(options.create === true ? { missing: "final" } : {}),
|
|
523
|
+
deferTrailingSeparator: true, createFile: options.create === true,
|
|
524
|
+
});
|
|
501
525
|
options.signal?.throwIfAborted();
|
|
502
526
|
assertStock();
|
|
503
|
-
|
|
527
|
+
const flags = constants.O_WRONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
|
|
528
|
+
| (options.create === true ? constants.O_CREAT : 0);
|
|
529
|
+
resource = await native.open(target, flags, options.mode ?? 0o666);
|
|
504
530
|
options.signal?.throwIfAborted();
|
|
505
531
|
const stats = await resource.stat();
|
|
506
532
|
options.signal?.throwIfAborted();
|
|
@@ -513,6 +539,127 @@ export class RealFileSystem {
|
|
|
513
539
|
catch (error) {
|
|
514
540
|
await finishCleanup(async () => { await resource?.close(); }, true);
|
|
515
541
|
options.signal?.throwIfAborted();
|
|
542
|
+
throw failure(error, "openResizeFile");
|
|
543
|
+
}
|
|
544
|
+
const handle = resource;
|
|
545
|
+
const pending = new Set();
|
|
546
|
+
let accepting = true;
|
|
547
|
+
let closing;
|
|
548
|
+
const assertOpen = (options, syscall) => {
|
|
549
|
+
options.signal?.throwIfAborted();
|
|
550
|
+
if (!accepting)
|
|
551
|
+
throw new FsError("EBADF", { syscall, path });
|
|
552
|
+
};
|
|
553
|
+
const perform = async (options, syscall, action) => {
|
|
554
|
+
assertOpen(options, syscall);
|
|
555
|
+
let settled;
|
|
556
|
+
const admitted = new Promise(resolve => { settled = resolve; });
|
|
557
|
+
pending.add(admitted);
|
|
558
|
+
try {
|
|
559
|
+
const value = await action();
|
|
560
|
+
options.signal?.throwIfAborted();
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
options.signal?.throwIfAborted();
|
|
565
|
+
throw failure(error, syscall);
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
pending.delete(admitted);
|
|
569
|
+
settled();
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
return {
|
|
573
|
+
stat(options = {}) {
|
|
574
|
+
return perform(options, "fstat", async () => fileStat(await handle.stat()));
|
|
575
|
+
},
|
|
576
|
+
async truncate(length, options = {}) {
|
|
577
|
+
assertOpen(options, "ftruncate");
|
|
578
|
+
try {
|
|
579
|
+
integer(length);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
throw failure(error, "ftruncate");
|
|
583
|
+
}
|
|
584
|
+
return perform(options, "ftruncate", async () => { await handle.truncate(length); });
|
|
585
|
+
},
|
|
586
|
+
async seekEnd(options = {}) {
|
|
587
|
+
const signal = options.signal;
|
|
588
|
+
const operationOptions = signal === undefined ? {} : { signal };
|
|
589
|
+
assertOpen(operationOptions, "lseek");
|
|
590
|
+
try {
|
|
591
|
+
const binding = await loadNativeSeekBinding(signal);
|
|
592
|
+
assertOpen(operationOptions, "lseek");
|
|
593
|
+
const seek = binding.seekEnd;
|
|
594
|
+
assertOpen(operationOptions, "lseek");
|
|
595
|
+
if (typeof seek !== "function")
|
|
596
|
+
throw new FsError("EIO");
|
|
597
|
+
const descriptor = handle.fd;
|
|
598
|
+
assertOpen(operationOptions, "lseek");
|
|
599
|
+
return await perform(operationOptions, "lseek", () => callNativeSeekEnd(binding, seek, descriptor));
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
signal?.throwIfAborted();
|
|
603
|
+
throw failure(error, "lseek");
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
close() {
|
|
607
|
+
accepting = false;
|
|
608
|
+
closing ??= Promise.all([...pending]).then(async () => {
|
|
609
|
+
try {
|
|
610
|
+
await handle.close();
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
throw failure(error, "close");
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
return closing;
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
async openReadFile(path, options = {}) {
|
|
621
|
+
const signal = options.signal;
|
|
622
|
+
signal?.throwIfAborted();
|
|
623
|
+
const assertStock = () => {
|
|
624
|
+
const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openReadFile", path }); };
|
|
625
|
+
if (Object.getPrototypeOf(this) !== RealFileSystem.prototype)
|
|
626
|
+
unsupported();
|
|
627
|
+
for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
|
|
628
|
+
"root", "absoluteTarget", "walk", "path"]) {
|
|
629
|
+
const descriptor = Object.getOwnPropertyDescriptor(this, name)
|
|
630
|
+
?? Object.getOwnPropertyDescriptor(RealFileSystem.prototype, name);
|
|
631
|
+
if (!descriptor || !("value" in descriptor) || descriptor.value !== realImplementation[name]?.value)
|
|
632
|
+
unsupported();
|
|
633
|
+
}
|
|
634
|
+
if (Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead !== true)
|
|
635
|
+
unsupported();
|
|
636
|
+
};
|
|
637
|
+
assertStock();
|
|
638
|
+
const failure = (error, syscall) => error
|
|
639
|
+
? new FsError(nativeError(error).code, { syscall, path }) : error;
|
|
640
|
+
let resource;
|
|
641
|
+
let directory = false;
|
|
642
|
+
try {
|
|
643
|
+
const allowDirectory = options.allowDirectory === true;
|
|
644
|
+
signal?.throwIfAborted();
|
|
645
|
+
const target = await this.path(path, signal === undefined ? {} : { signal });
|
|
646
|
+
signal?.throwIfAborted();
|
|
647
|
+
assertStock();
|
|
648
|
+
resource = await native.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
649
|
+
signal?.throwIfAborted();
|
|
650
|
+
const stats = await resource.stat();
|
|
651
|
+
signal?.throwIfAborted();
|
|
652
|
+
assertStock();
|
|
653
|
+
directory = stats.isDirectory();
|
|
654
|
+
if (directory && !allowDirectory)
|
|
655
|
+
throw new FsError("EISDIR");
|
|
656
|
+
if (!directory && !stats.isFile())
|
|
657
|
+
throw new FsError("ENOTSUP");
|
|
658
|
+
signal?.throwIfAborted();
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
await finishCleanup(async () => { await resource?.close(); }, true);
|
|
662
|
+
signal?.throwIfAborted();
|
|
516
663
|
throw failure(error, "openReadFile");
|
|
517
664
|
}
|
|
518
665
|
const handle = resource;
|
|
@@ -558,6 +705,8 @@ export class RealFileSystem {
|
|
|
558
705
|
}
|
|
559
706
|
if (maxBytes > Number.MAX_SAFE_INTEGER - position)
|
|
560
707
|
throw new FsError("EINVAL", { syscall: "read", path });
|
|
708
|
+
if (directory)
|
|
709
|
+
throw new FsError("EISDIR", { syscall: "read", path });
|
|
561
710
|
return perform(options, "read", async () => {
|
|
562
711
|
let bytes;
|
|
563
712
|
try {
|
|
@@ -572,6 +721,26 @@ export class RealFileSystem {
|
|
|
572
721
|
return bytes.slice(0, bytesRead);
|
|
573
722
|
});
|
|
574
723
|
},
|
|
724
|
+
async seekEnd(options = {}) {
|
|
725
|
+
const signal = options.signal;
|
|
726
|
+
const operationOptions = signal === undefined ? {} : { signal };
|
|
727
|
+
assertOpen(operationOptions, "lseek");
|
|
728
|
+
try {
|
|
729
|
+
const binding = await loadNativeSeekBinding(signal);
|
|
730
|
+
assertOpen(operationOptions, "lseek");
|
|
731
|
+
const seek = binding.seekEnd;
|
|
732
|
+
assertOpen(operationOptions, "lseek");
|
|
733
|
+
if (typeof seek !== "function")
|
|
734
|
+
throw new FsError("EIO");
|
|
735
|
+
const descriptor = handle.fd;
|
|
736
|
+
assertOpen(operationOptions, "lseek");
|
|
737
|
+
return await perform(operationOptions, "lseek", () => callNativeSeekEnd(binding, seek, descriptor));
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
signal?.throwIfAborted();
|
|
741
|
+
throw failure(error, "lseek");
|
|
742
|
+
}
|
|
743
|
+
},
|
|
575
744
|
close() {
|
|
576
745
|
accepting = false;
|
|
577
746
|
closing ??= Promise.all([...pending]).then(async () => {
|