@poe-platform/safe-fs 0.1.658 → 0.1.659

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.
@@ -62,6 +62,7 @@ export interface FileSystemCapabilities {
62
62
  readonly atomicFileStaging?: boolean;
63
63
  readonly atomicFileMutation?: boolean;
64
64
  readonly atomicEntryRemoval?: boolean;
65
+ readonly atomicTreeRemoval?: boolean;
65
66
  readonly atomicDirectoryMetadata?: boolean;
66
67
  readonly atomicRenameNoReplace?: boolean;
67
68
  readonly snapshotRmdir?: boolean;
@@ -200,6 +201,7 @@ export interface PrepareDirectoryOptions extends FsOptions {
200
201
  export interface FileSystem {
201
202
  writeFileConditional?(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
202
203
  removeEntryConditional?(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
204
+ removeTreeConditional?(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
203
205
  removeFileConditional?(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
204
206
  readonly capabilities: FileSystemCapabilities;
205
207
  open?(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
@@ -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: "atomicEntryRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
10
+ export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "atomicEntryRemoval" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
@@ -108,7 +108,7 @@ export function readOnlyCapabilities(capabilities) {
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, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
111
+ descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
112
112
  atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
113
113
  });
114
114
  }
@@ -125,6 +125,8 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
125
125
  const unavailable = {};
126
126
  if (capabilities.atomicEntryRemoval === true && (capabilities.readOnly === true || typeof filesystem.removeEntryConditional !== "function"))
127
127
  unavailable.atomicEntryRemoval = false;
128
+ if (capabilities.atomicTreeRemoval === true && (capabilities.readOnly === true || typeof filesystem.removeTreeConditional !== "function"))
129
+ unavailable.atomicTreeRemoval = false;
128
130
  if (capabilities.atomicFileMutation === true && (capabilities.readOnly === true || typeof filesystem.writeFileConditional !== "function" || typeof filesystem.removeFileConditional !== "function"))
129
131
  unavailable.atomicFileMutation = false;
130
132
  if (capabilities.atomicFileStaging === true && (capabilities.readOnly === true
@@ -28,6 +28,7 @@ export declare class DeviceFileSystem implements FileSystem {
28
28
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
29
29
  removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
30
30
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
31
+ removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
31
32
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
32
33
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
33
34
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
@@ -14,14 +14,14 @@ const deviceCapabilities = Object.freeze({
14
14
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
15
15
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
16
16
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
17
- open: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
17
+ open: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
18
18
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
19
19
  });
20
20
  function globalCapabilities(filesystem) {
21
21
  const capabilities = { readOnly: false };
22
22
  const optional = {
23
23
  open: ["open"],
24
- atomicEntryRemoval: ["removeEntryConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
24
+ atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
25
25
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
26
26
  streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
27
27
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
@@ -416,6 +416,13 @@ export class DeviceFileSystem {
416
416
  throw new FsError("ENOTSUP", { path });
417
417
  await this.#filesystem.removeFileConditional(path, options);
418
418
  }
419
+ async removeTreeConditional(path, options) {
420
+ await this.#mutable(path, options, false);
421
+ await requireOwnedMutation(this.#filesystem, path, "atomicTreeRemoval", options);
422
+ if (!this.#filesystem.removeTreeConditional)
423
+ throw new FsError("ENOTSUP", { path });
424
+ await this.#filesystem.removeTreeConditional(path, options);
425
+ }
419
426
  async createStagedFile(path, name, content, options) {
420
427
  await this.#mutable(path, options, false);
421
428
  await requireOwnedMutation(this.#filesystem, path, "atomicFileStaging", options, true);
@@ -37,6 +37,7 @@ export declare class MemoryFileSystem implements FileSystem {
37
37
  atomicFileStaging: true;
38
38
  atomicFileMutation: true;
39
39
  atomicEntryRemoval: true;
40
+ atomicTreeRemoval: true;
40
41
  atomicDirectoryMetadata: true;
41
42
  streamingRead: true;
42
43
  retainedRead: true;
@@ -87,6 +88,7 @@ export declare class MemoryFileSystem implements FileSystem {
87
88
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
88
89
  removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
89
90
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
91
+ removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
90
92
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
91
93
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
92
94
  stat(path: string, options?: FsOptions): Promise<FileStat>;
@@ -95,6 +97,7 @@ export declare class MemoryFileSystem implements FileSystem {
95
97
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
96
98
  rmdir(path: string, options?: FsOptions): Promise<void>;
97
99
  rm(path: string, options?: RemoveOptions): Promise<void>;
100
+ private removeLocation;
98
101
  unlink(path: string, options?: FsOptions): Promise<void>;
99
102
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
100
103
  copyFile(source: string, destination: string, options?: CopyFileOptions): Promise<void>;
@@ -68,7 +68,7 @@ export class MemoryFileSystem {
68
68
  permissions: true,
69
69
  timestamps: true,
70
70
  atomicRename: true,
71
- atomicFileStaging: true, atomicFileMutation: true, atomicEntryRemoval: true,
71
+ atomicFileStaging: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicTreeRemoval: true,
72
72
  atomicDirectoryMetadata: true,
73
73
  streamingRead: true,
74
74
  retainedRead: true,
@@ -696,6 +696,15 @@ export class MemoryFileSystem {
696
696
  this.releaseNode(node);
697
697
  this.changed(location.parent);
698
698
  }
699
+ async removeTreeConditional(path, options) {
700
+ options.signal?.throwIfAborted();
701
+ const location = this.entry(path, "removeTreeConditional", true);
702
+ this.expectEntry(location.parent, options.parent, path, false);
703
+ this.expectEntry(location.node, options.expected, path, false);
704
+ if (location.node?.type !== "directory")
705
+ this.fail("ENOTDIR", "removeTreeConditional", path);
706
+ this.removeLocation(location, path, "removeTreeConditional", true);
707
+ }
699
708
  async writeFile(path, data, options = {}) {
700
709
  options.signal?.throwIfAborted();
701
710
  this.writeData(path, data, options, "writeFile");
@@ -773,19 +782,22 @@ export class MemoryFileSystem {
773
782
  return;
774
783
  throw error;
775
784
  }
785
+ this.removeLocation(location, path, "rm", options.recursive === true);
786
+ }
787
+ removeLocation(location, path, syscall, recursive) {
776
788
  const node = location.node;
777
789
  if (this.terminalDot(path))
778
- this.fail("EINVAL", "rm", path);
790
+ this.fail("EINVAL", syscall, path);
779
791
  if (node === this.root)
780
- this.fail("EBUSY", "rm", path);
781
- this.permission(location.parent, 3, "rm", path);
782
- if (node.type === "directory" && !options.recursive)
783
- this.fail("EISDIR", "rm", path);
792
+ this.fail("EBUSY", syscall, path);
793
+ this.permission(location.parent, 3, syscall, path);
794
+ if (node.type === "directory" && !recursive)
795
+ this.fail("EISDIR", syscall, path);
784
796
  const removed = [node];
785
797
  for (let index = 0; index < removed.length; index++) {
786
798
  const entry = removed[index];
787
799
  if (entry.type === "directory" && entry.entries.size > 0) {
788
- this.permission(entry, 7, "rm", path);
800
+ this.permission(entry, 7, syscall, path);
789
801
  for (const child of entry.entries.values())
790
802
  removed.push(child);
791
803
  }
@@ -38,6 +38,7 @@ export declare class MountFileSystem implements FileSystem {
38
38
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
39
39
  removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
40
40
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
41
+ removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
41
42
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
42
43
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
43
44
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
@@ -96,7 +96,7 @@ export class MountFileSystem {
96
96
  const common = (capability) => {
97
97
  const optional = {
98
98
  open: ["open"],
99
- atomicEntryRemoval: ["removeEntryConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
99
+ atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
100
100
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
101
101
  descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
102
102
  };
@@ -114,7 +114,7 @@ export class MountFileSystem {
114
114
  return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
115
115
  };
116
116
  const semantics = Object.fromEntries([
117
- "atomicEntryRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open",
117
+ "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open",
118
118
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
119
119
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
120
120
  "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "atomicResize", "symlinks", "hardlinks", "permissions", "timestamps",
@@ -620,6 +620,19 @@ export class MountFileSystem {
620
620
  await backend.removeFileConditional(location.local, options);
621
621
  });
622
622
  }
623
+ removeTreeConditional(path, options) {
624
+ return this.operation("removeTreeConditional", path, options, async () => {
625
+ const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
626
+ if (this.protected(location.path))
627
+ fail("EBUSY");
628
+ this.mutable(location);
629
+ const backend = location.mount.backend;
630
+ await requireOwnedMutation(backend, location.local, "atomicTreeRemoval", options);
631
+ if (!backend.removeTreeConditional)
632
+ fail("ENOTSUP");
633
+ await backend.removeTreeConditional(location.local, options);
634
+ });
635
+ }
623
636
  createStagedFile(path, name, content, options) {
624
637
  return this.operation("createStagedFile", path, options, async () => {
625
638
  const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true, missingDirectory: true });
@@ -4,7 +4,7 @@ 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
- "removeEntryConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
7
+ "removeEntryConditional", "removeTreeConditional", "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
10
  "readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "unlink", "stat", "symlink", "truncate", "utimes",
@@ -198,12 +198,12 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
198
198
  const options = args.at(-1);
199
199
  admit(options && typeof options === "object" && "signal" in options ? options : undefined);
200
200
  }
201
- if (["removeEntryConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory"].includes(String(property)))
201
+ if (["removeEntryConditional", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory"].includes(String(property)))
202
202
  return (async () => {
203
203
  const path = typeof args[0] === "string" ? args[0] : args[0].directory.path;
204
204
  const options = args[property === "createStagedFile" ? 3 : property === "publishStagedFile" || property === "writeFileConditional" ? 2 : 1];
205
205
  const create = property === "createStagedFile" || (property === "writeFileConditional" || property === "prepareDirectory") && options !== undefined && "expected" in options && options.expected === null;
206
- await requireOwnedMutation(original, path, property === "removeEntryConditional" ? "atomicEntryRemoval" : property === "prepareDirectory" ? "atomicDirectoryMetadata" : property === "writeFileConditional" || property === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", options ?? {}, create);
206
+ await requireOwnedMutation(original, path, property === "removeEntryConditional" ? "atomicEntryRemoval" : property === "removeTreeConditional" ? "atomicTreeRemoval" : property === "prepareDirectory" ? "atomicDirectoryMetadata" : property === "writeFileConditional" || property === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", options ?? {}, create);
207
207
  assertOpen(options);
208
208
  return Reflect.apply(method, original, args);
209
209
  })();
@@ -18,7 +18,7 @@ export type PythonFsRequest = {
18
18
  readonly op: "sync";
19
19
  readonly args: readonly [number, boolean];
20
20
  } | {
21
- readonly op: "stat" | "lstat" | "readdir" | "realpath" | "readlink" | "rm" | "rmdir";
21
+ readonly op: "stat" | "lstat" | "readdir" | "realpath" | "readlink" | "rm" | "rmdir" | "rmtree" | "rmtreeSupported";
22
22
  readonly args: readonly [string];
23
23
  } | {
24
24
  readonly op: "rename" | "symlink" | "link";
@@ -1,7 +1,7 @@
1
1
  import { parsePythonFsRequest } from "./request.js";
2
2
  import { composeAbortSignals } from "../contracts/abort.js";
3
3
  import { FsError } from "../contracts/errors.js";
4
- import { validatePath } from "../contracts/virtual-path.js";
4
+ import { dirname, validatePath } from "../contracts/virtual-path.js";
5
5
  /** Async half of a worker RPC filesystem. Never run this on an Atomics.wait-blocked interpreter thread. */
6
6
  export class PythonFileSystem {
7
7
  #fs;
@@ -176,6 +176,25 @@ export class PythonFileSystem {
176
176
  throw new FsError("ENOTSUP", { syscall: "rmdir", path });
177
177
  return fs.rmdir(path, options);
178
178
  }
179
+ case "rmtreeSupported":
180
+ case "rmtree": {
181
+ const path = this.#path(request.args[0]);
182
+ const capabilities = fs.capabilitiesFor ? await fs.capabilitiesFor(path, options) : fs.capabilities;
183
+ signal.throwIfAborted();
184
+ if (request.op === "rmtreeSupported")
185
+ return capabilities.readOnly === true || capabilities.atomicTreeRemoval === true && typeof fs.removeTreeConditional === "function";
186
+ if (capabilities.readOnly === true)
187
+ throw new FsError("EROFS", { syscall: "rmtree", path });
188
+ if (capabilities.atomicTreeRemoval !== true || !fs.removeTreeConditional)
189
+ throw new FsError("ENOTSUP", { syscall: "rmtree", path });
190
+ const expected = await fs.lstat(path, options);
191
+ signal.throwIfAborted();
192
+ if (expected.type !== "directory")
193
+ throw new FsError("ENOTDIR", { syscall: "rmtree", path });
194
+ const parent = await fs.stat(dirname(path), options);
195
+ signal.throwIfAborted();
196
+ return fs.removeTreeConditional(path, { ...options, parent, expected });
197
+ }
179
198
  case "rename": return fs.rename(this.#path(request.args[0]), this.#path(request.args[1]), options);
180
199
  case "readlink":
181
200
  if (fs.readlink)
@@ -2,7 +2,7 @@ import { FsError } from "../contracts/errors.js";
2
2
  const argumentKinds = {
3
3
  open: ["string", "object"], read: ["number", "number", "position"], write: ["number", "bytes", "position"],
4
4
  descriptorCapabilities: ["number"], fstat: ["number"], close: ["number"], position: ["number"], ftruncate: ["number", "number"], sync: ["number", "boolean"],
5
- stat: ["string"], lstat: ["string"], readdir: ["string"], realpath: ["string"], readlink: ["string"], rm: ["string"], rmdir: ["string"],
5
+ stat: ["string"], lstat: ["string"], readdir: ["string"], realpath: ["string"], readlink: ["string"], rm: ["string"], rmdir: ["string"], rmtree: ["string"], rmtreeSupported: ["string"],
6
6
  rename: ["string", "string"], symlink: ["string", "string"], link: ["string", "string"], mkdir: ["string", "optionalObject"],
7
7
  chmod: ["string", "number"], truncate: ["string", "number"], access: ["string", "number"], utimes: ["string", "number", "number"],
8
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.658",
3
+ "version": "0.1.659",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",