@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
@@ -1,24 +1,29 @@
1
1
  import { ACCESS_MODES } from "../../contracts/filesystem.js";
2
2
  import { FsError } from "../../contracts/errors.js";
3
3
  import { readBytes } from "../../contracts/io.js";
4
+ import { finishCleanup } from "../../contracts/cleanup.js";
4
5
  import { compareEntries, registerEntryView } from "../mount/comparison.js";
5
6
  import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities } from "../capabilities.js";
6
7
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
8
+ import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
7
9
  import { pathNamespace, readOnlyPathNamespace } from "../path-namespace.js";
8
10
  function readOnly(syscall, path, dest) {
9
11
  throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
10
12
  }
11
13
  function snapshotStat(stat) {
12
- const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, nlink, uid, gid } = stat;
14
+ const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
13
15
  return {
14
16
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
15
17
  ...(revision === undefined ? {} : { revision }),
16
18
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
19
+ ...(ioBlockSize === undefined ? {} : { ioBlockSize }),
17
20
  ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
18
21
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
19
22
  ...(identityScope === undefined ? {} : { identityScope }),
20
23
  ...(ino === undefined ? {} : { ino }),
21
24
  ...(dev === undefined ? {} : { dev }),
25
+ ...(rdevMajor === undefined ? {} : { rdevMajor }),
26
+ ...(rdevMinor === undefined ? {} : { rdevMinor }),
22
27
  ...(nlink === undefined ? {} : { nlink }),
23
28
  ...(uid === undefined ? {} : { uid }),
24
29
  ...(gid === undefined ? {} : { gid }),
@@ -37,6 +42,7 @@ export class ReadOnlyFileSystem {
37
42
  const streamingRead = typeof filesystem.readStream === "function" ? filesystem.capabilities.streamingRead : false;
38
43
  this.#capabilities = readOnlyCapabilities({
39
44
  ...retainedReadCapabilities(filesystem),
45
+ ...(typeof filesystem.open === "function" ? {} : { open: false }),
40
46
  readOnly: true,
41
47
  append: false,
42
48
  symlinks: filesystem.capabilities.symlinks === true && typeof filesystem.readlink === "function",
@@ -56,7 +62,34 @@ export class ReadOnlyFileSystem {
56
62
  if (options?.create !== undefined)
57
63
  readOnly("capabilitiesFor", path);
58
64
  const capabilities = await this.#filesystem.capabilitiesFor?.(path, options) ?? this.#filesystem.capabilities;
59
- return readOnlyCapabilities(retainedReadCapabilities(this.#filesystem, capabilities));
65
+ return readOnlyCapabilities({ ...retainedReadCapabilities(this.#filesystem, capabilities), ...(typeof this.#filesystem.open === "function" ? {} : { open: false }) });
66
+ }
67
+ async open(path, options) {
68
+ options?.signal?.throwIfAborted();
69
+ if (options && (options.access === "write" || options.access === "readwrite" || options.truncate === true || options.append === true
70
+ || options.creation === "ifMissing" || options.creation === "exclusive"))
71
+ readOnly("open", path);
72
+ return openFileDescriptor(path, options, {
73
+ positionedRead: true, positionedWrite: false, truncate: false, synchronization: "none",
74
+ }, async (admitted) => {
75
+ const capabilities = await this.#filesystem.capabilitiesFor?.(path, admitted) ?? this.#filesystem.capabilities;
76
+ if (!this.#filesystem.open || capabilities.open === false)
77
+ throw new FsError("ENOTSUP", { syscall: "open", path });
78
+ admitted.signal?.throwIfAborted();
79
+ const descriptor = await this.#filesystem.open(path, admitted);
80
+ try {
81
+ return forwardFileDescriptor(descriptor, async (_syscall, forwarded, action) => {
82
+ forwarded.signal?.throwIfAborted();
83
+ return action();
84
+ }, { ...descriptor.capabilities,
85
+ ...(descriptor.capabilities.openTruncate === undefined ? {} : { openTruncate: false }),
86
+ positionedWrite: false, truncate: false, synchronization: "none" }, snapshotStat);
87
+ }
88
+ catch (error) {
89
+ await finishCleanup(() => descriptor.close(), true);
90
+ throw error;
91
+ }
92
+ });
60
93
  }
61
94
  openReadFile(path, options = {}) {
62
95
  return openRetainedReadFile(this.#filesystem, path, options);
@@ -1,3 +1,4 @@
1
+ import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
1
2
  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
3
  export interface RealFileSystemOptions {
3
4
  /** An existing, absolute host directory. It is never created implicitly. */
@@ -49,6 +50,7 @@ export declare class RealFileSystem implements FileSystem {
49
50
  private path;
50
51
  private operation;
51
52
  private protectRoot;
53
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
52
54
  private protectTerminal;
53
55
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
54
56
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
@@ -59,6 +61,7 @@ export declare class RealFileSystem implements FileSystem {
59
61
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
60
62
  rmdir(path: string, options?: FsOptions): Promise<void>;
61
63
  rm(path: string, options?: RemoveOptions): Promise<void>;
64
+ unlink(path: string, options?: FsOptions): Promise<void>;
62
65
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
63
66
  copyFile(source: string, destination: string, options?: CopyFileOptions): Promise<void>;
64
67
  realpath(path: string, options?: FsOptions): Promise<string>;
@@ -2,6 +2,7 @@ import { constants } from "node:fs";
2
2
  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
+ import { openFileDescriptor } from "../descriptor.js";
5
6
  import { finishCleanup } from "../../contracts/cleanup.js";
6
7
  import { callNativeSeekEnd, loadNativeSeekBinding } from "../../node/native-seek.js";
7
8
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
@@ -21,6 +22,7 @@ function fileStat(stats) {
21
22
  return {
22
23
  type: fileType(stats), size: stats.size, mode: stats.mode,
23
24
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
25
+ ...(Number.isSafeInteger(stats.blksize) && stats.blksize > 0 ? { ioBlockSize: stats.blksize } : {}),
24
26
  ...(Number.isSafeInteger(preferredIoBlockSize) && preferredIoBlockSize > 0 ? { preferredIoBlockSize } : {}),
25
27
  atimeMs: stats.atimeMs, mtimeMs: stats.mtimeMs, ctimeMs: stats.ctimeMs,
26
28
  birthtimeMs: stats.birthtimeMs, ino: stats.ino, dev: stats.dev,
@@ -84,7 +86,7 @@ function nativeError(error) {
84
86
  */
85
87
  export class RealFileSystem {
86
88
  capabilities = Object.freeze({
87
- read: true, stat: true, readdir: true, realpath: true, access: true,
89
+ read: true, stat: true, readdir: true, realpath: true, access: true, open: true,
88
90
  write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
89
91
  mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
90
92
  rename: true, atomicRenameNoReplace: false, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
@@ -251,6 +253,62 @@ export class RealFileSystem {
251
253
  if (resolve(path) === root)
252
254
  throw new FsError("EBUSY", { message: "the filesystem root cannot be removed or replaced" });
253
255
  }
256
+ open(path, options) {
257
+ return openFileDescriptor(path, options, {
258
+ positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
259
+ }, async (admitted) => {
260
+ let handle;
261
+ try {
262
+ const target = await this.path(path, {
263
+ ...admitted, followFinal: admitted.creation !== "exclusive",
264
+ ...(admitted.creation === "never" ? {} : { missing: "final" }),
265
+ });
266
+ let flags = (admitted.access === "read" ? constants.O_RDONLY : admitted.access === "write" ? constants.O_WRONLY : constants.O_RDWR)
267
+ | constants.O_NOFOLLOW | constants.O_NONBLOCK;
268
+ if (admitted.creation !== "never")
269
+ flags |= constants.O_CREAT;
270
+ if (admitted.creation === "exclusive")
271
+ flags |= constants.O_EXCL;
272
+ if (admitted.append)
273
+ flags |= constants.O_APPEND;
274
+ handle = await native.open(target, flags, admitted.mode);
275
+ admitted.signal?.throwIfAborted();
276
+ const stat = await handle.stat();
277
+ if (stat.isDirectory())
278
+ throw new FsError("EISDIR");
279
+ if (!stat.isFile())
280
+ throw new FsError("ENOTSUP");
281
+ admitted.signal?.throwIfAborted();
282
+ if (admitted.truncate)
283
+ await handle.truncate(0);
284
+ const resource = { handle };
285
+ handle = undefined;
286
+ return {
287
+ resource,
288
+ stat: (retained, forwarded) => this.operation("fstat", path, forwarded, async () => fileStat(await retained.handle.stat())),
289
+ read: (retained, buffer, position, forwarded) => this.operation("read", path, forwarded, async () => (await retained.handle.read(buffer, 0, buffer.byteLength, position)).bytesRead),
290
+ write: (retained, buffer, position, forwarded) => this.operation("write", path, forwarded, async () => (await retained.handle.write(buffer, 0, buffer.byteLength, position)).bytesWritten),
291
+ truncate: (retained, length, forwarded) => this.operation("ftruncate", path, forwarded, () => retained.handle.truncate(length)),
292
+ sync: (retained, dataOnly, forwarded) => this.operation(dataOnly ? "fdatasync" : "fsync", path, forwarded, () => dataOnly ? retained.handle.datasync() : retained.handle.sync()),
293
+ close: async (retained) => {
294
+ try {
295
+ await this.operation("close", path, {}, () => retained.handle.close());
296
+ }
297
+ finally {
298
+ retained.handle = undefined;
299
+ }
300
+ },
301
+ };
302
+ }
303
+ catch (error) {
304
+ if (handle)
305
+ await finishCleanup(() => handle.close(), true);
306
+ handle = undefined;
307
+ admitted.signal?.throwIfAborted();
308
+ throw new FsError(nativeError(error).code, { syscall: "open", path });
309
+ }
310
+ });
311
+ }
254
312
  protectTerminal(path) {
255
313
  const terminal = path.split("/").filter(Boolean).at(-1);
256
314
  if (terminal === "." || terminal === "..")
@@ -358,6 +416,15 @@ export class RealFileSystem {
358
416
  await native.rm(target, { recursive: options.recursive ?? false, force: options.force ?? false });
359
417
  });
360
418
  }
419
+ async unlink(path, options = {}) {
420
+ return this.operation("unlink", path, options, async () => {
421
+ const target = await this.path(path, { ...options, followFinal: false });
422
+ this.protectTerminal(path);
423
+ this.protectRoot(target, await this.root(options));
424
+ options.signal?.throwIfAborted();
425
+ await native.unlink(target);
426
+ });
427
+ }
361
428
  async rename(source, destination, options = {}) {
362
429
  return this.operation("rename", source, options, async () => {
363
430
  if (options.noReplace)
@@ -465,11 +532,14 @@ export class RealFileSystem {
465
532
  }
466
533
  async utimes(path, atimeMs, mtimeMs, options = {}) {
467
534
  return this.operation("utimes", path, options, async () => {
468
- if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs))
535
+ if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs)
536
+ || Math.abs(atimeMs) > 8.64e15 || Math.abs(mtimeMs) > 8.64e15)
469
537
  throw new FsError("EINVAL");
470
538
  const target = await this.path(path, options);
471
539
  options.signal?.throwIfAborted();
472
- await native.utimes(target, new Date(atimeMs), new Date(mtimeMs));
540
+ const atimeSeconds = atimeMs / 1000;
541
+ const mtimeSeconds = mtimeMs / 1000;
542
+ await native.utimes(target, atimeSeconds < 0 ? String(atimeSeconds) : atimeSeconds, mtimeSeconds < 0 ? String(mtimeSeconds) : mtimeSeconds);
473
543
  });
474
544
  }
475
545
  async truncate(path, length = 0, options = {}) {
@@ -1,6 +1,7 @@
1
1
  import { FsError } from "../../contracts/errors.js";
2
2
  import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
3
  import type { ByteSource } from "../../contracts/io.js";
4
+ import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
4
5
  import type { S3Transport } from "./transport.js";
5
6
  export interface S3FileSystemOptions {
6
7
  readonly transport: S3Transport;
@@ -21,7 +22,9 @@ export declare class S3RenameError extends FsError {
21
22
  constructor(source: string, destination: string, phase: "copy" | "delete", copied: string[], deleted: string[], cause: FsError);
22
23
  }
23
24
  export declare class S3FileSystem implements FileSystem {
25
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
24
26
  readonly capabilities: Readonly<{
27
+ open: false;
25
28
  read: true;
26
29
  stat: true;
27
30
  readdir: true;
@@ -67,6 +67,10 @@ function isWellFormed(value) {
67
67
  return !/[\uD800-\uDFFF]/u.test(value);
68
68
  }
69
69
  export class S3FileSystem {
70
+ async open(path, options) {
71
+ options.signal?.throwIfAborted();
72
+ throw new FsError("ENOTSUP", { syscall: "open", path });
73
+ }
70
74
  capabilities;
71
75
  readStream;
72
76
  writeStream;
@@ -105,6 +109,7 @@ export class S3FileSystem {
105
109
  this.maxStreamBytes = validateLimit(options.maxStreamBytes ?? 5_000_000_000, "maxStreamBytes", 0, 5_000_000_000);
106
110
  this.maxListEntries = validateLimit(options.maxListEntries ?? 100_000, "maxListEntries", 1);
107
111
  this.capabilities = Object.freeze({
112
+ open: false,
108
113
  read: true, stat: true, readdir: true, realpath: true, access: true,
109
114
  write: true, explicitDirectories: true, implicitDirectories: true, mkdir: true, recursiveMkdir: true,
110
115
  remove: true, removeDirectory: true, recursiveRemove: true, copy: true, readlink: false,
@@ -4,10 +4,10 @@ import { registerEntryView } from "./mount/comparison.js";
4
4
  import { openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "./capabilities.js";
5
5
  const originals = new WeakMap();
6
6
  const operations = new Set([
7
- "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
7
+ "removeEntryConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
8
8
  "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
9
9
  "copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir",
10
- "readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "stat", "symlink", "truncate", "utimes",
10
+ "readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "unlink", "stat", "symlink", "truncate", "utimes",
11
11
  "writeFile", "writeStream",
12
12
  ]);
13
13
  export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = charge) {
@@ -113,6 +113,40 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
113
113
  close: () => closing ??= Promise.resolve().then(() => handle.close()),
114
114
  };
115
115
  };
116
+ const wrapDescriptor = (descriptor) => {
117
+ let closing;
118
+ const invoke = async (options, action) => {
119
+ assertOpen(options);
120
+ if (closing)
121
+ throw new FsError("EBADF");
122
+ admit(options);
123
+ assertOpen(options);
124
+ if (closing)
125
+ throw new FsError("EBADF");
126
+ try {
127
+ const result = await action(resizeOptions(options));
128
+ assertOpen(options);
129
+ return result;
130
+ }
131
+ catch (error) {
132
+ assertOpen(options);
133
+ throw error;
134
+ }
135
+ };
136
+ const getPosition = descriptor.getPosition;
137
+ const probeRead = descriptor.probeRead;
138
+ return {
139
+ capabilities: Object.freeze({ ...descriptor.capabilities }),
140
+ ...(getPosition === undefined ? {} : { getPosition: (options = {}) => invoke(options, scoped => getPosition.call(descriptor, scoped)) }),
141
+ ...(probeRead === undefined ? {} : { probeRead: (options = {}) => invoke(options, scoped => probeRead.call(descriptor, scoped)) }),
142
+ stat: (options = {}) => invoke(options, scoped => descriptor.stat(scoped)),
143
+ read: (buffer, position, options = {}) => invoke(options, scoped => descriptor.read(buffer, position, scoped)),
144
+ write: (buffer, position, options = {}) => invoke(options, scoped => descriptor.write(buffer, position, scoped)),
145
+ truncate: (length, options = {}) => invoke(options, scoped => descriptor.truncate(length, scoped)),
146
+ sync: (dataOnly, options = {}) => invoke(options, scoped => descriptor.sync(dataOnly, scoped)),
147
+ close: () => closing ??= Promise.resolve().then(() => descriptor.close()),
148
+ };
149
+ };
116
150
  const wrapStream = (source, options) => ({
117
151
  [Symbol.asyncIterator]() {
118
152
  assertOpen(options);
@@ -164,12 +198,12 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
164
198
  const options = args.at(-1);
165
199
  admit(options && typeof options === "object" && "signal" in options ? options : undefined);
166
200
  }
167
- if (["writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory"].includes(String(property)))
201
+ if (["removeEntryConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory"].includes(String(property)))
168
202
  return (async () => {
169
203
  const path = typeof args[0] === "string" ? args[0] : args[0].directory.path;
170
204
  const options = args[property === "createStagedFile" ? 3 : property === "publishStagedFile" || property === "writeFileConditional" ? 2 : 1];
171
205
  const create = property === "createStagedFile" || (property === "writeFileConditional" || property === "prepareDirectory") && options !== undefined && "expected" in options && options.expected === null;
172
- await requireOwnedMutation(original, path, property === "prepareDirectory" ? "atomicDirectoryMetadata" : property === "writeFileConditional" || property === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", options ?? {}, create);
206
+ await requireOwnedMutation(original, path, property === "removeEntryConditional" ? "atomicEntryRemoval" : property === "prepareDirectory" ? "atomicDirectoryMetadata" : property === "writeFileConditional" || property === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", options ?? {}, create);
173
207
  assertOpen(options);
174
208
  return Reflect.apply(method, original, args);
175
209
  })();
@@ -190,21 +224,38 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
190
224
  })();
191
225
  return Reflect.apply(method, original, args);
192
226
  };
193
- const scoped = property === "openResizeFile"
194
- ? async (path, options = {}) => {
227
+ const scoped = property === "open"
228
+ ? async (path, options) => {
195
229
  admit(options);
196
- assertOpen(options);
197
- return wrapResizeHandle(await openRetainedResizeFile(original, path, resizeOptions(options)));
230
+ const selected = resizeOptions(options);
231
+ let descriptor;
232
+ try {
233
+ descriptor = await Reflect.apply(method, original, [path, selected]);
234
+ assertOpen(options);
235
+ return wrapDescriptor(descriptor);
236
+ }
237
+ catch (error) {
238
+ if (descriptor)
239
+ await finishCleanup(() => descriptor.close(), true);
240
+ assertOpen(options);
241
+ throw error;
242
+ }
198
243
  }
199
- : property === "capabilitiesFor"
200
- ? async (...args) => ownedMutationCapabilities(original, retainedResizeCapabilities(original, await dispatch(...args)))
201
- : property === "openReadFile"
202
- ? async (...args) => wrapHandle(await dispatch(...args))
203
- : property === "readStream"
204
- ? (...args) => wrapStream(dispatch(...args), args[1])
205
- : operations.has(property) && property !== "canonicalizeMissingTarget"
206
- ? async (...args) => dispatch(...args)
207
- : dispatch;
244
+ : property === "openResizeFile"
245
+ ? async (path, options = {}) => {
246
+ admit(options);
247
+ assertOpen(options);
248
+ return wrapResizeHandle(await openRetainedResizeFile(original, path, resizeOptions(options)));
249
+ }
250
+ : property === "capabilitiesFor"
251
+ ? async (...args) => ownedMutationCapabilities(original, retainedResizeCapabilities(original, await dispatch(...args)))
252
+ : property === "openReadFile"
253
+ ? async (...args) => wrapHandle(await dispatch(...args))
254
+ : property === "readStream"
255
+ ? (...args) => wrapStream(dispatch(...args), args[1])
256
+ : operations.has(property) && property !== "canonicalizeMissingTarget"
257
+ ? async (...args) => dispatch(...args)
258
+ : dispatch;
208
259
  methods.set(property, { original: method, scoped });
209
260
  return scoped;
210
261
  },
@@ -1,6 +1,7 @@
1
1
  import type { PlatformComparisonCallback } from "#safe-fs-platform";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
4
+ import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
4
5
  export type WebDavFetch = (url: string, init: RequestInit) => Promise<Response>;
5
6
  export interface WebDavAtomicEmptyDirectoryRequest {
6
7
  readonly operation: "atomic-empty-rmdir/v1";
@@ -33,7 +34,9 @@ export interface WebDavFileSystemOptions {
33
34
  readonly compareEntry?: PlatformComparisonCallback<(this: FileSystem, ...args: Parameters<NonNullable<FileSystem["compareEntry"]>>) => ReturnType<NonNullable<FileSystem["compareEntry"]>>>;
34
35
  }
35
36
  export declare class WebDavFileSystem implements FileSystem {
37
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
36
38
  readonly capabilities: Readonly<{
39
+ open: false;
37
40
  read: true;
38
41
  stat: true;
39
42
  readdir: true;
@@ -140,7 +140,12 @@ function statusCode(element) {
140
140
  return Number(text.split(" ")[1]);
141
141
  }
142
142
  export class WebDavFileSystem {
143
+ async open(path, options) {
144
+ options.signal?.throwIfAborted();
145
+ throw new FsError("ENOTSUP", { syscall: "open", path });
146
+ }
143
147
  capabilities = Object.freeze({
148
+ open: false,
144
149
  read: true, stat: true, readdir: true, realpath: true, access: true,
145
150
  write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
146
151
  mkdir: true, recursiveMkdir: true, remove: true, recursiveRemove: true, rename: true, atomicRenameNoReplace: false, copy: true,
@@ -1,4 +1,7 @@
1
1
  export * from "./contracts/index.js";
2
+ export * from "./python/index.js";
3
+ export { openFileDescriptor } from "./fs/descriptor.js";
4
+ export type { DescriptorBackend, DescriptorOpenOptions } from "./fs/descriptor.js";
2
5
  export * from "./fs/memory/index.js";
3
6
  export * from "./fs/devices/index.js";
4
7
  export * from "./fs/real/index.js";
@@ -1,4 +1,6 @@
1
1
  export * from "./contracts/index.js";
2
+ export * from "./python/index.js";
3
+ export { openFileDescriptor } from "./fs/descriptor.js";
2
4
  export * from "./fs/memory/index.js";
3
5
  export * from "./fs/devices/index.js";
4
6
  export * from "./fs/real/index.js";
@@ -0,0 +1,14 @@
1
+ export interface PythonMountOptions {
2
+ request: (operation: string, ...args: any[]) => any;
3
+ cwd: string;
4
+ runtimeMount: string;
5
+ maxTransferBytes: number;
6
+ errno: Record<string, number>;
7
+ synchronizationFlags: number;
8
+ runtimeModule: any;
9
+ }
10
+ export declare function mountPythonFileSystem(FS: any, options: PythonMountOptions): void;
11
+ /** Encode the pinned wasm32 musl stat layout without truncating canonical values. */
12
+ export declare function writePythonStat(heap: Uint8Array, pointer: number, stat: Record<string, any>): number;
13
+ /** Runtime storage and application storage occupy disjoint guest device ranges. */
14
+ export declare function createPythonRuntimeStatMapper(maxDevices?: number): (stat: Record<string | symbol, any>) => Record<string | symbol, any>;