@poe-platform/safe-fs 0.1.629 → 0.1.631

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.
Files changed (49) hide show
  1. package/dist/safe-fs/bridge/stats.js +1 -1
  2. package/dist/safe-fs/contracts/descriptor.d.ts +31 -0
  3. package/dist/safe-fs/contracts/descriptor.js +1 -0
  4. package/dist/safe-fs/contracts/errors.d.ts +1 -1
  5. package/dist/safe-fs/contracts/errors.js +1 -1
  6. package/dist/safe-fs/contracts/filesystem.d.ts +14 -1
  7. package/dist/safe-fs/core.d.ts +3 -0
  8. package/dist/safe-fs/core.js +2 -0
  9. package/dist/safe-fs/fs/capabilities.d.ts +1 -1
  10. package/dist/safe-fs/fs/capabilities.js +5 -3
  11. package/dist/safe-fs/fs/descriptor.d.ts +21 -0
  12. package/dist/safe-fs/fs/descriptor.js +244 -0
  13. package/dist/safe-fs/fs/devices/index.d.ts +5 -1
  14. package/dist/safe-fs/fs/devices/index.js +53 -4
  15. package/dist/safe-fs/fs/memory/index.d.ts +8 -1
  16. package/dist/safe-fs/fs/memory/index.js +125 -8
  17. package/dist/safe-fs/fs/memory/limits.d.ts +1 -0
  18. package/dist/safe-fs/fs/memory/limits.js +7 -1
  19. package/dist/safe-fs/fs/mount/index.d.ts +5 -1
  20. package/dist/safe-fs/fs/mount/index.js +69 -6
  21. package/dist/safe-fs/fs/overlay/index.d.ts +2 -0
  22. package/dist/safe-fs/fs/overlay/index.js +9 -1
  23. package/dist/safe-fs/fs/quota/index.js +5 -1
  24. package/dist/safe-fs/fs/readonly/index.d.ts +2 -0
  25. package/dist/safe-fs/fs/readonly/index.js +35 -2
  26. package/dist/safe-fs/fs/real/index.d.ts +3 -0
  27. package/dist/safe-fs/fs/real/index.js +73 -3
  28. package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
  29. package/dist/safe-fs/fs/s3/filesystem.js +5 -0
  30. package/dist/safe-fs/fs/scoped.js +68 -17
  31. package/dist/safe-fs/fs/webdav/webdav.d.ts +3 -0
  32. package/dist/safe-fs/fs/webdav/webdav.js +5 -0
  33. package/dist/safe-fs/index.d.ts +3 -0
  34. package/dist/safe-fs/index.js +2 -0
  35. package/dist/safe-fs/python/emscripten.d.ts +14 -0
  36. package/dist/safe-fs/python/emscripten.js +354 -0
  37. package/dist/safe-fs/python/filesystem.d.ts +51 -0
  38. package/dist/safe-fs/python/filesystem.js +216 -0
  39. package/dist/safe-fs/python/flags.d.ts +3 -0
  40. package/dist/safe-fs/python/flags.js +14 -0
  41. package/dist/safe-fs/python/index.d.ts +7 -0
  42. package/dist/safe-fs/python/index.js +4 -0
  43. package/dist/safe-fs/python/request.d.ts +2 -0
  44. package/dist/safe-fs/python/request.js +61 -0
  45. package/dist/safe-fs/python/stat.d.ts +8 -0
  46. package/dist/safe-fs/python/stat.js +31 -0
  47. package/dist/safe-fs/xml.d.ts +3 -0
  48. package/dist/safe-fs/xml.js +41 -19
  49. package/package.json +5 -1
@@ -19,7 +19,7 @@ export function bridgeStats(stat) {
19
19
  gid: stat.gid ?? 0,
20
20
  rdev: 0,
21
21
  size: stat.size,
22
- blksize: stat.preferredIoBlockSize ?? 4096,
22
+ blksize: stat.ioBlockSize ?? stat.preferredIoBlockSize ?? 4096,
23
23
  blocks: Math.ceil(stat.size / 512),
24
24
  atimeMs: stat.atimeMs,
25
25
  mtimeMs: stat.mtimeMs,
@@ -0,0 +1,31 @@
1
+ import type { FileStat, FsOptions } from "./filesystem.js";
2
+ export interface OpenFileOptions extends FsOptions {
3
+ readonly access: "read" | "write" | "readwrite";
4
+ readonly creation?: "never" | "ifMissing" | "exclusive";
5
+ readonly truncate?: boolean;
6
+ readonly append?: boolean;
7
+ readonly mode?: number;
8
+ readonly synchronization?: "data" | "all";
9
+ }
10
+ export interface FileDescriptorCapabilities {
11
+ readonly position?: boolean;
12
+ readonly readObservation?: boolean;
13
+ readonly openTruncate?: boolean;
14
+ readonly positionedRead: boolean;
15
+ readonly positionedWrite: boolean;
16
+ readonly positionedAppendWrite?: boolean;
17
+ readonly delegateZeroLengthWrite?: boolean;
18
+ readonly truncate: boolean;
19
+ readonly synchronization: "none" | "volatile" | "storage";
20
+ }
21
+ export interface FileDescriptor {
22
+ readonly capabilities: FileDescriptorCapabilities;
23
+ getPosition?(options?: FsOptions): Promise<number>;
24
+ probeRead?(options?: FsOptions): Promise<"ready" | "blocked" | "unknown">;
25
+ stat(options?: FsOptions): Promise<FileStat>;
26
+ read(buffer: Uint8Array, position: number | null, options?: FsOptions): Promise<number>;
27
+ write(buffer: Uint8Array, position: number | null, options?: FsOptions): Promise<number>;
28
+ truncate(length: number, options?: FsOptions): Promise<void>;
29
+ sync(dataOnly: boolean, options?: FsOptions): Promise<void>;
30
+ close(): Promise<void>;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -27,7 +27,7 @@ declare const descriptions: {
27
27
  readonly EPERM: "operation not permitted";
28
28
  readonly EPIPE: "broken pipe";
29
29
  readonly EROFS: "read-only file system";
30
- readonly ESPIPE: "invalid seek";
30
+ readonly ESPIPE: "illegal seek";
31
31
  readonly ETIMEDOUT: "operation timed out";
32
32
  readonly EXDEV: "cross-device link not permitted";
33
33
  };
@@ -26,7 +26,7 @@ const descriptions = {
26
26
  EPERM: "operation not permitted",
27
27
  EPIPE: "broken pipe",
28
28
  EROFS: "read-only file system",
29
- ESPIPE: "invalid seek",
29
+ ESPIPE: "illegal seek",
30
30
  ETIMEDOUT: "operation timed out",
31
31
  EXDEV: "cross-device link not permitted",
32
32
  };
@@ -1,10 +1,13 @@
1
1
  import type { ByteSource } from "./io.js";
2
+ import type { FileDescriptor, OpenFileOptions } from "./descriptor.js";
3
+ export type { FileDescriptor, FileDescriptorCapabilities, OpenFileOptions } from "./descriptor.js";
2
4
  export type FileType = "file" | "directory" | "symlink" | "character";
3
5
  export type EntryComparison = "same" | "distinct" | "unknown";
4
6
  export interface FileStat {
5
7
  readonly type: FileType;
6
8
  readonly size: number;
7
9
  readonly allocatedBytes?: number;
10
+ readonly ioBlockSize?: number;
8
11
  readonly preferredIoBlockSize?: number;
9
12
  readonly mode: number;
10
13
  readonly mtimeMs: number;
@@ -15,6 +18,8 @@ export interface FileStat {
15
18
  readonly identityScope?: object | symbol;
16
19
  readonly ino?: number;
17
20
  readonly dev?: number;
21
+ readonly rdevMajor?: number;
22
+ readonly rdevMinor?: number;
18
23
  readonly nlink?: number;
19
24
  readonly uid?: number;
20
25
  readonly gid?: number;
@@ -24,6 +29,7 @@ export interface DirectoryEntry {
24
29
  readonly type: FileType;
25
30
  }
26
31
  export interface FileSystemCapabilities {
32
+ readonly open?: boolean;
27
33
  readonly readOnly?: boolean;
28
34
  readonly read?: boolean;
29
35
  readonly stat?: boolean;
@@ -47,6 +53,7 @@ export interface FileSystemCapabilities {
47
53
  readonly truncate?: boolean;
48
54
  readonly streamingAppend?: boolean;
49
55
  readonly randomAccessWrite?: boolean;
56
+ readonly independentWriteStreams?: boolean;
50
57
  readonly symlinks?: boolean;
51
58
  readonly hardlinks?: boolean;
52
59
  readonly permissions?: boolean;
@@ -54,6 +61,7 @@ export interface FileSystemCapabilities {
54
61
  readonly atomicRename?: boolean;
55
62
  readonly atomicFileStaging?: boolean;
56
63
  readonly atomicFileMutation?: boolean;
64
+ readonly atomicEntryRemoval?: boolean;
57
65
  readonly atomicDirectoryMetadata?: boolean;
58
66
  readonly atomicRenameNoReplace?: boolean;
59
67
  readonly snapshotRmdir?: boolean;
@@ -151,10 +159,11 @@ export interface ConditionalWriteFileOptions extends FsOptions {
151
159
  readonly append?: boolean;
152
160
  readonly mode?: number;
153
161
  }
154
- export interface ConditionalRemoveFileOptions extends FsOptions {
162
+ export interface ConditionalRemoveEntryOptions extends FsOptions {
155
163
  readonly parent: FileStat;
156
164
  readonly expected: FileStat;
157
165
  }
166
+ export type ConditionalRemoveFileOptions = ConditionalRemoveEntryOptions;
158
167
  export interface FileStagingEntry {
159
168
  readonly path: string;
160
169
  readonly stat: FileStat;
@@ -190,8 +199,10 @@ export interface PrepareDirectoryOptions extends FsOptions {
190
199
  }
191
200
  export interface FileSystem {
192
201
  writeFileConditional?(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
202
+ removeEntryConditional?(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
193
203
  removeFileConditional?(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
194
204
  readonly capabilities: FileSystemCapabilities;
205
+ open?(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
195
206
  prepareDirectory?(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
196
207
  createStagedFile?(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
197
208
  publishStagedFile?(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
@@ -211,6 +222,8 @@ export interface FileSystem {
211
222
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
212
223
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
213
224
  rm(path: string, options?: RemoveOptions): Promise<void>;
225
+ /** Atomic final-entry removal; every directory is refused, including raced replacements. */
226
+ unlink?(path: string, options?: FsOptions): Promise<void>;
214
227
  rmdir?(path: string, options?: FsOptions): Promise<void>;
215
228
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
216
229
  copyFile(source: string, destination: string, options?: CopyFileOptions): Promise<void>;
@@ -1,6 +1,8 @@
1
1
  export * from "./contracts/errors.js";
2
2
  export * from "./contracts/filesystem.js";
3
3
  export * from "./contracts/io.js";
4
+ export { openFileDescriptor } from "./fs/descriptor.js";
5
+ export type { DescriptorBackend, DescriptorOpenOptions } from "./fs/descriptor.js";
4
6
  export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
5
7
  export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
6
8
  export * from "./fs/memory/index.js";
@@ -13,6 +15,7 @@ export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
13
15
  export type { RetainedFileSystemCleanupView, RetainedFileSystemCleanupOptions } from "./fs/scoped.js";
14
16
  export * from "./fs/webdav/index.js";
15
17
  export * from "./bridge/index.js";
18
+ export * from "./python/index.js";
16
19
  export { compareEntries } from "./fs/mount/comparison.js";
17
20
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
18
21
  export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
@@ -1,6 +1,7 @@
1
1
  export * from "./contracts/errors.js";
2
2
  export * from "./contracts/filesystem.js";
3
3
  export * from "./contracts/io.js";
4
+ export { openFileDescriptor } from "./fs/descriptor.js";
4
5
  export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
5
6
  export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
6
7
  export * from "./fs/memory/index.js";
@@ -12,5 +13,6 @@ export * from "./fs/quota/index.js";
12
13
  export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
13
14
  export * from "./fs/webdav/index.js";
14
15
  export * from "./bridge/index.js";
16
+ export * from "./python/index.js";
15
17
  export { compareEntries } from "./fs/mount/comparison.js";
16
18
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
@@ -7,4 +7,4 @@ export declare function openRetainedResizeFile(filesystem: FileSystem, path: str
7
7
  export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
8
8
  export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
9
9
  export declare function ownedMutationCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
10
- export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "atomicFileMutation" | "atomicFileStaging" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
10
+ export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "atomicEntryRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
@@ -101,14 +101,14 @@ export async function openRetainedResizeFile(filesystem, path, options) {
101
101
  export function readOnlyCapabilities(capabilities) {
102
102
  const inspection = Object.fromEntries([
103
103
  "read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories",
104
- "symlinks", "streamingRead", "retainedRead",
104
+ "symlinks", "streamingRead", "open", "retainedRead",
105
105
  ].filter(name => capabilities[name] !== undefined).map(name => [name, capabilities[name]]));
106
106
  return Object.freeze({
107
107
  ...inspection, readOnly: true, write: false, append: false, exclusiveCreate: false,
108
108
  mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
109
109
  rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
110
110
  randomAccessWrite: false, hardlinks: false, permissions: false, timestamps: false,
111
- descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
111
+ descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
112
112
  atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
113
113
  });
114
114
  }
@@ -116,13 +116,15 @@ export function quotaCapabilities(capabilities) {
116
116
  const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
117
117
  const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
118
118
  const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
119
- return Object.freeze({ ...rest, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
119
+ return Object.freeze({ ...rest, open: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
120
120
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
121
121
  ...(streamingAppend === undefined ? {} : { streamingAppend }),
122
122
  });
123
123
  }
124
124
  export function ownedMutationCapabilities(filesystem, capabilities = filesystem.capabilities) {
125
125
  const unavailable = {};
126
+ if (capabilities.atomicEntryRemoval === true && (capabilities.readOnly === true || typeof filesystem.removeEntryConditional !== "function"))
127
+ unavailable.atomicEntryRemoval = false;
126
128
  if (capabilities.atomicFileMutation === true && (capabilities.readOnly === true || typeof filesystem.writeFileConditional !== "function" || typeof filesystem.removeFileConditional !== "function"))
127
129
  unavailable.atomicFileMutation = false;
128
130
  if (capabilities.atomicFileStaging === true && (capabilities.readOnly === true
@@ -0,0 +1,21 @@
1
+ import type { FileDescriptor, FileDescriptorCapabilities, FileStat, FsOptions, OpenFileOptions } from "../contracts/filesystem.js";
2
+ export interface DescriptorOpenOptions extends OpenFileOptions {
3
+ readonly creation: "never" | "ifMissing" | "exclusive";
4
+ readonly truncate: boolean;
5
+ readonly append: boolean;
6
+ readonly mode: number;
7
+ }
8
+ export interface DescriptorBackend<Resource> {
9
+ readonly resource: Resource;
10
+ readonly capabilities?: FileDescriptorCapabilities;
11
+ getPosition?(resource: Resource, options: FsOptions): Promise<number>;
12
+ probeRead?(resource: Resource, options: FsOptions): Promise<"ready" | "blocked" | "unknown">;
13
+ stat(resource: Resource, options: FsOptions): Promise<FileStat>;
14
+ read(resource: Resource, buffer: Uint8Array, position: number | null, options: FsOptions): Promise<number>;
15
+ write(resource: Resource, buffer: Uint8Array, position: number | null, options: FsOptions): Promise<number>;
16
+ truncate(resource: Resource, length: number, options: FsOptions): Promise<void>;
17
+ sync(resource: Resource, dataOnly: boolean, options: FsOptions): Promise<void>;
18
+ close(resource: Resource): Promise<void>;
19
+ }
20
+ export declare function forwardFileDescriptor(descriptor: FileDescriptor, operation: <Result>(syscall: string, options: FsOptions, action: () => Promise<Result>) => Promise<Result>, capabilities?: FileDescriptorCapabilities, snapshot?: (stat: FileStat) => FileStat): DescriptorBackend<FileDescriptor>;
21
+ export declare function openFileDescriptor<Resource>(path: string, options: OpenFileOptions, capabilities: FileDescriptorCapabilities, acquire: (options: DescriptorOpenOptions) => Promise<DescriptorBackend<Resource>>): Promise<FileDescriptor>;
@@ -0,0 +1,244 @@
1
+ import { FsError } from "../contracts/errors.js";
2
+ import { finishCleanup } from "../contracts/cleanup.js";
3
+ function admitCapabilities(path, options, capabilities) {
4
+ if (![capabilities.positionedRead, capabilities.positionedWrite, capabilities.truncate].every(value => typeof value === "boolean")
5
+ || capabilities.position !== undefined && typeof capabilities.position !== "boolean"
6
+ || capabilities.readObservation !== undefined && typeof capabilities.readObservation !== "boolean"
7
+ || capabilities.openTruncate !== undefined && typeof capabilities.openTruncate !== "boolean"
8
+ || capabilities.positionedAppendWrite !== undefined && typeof capabilities.positionedAppendWrite !== "boolean"
9
+ || capabilities.delegateZeroLengthWrite !== undefined && typeof capabilities.delegateZeroLengthWrite !== "boolean"
10
+ || !["none", "volatile", "storage"].includes(capabilities.synchronization))
11
+ throw new FsError("EINVAL", { syscall: "open", path });
12
+ if (options.truncate && !(capabilities.openTruncate ?? capabilities.truncate) || options.synchronization !== undefined && capabilities.synchronization === "none") {
13
+ throw new FsError("ENOTSUP", { syscall: "open", path });
14
+ }
15
+ }
16
+ export function forwardFileDescriptor(descriptor, operation, capabilities, snapshot = stat => ({ ...stat })) {
17
+ const retainedCapabilities = Object.freeze({ ...descriptor.capabilities });
18
+ const selected = Object.freeze({ ...(capabilities ?? retainedCapabilities) });
19
+ const getPosition = selected.position === true && retainedCapabilities.position === true ? descriptor.getPosition : undefined;
20
+ if (selected.position === true && typeof getPosition !== "function")
21
+ throw new FsError("ENOTSUP", { syscall: "getPosition" });
22
+ const probeRead = selected.readObservation === true && retainedCapabilities.readObservation === true ? descriptor.probeRead : undefined;
23
+ if (selected.readObservation === true && typeof probeRead !== "function")
24
+ throw new FsError("ENOTSUP", { syscall: "probeRead" });
25
+ return {
26
+ resource: descriptor, capabilities: selected,
27
+ ...(getPosition === undefined ? {} : {
28
+ getPosition: (retained, options) => operation("getPosition", options, () => getPosition.call(retained, options)),
29
+ }),
30
+ ...(probeRead === undefined ? {} : {
31
+ probeRead: (retained, options) => operation("probeRead", options, () => probeRead.call(retained, options)),
32
+ }),
33
+ stat: (retained, options) => operation("fstat", options, async () => snapshot(await retained.stat(options))),
34
+ read: (retained, buffer, position, options) => operation("read", options, () => retained.read(buffer, position, options)),
35
+ write: (retained, buffer, position, options) => operation("write", options, () => retained.write(buffer, position, options)),
36
+ truncate: (retained, length, options) => operation("ftruncate", options, () => retained.truncate(length, options)),
37
+ sync: (retained, dataOnly, options) => operation(dataOnly ? "fdatasync" : "fsync", options, () => retained.sync(dataOnly, options)),
38
+ close: retained => operation("close", {}, () => retained.close()),
39
+ };
40
+ }
41
+ function integer(value, syscall, path) {
42
+ if (!Number.isSafeInteger(value) || value < 0)
43
+ throw new FsError("EINVAL", { syscall, path });
44
+ }
45
+ class ManagedFileDescriptor {
46
+ capabilities;
47
+ #path;
48
+ #access;
49
+ #append;
50
+ #getPosition;
51
+ #probeRead;
52
+ #backend;
53
+ #pending = Promise.resolve();
54
+ #closing;
55
+ constructor(path, options, capabilities, backend) {
56
+ this.#path = path;
57
+ this.#access = options.access;
58
+ this.#append = options.append;
59
+ const getPosition = capabilities.position === true ? backend.getPosition : undefined;
60
+ if (capabilities.position === true && typeof getPosition !== "function")
61
+ throw new FsError("ENOTSUP", { syscall: "getPosition", path });
62
+ this.#getPosition = getPosition?.bind(backend);
63
+ const probeRead = capabilities.readObservation === true ? backend.probeRead : undefined;
64
+ if (capabilities.readObservation === true && typeof probeRead !== "function")
65
+ throw new FsError("ENOTSUP", { syscall: "probeRead", path });
66
+ this.#probeRead = probeRead?.bind(backend);
67
+ this.#backend = backend;
68
+ const positionedAppendWrite = capabilities.positionedAppendWrite === true && capabilities.positionedWrite && options.access !== "read";
69
+ this.capabilities = Object.freeze({
70
+ ...(capabilities.position === undefined ? {} : { position: capabilities.position }),
71
+ ...(capabilities.readObservation === undefined ? {} : { readObservation: capabilities.readObservation }),
72
+ ...(capabilities.openTruncate === undefined ? {} : { openTruncate: capabilities.openTruncate }),
73
+ positionedRead: capabilities.positionedRead && options.access !== "write",
74
+ positionedWrite: capabilities.positionedWrite && options.access !== "read" && (!options.append || positionedAppendWrite),
75
+ ...(capabilities.positionedAppendWrite === undefined ? {} : { positionedAppendWrite }),
76
+ ...(capabilities.delegateZeroLengthWrite === undefined ? {} : { delegateZeroLengthWrite: capabilities.delegateZeroLengthWrite && options.access !== "read" }),
77
+ truncate: capabilities.truncate && options.access !== "read",
78
+ synchronization: capabilities.synchronization,
79
+ });
80
+ }
81
+ #run(syscall, options, action) {
82
+ const signal = options.signal;
83
+ try {
84
+ signal?.throwIfAborted();
85
+ if (this.#closing || !this.#backend)
86
+ throw new FsError("EBADF", { syscall, path: this.#path });
87
+ }
88
+ catch (error) {
89
+ return Promise.reject(error);
90
+ }
91
+ const forwarded = signal === undefined ? {} : { signal };
92
+ const operation = this.#pending.then(async () => {
93
+ signal?.throwIfAborted();
94
+ try {
95
+ const result = await action(this.#backend, forwarded);
96
+ signal?.throwIfAborted();
97
+ return result;
98
+ }
99
+ catch (error) {
100
+ signal?.throwIfAborted();
101
+ throw error;
102
+ }
103
+ });
104
+ this.#pending = operation.then(() => { }, () => { });
105
+ return operation;
106
+ }
107
+ #position(buffer, position, syscall) {
108
+ if (!(buffer instanceof Uint8Array))
109
+ throw new FsError("EINVAL", { syscall, path: this.#path });
110
+ if (position !== null) {
111
+ integer(position, syscall, this.#path);
112
+ if (syscall === "write" && this.#append && this.capabilities.positionedAppendWrite !== true)
113
+ throw new FsError("EINVAL", { syscall, path: this.#path });
114
+ if (!(syscall === "read" ? this.capabilities.positionedRead : this.capabilities.positionedWrite)) {
115
+ throw new FsError("ESPIPE", { syscall, path: this.#path });
116
+ }
117
+ }
118
+ }
119
+ stat(options = {}) {
120
+ return this.#run("fstat", options, (backend, forwarded) => backend.stat(backend.resource, forwarded));
121
+ }
122
+ getPosition(options = {}) {
123
+ return this.#run("getPosition", options, async (backend, forwarded) => {
124
+ if (!this.capabilities.position || !this.#getPosition)
125
+ throw new FsError("ENOTSUP", { syscall: "getPosition", path: this.#path });
126
+ const position = await this.#getPosition(backend.resource, forwarded);
127
+ if (!Number.isSafeInteger(position) || position < 0)
128
+ throw new FsError("EIO", { syscall: "getPosition", path: this.#path });
129
+ return position;
130
+ });
131
+ }
132
+ probeRead(options = {}) {
133
+ return this.#run("probeRead", options, async (backend, forwarded) => {
134
+ if (!this.capabilities.readObservation || !this.#probeRead)
135
+ throw new FsError("ENOTSUP", { syscall: "probeRead", path: this.#path });
136
+ const readiness = await this.#probeRead(backend.resource, forwarded);
137
+ if (readiness !== "ready" && readiness !== "blocked" && readiness !== "unknown")
138
+ throw new FsError("EIO", { syscall: "probeRead", path: this.#path });
139
+ return readiness;
140
+ });
141
+ }
142
+ read(buffer, position, options = {}) {
143
+ return this.#run("read", options, async (backend, forwarded) => {
144
+ if (this.#access === "write")
145
+ throw new FsError("EBADF", { syscall: "read", path: this.#path });
146
+ this.#position(buffer, position, "read");
147
+ if (buffer.byteLength === 0)
148
+ return 0;
149
+ const count = await backend.read(backend.resource, buffer, position, forwarded);
150
+ if (!Number.isSafeInteger(count) || count < 0 || count > buffer.byteLength)
151
+ throw new FsError("EIO", { syscall: "read", path: this.#path });
152
+ return count;
153
+ });
154
+ }
155
+ write(buffer, position, options = {}) {
156
+ return this.#run("write", options, async (backend, forwarded) => {
157
+ if (this.#access === "read")
158
+ throw new FsError("EBADF", { syscall: "write", path: this.#path });
159
+ this.#position(buffer, position, "write");
160
+ if (buffer.byteLength === 0 && this.capabilities.delegateZeroLengthWrite !== true)
161
+ return 0;
162
+ const count = await backend.write(backend.resource, buffer, position, forwarded);
163
+ if (!Number.isSafeInteger(count) || count < 0 || count > buffer.byteLength)
164
+ throw new FsError("EIO", { syscall: "write", path: this.#path });
165
+ return count;
166
+ });
167
+ }
168
+ truncate(length, options = {}) {
169
+ return this.#run("ftruncate", options, async (backend, forwarded) => {
170
+ if (this.#access === "read")
171
+ throw new FsError("EBADF", { syscall: "ftruncate", path: this.#path });
172
+ integer(length, "ftruncate", this.#path);
173
+ if (!this.capabilities.truncate)
174
+ throw new FsError("ENOTSUP", { syscall: "ftruncate", path: this.#path });
175
+ await backend.truncate(backend.resource, length, forwarded);
176
+ });
177
+ }
178
+ sync(dataOnly, options = {}) {
179
+ return this.#run(dataOnly ? "fdatasync" : "fsync", options, async (backend, forwarded) => {
180
+ if (typeof dataOnly !== "boolean")
181
+ throw new FsError("EINVAL", { syscall: "fsync", path: this.#path });
182
+ if (this.capabilities.synchronization === "none")
183
+ throw new FsError("ENOTSUP", { syscall: dataOnly ? "fdatasync" : "fsync", path: this.#path });
184
+ await backend.sync(backend.resource, dataOnly, forwarded);
185
+ });
186
+ }
187
+ close() {
188
+ this.#closing ??= this.#pending.then(async () => {
189
+ try {
190
+ await this.#backend.close(this.#backend.resource);
191
+ }
192
+ finally {
193
+ this.#backend = undefined;
194
+ this.#getPosition = undefined;
195
+ this.#probeRead = undefined;
196
+ this.#pending = Promise.resolve();
197
+ }
198
+ });
199
+ return this.#closing;
200
+ }
201
+ }
202
+ export async function openFileDescriptor(path, options, capabilities, acquire) {
203
+ if (!options || typeof options !== "object")
204
+ throw new FsError("EINVAL", { syscall: "open", path });
205
+ const signal = options.signal;
206
+ signal?.throwIfAborted();
207
+ const keys = ["access", "creation", "truncate", "append", "mode", "synchronization", "signal"];
208
+ const { access, creation = "never", truncate = false, append = false, mode = 0o666, synchronization } = options;
209
+ if (Object.keys(options).some(key => !keys.includes(key))
210
+ || !["read", "write", "readwrite"].includes(access)
211
+ || !["never", "ifMissing", "exclusive"].includes(creation)
212
+ || typeof truncate !== "boolean" || typeof append !== "boolean"
213
+ || access === "read" && (truncate || append)
214
+ || synchronization !== undefined && !["data", "all"].includes(synchronization)
215
+ || !Number.isSafeInteger(mode) || mode < 0 || mode > 0o7777) {
216
+ throw new FsError("EINVAL", { syscall: "open", path });
217
+ }
218
+ const admitted = Object.freeze({ access, creation, truncate, append, mode,
219
+ ...(signal === undefined ? {} : { signal }), ...(synchronization === undefined ? {} : { synchronization }) });
220
+ const admittedCapabilities = Object.freeze({ ...capabilities });
221
+ admitCapabilities(path, admitted, admittedCapabilities);
222
+ let backend;
223
+ try {
224
+ backend = await acquire(admitted);
225
+ }
226
+ catch (error) {
227
+ signal?.throwIfAborted();
228
+ throw error;
229
+ }
230
+ try {
231
+ signal?.throwIfAborted();
232
+ const selected = Object.freeze({ ...(backend.capabilities ?? admittedCapabilities) });
233
+ signal?.throwIfAborted();
234
+ admitCapabilities(path, admitted, selected);
235
+ const descriptor = new ManagedFileDescriptor(path, admitted, selected, backend);
236
+ signal?.throwIfAborted();
237
+ return descriptor;
238
+ }
239
+ catch (error) {
240
+ await finishCleanup(() => backend.close(backend.resource), true);
241
+ signal?.throwIfAborted();
242
+ throw error;
243
+ }
244
+ }
@@ -1,4 +1,5 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
3
  import type { ByteSource } from "../../contracts/io.js";
3
4
  export declare class DeviceFileSystem implements FileSystem {
4
5
  #private;
@@ -13,6 +14,7 @@ export declare class DeviceFileSystem implements FileSystem {
13
14
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
14
15
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
15
16
  openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
17
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
16
18
  resizeFile(path: string, operation: FileResizeOperation, options?: FileResizeOptions): Promise<void>;
17
19
  openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
18
20
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
@@ -21,8 +23,10 @@ export declare class DeviceFileSystem implements FileSystem {
21
23
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
22
24
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
23
25
  rm(path: string, options?: RemoveOptions): Promise<void>;
26
+ unlink(path: string, options?: FsOptions): Promise<void>;
24
27
  rmdir(path: string, options?: FsOptions): Promise<void>;
25
28
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
29
+ removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
26
30
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
27
31
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
28
32
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { FsError, isFsError } from "../../contracts/errors.js";
2
+ import { finishCleanup } from "../../contracts/cleanup.js";
2
3
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
3
4
  import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
4
5
  import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
@@ -9,17 +10,18 @@ const views = new WeakMap();
9
10
  const deviceCapabilities = Object.freeze({
10
11
  readOnly: false, read: true, stat: true, realpath: true, access: true, readdir: false,
11
12
  write: true, append: true, exclusiveCreate: true, streamingRead: true, retainedRead: true,
12
- streamingWrite: true, streamingAppend: true, copy: true, exclusiveCopy: true,
13
+ streamingWrite: true, streamingAppend: true, independentWriteStreams: true, copy: true, exclusiveCopy: true,
13
14
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
14
15
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
15
16
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
16
- atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
17
+ open: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
17
18
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
18
19
  });
19
20
  function globalCapabilities(filesystem) {
20
21
  const capabilities = { readOnly: false };
21
22
  const optional = {
22
- atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
23
+ open: ["open"],
24
+ atomicEntryRemoval: ["removeEntryConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
23
25
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
24
26
  streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
25
27
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
@@ -124,7 +126,7 @@ export class DeviceFileSystem {
124
126
  return deviceCapabilities;
125
127
  if (resolved === deviceDirectory)
126
128
  return { ...deviceCapabilities, readdir: true, write: false, append: false,
127
- exclusiveCreate: false, streamingWrite: false, streamingAppend: false, descriptorWriteStream: false,
129
+ exclusiveCreate: false, streamingWrite: false, streamingAppend: false, independentWriteStreams: false, descriptorWriteStream: false,
128
130
  retainedRead: false, retainedResize: false, streamingRead: false, copy: false, exclusiveCopy: false };
129
131
  const query = this.#filesystem.capabilitiesFor;
130
132
  options.signal?.throwIfAborted();
@@ -134,6 +136,8 @@ export class DeviceFileSystem {
134
136
  options.signal?.throwIfAborted();
135
137
  const capabilities = observed.retainedResize === true ? retainedResizeCapabilities(this.#filesystem, observed) : observed;
136
138
  const unavailable = {};
139
+ if (typeof this.#filesystem.open !== "function")
140
+ unavailable.open = false;
137
141
  if (typeof this.#filesystem.readStream !== "function")
138
142
  unavailable.streamingRead = false;
139
143
  if (typeof this.#filesystem.writeStream !== "function") {
@@ -229,6 +233,37 @@ export class DeviceFileSystem {
229
233
  async close() { closed = true; },
230
234
  };
231
235
  }
236
+ async open(path, options) {
237
+ options?.signal?.throwIfAborted();
238
+ if (!options || typeof options !== "object")
239
+ throw new FsError("EINVAL", { syscall: "open", path });
240
+ const resolved = await this.#resolve(path, options, options.creation !== "exclusive");
241
+ options.signal?.throwIfAborted();
242
+ if (resolved === nullPath || resolved === deviceDirectory)
243
+ throw new FsError("ENOTSUP", { syscall: "open", path });
244
+ const query = this.#filesystem.capabilitiesFor;
245
+ options.signal?.throwIfAborted();
246
+ const capabilities = query && options.creation !== "exclusive" ? await Reflect.apply(query, this.#filesystem, [path, options]) : this.#filesystem.capabilities;
247
+ options.signal?.throwIfAborted();
248
+ const open = this.#filesystem.open;
249
+ options.signal?.throwIfAborted();
250
+ if (capabilities.readOnly === true && (options.access === "write" || options.access === "readwrite" || options.creation === "ifMissing" || options.creation === "exclusive" || options.truncate === true || options.append === true))
251
+ throw new FsError("EROFS", { syscall: "open", path });
252
+ if (capabilities.open === false || typeof open !== "function")
253
+ throw new FsError("ENOTSUP", { syscall: "open", path });
254
+ let descriptor;
255
+ try {
256
+ descriptor = await Reflect.apply(open, this.#filesystem, [path, options]);
257
+ options.signal?.throwIfAborted();
258
+ return descriptor;
259
+ }
260
+ catch (error) {
261
+ if (descriptor)
262
+ await finishCleanup(() => descriptor.close(), true);
263
+ options.signal?.throwIfAborted();
264
+ throw error;
265
+ }
266
+ }
232
267
  async resizeFile(path, operation, options = {}) {
233
268
  await this.#mutable(path, options);
234
269
  const capabilities = await this.capabilitiesFor(path, options);
@@ -343,6 +378,13 @@ export class DeviceFileSystem {
343
378
  await this.#mutable(path, options, false);
344
379
  await this.#filesystem.rm(path, options);
345
380
  }
381
+ async unlink(path, options = {}) {
382
+ await this.#mutable(path, options, false);
383
+ if (!this.#filesystem.unlink)
384
+ throw new FsError("ENOTSUP", { syscall: "unlink", path });
385
+ options.signal?.throwIfAborted();
386
+ await this.#filesystem.unlink(path, options);
387
+ }
346
388
  async rmdir(path, options = {}) {
347
389
  const resolved = await this.#resolve(path, options, false);
348
390
  if (resolved === nullPath)
@@ -360,6 +402,13 @@ export class DeviceFileSystem {
360
402
  throw new FsError("ENOTSUP", { path });
361
403
  return this.#filesystem.writeFileConditional(path, data, options);
362
404
  }
405
+ async removeEntryConditional(path, options) {
406
+ await this.#mutable(path, options, false);
407
+ await requireOwnedMutation(this.#filesystem, path, "atomicEntryRemoval", options);
408
+ if (!this.#filesystem.removeEntryConditional)
409
+ throw new FsError("ENOTSUP", { path });
410
+ await this.#filesystem.removeEntryConditional(path, options);
411
+ }
363
412
  async removeFileConditional(path, options) {
364
413
  await this.#mutable(path, options, false);
365
414
  await requireOwnedMutation(this.#filesystem, path, "atomicFileMutation", options);