@poe-platform/safe-fs 0.1.52 → 0.1.54

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.
@@ -23,7 +23,28 @@ export interface DirectoryEntry {
23
23
  }
24
24
  export interface FileSystemCapabilities {
25
25
  readonly readOnly?: boolean;
26
+ readonly read?: boolean;
27
+ readonly stat?: boolean;
28
+ readonly readdir?: boolean;
29
+ readonly realpath?: boolean;
30
+ readonly access?: boolean;
31
+ readonly write?: boolean;
26
32
  readonly append?: boolean;
33
+ readonly exclusiveCreate?: boolean;
34
+ readonly explicitDirectories?: boolean;
35
+ readonly implicitDirectories?: boolean;
36
+ readonly mkdir?: boolean;
37
+ readonly recursiveMkdir?: boolean;
38
+ readonly remove?: boolean;
39
+ readonly removeDirectory?: boolean;
40
+ readonly recursiveRemove?: boolean;
41
+ readonly rename?: boolean;
42
+ readonly copy?: boolean;
43
+ readonly exclusiveCopy?: boolean;
44
+ readonly readlink?: boolean;
45
+ readonly truncate?: boolean;
46
+ readonly streamingAppend?: boolean;
47
+ readonly randomAccessWrite?: boolean;
27
48
  readonly symlinks?: boolean;
28
49
  readonly hardlinks?: boolean;
29
50
  readonly permissions?: boolean;
@@ -65,6 +86,7 @@ export interface ReadStreamOptions extends FsOptions {
65
86
  }
66
87
  export interface FileSystem {
67
88
  readonly capabilities: FileSystemCapabilities;
89
+ capabilitiesFor?(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
68
90
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
69
91
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
70
92
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
@@ -0,0 +1,4 @@
1
+ import type { FileSystemCapabilities } from "../contracts/filesystem.js";
2
+ export declare function requireCapabilities(...values: readonly (boolean | undefined)[]): boolean | undefined;
3
+ export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
4
+ export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
@@ -0,0 +1,25 @@
1
+ export function requireCapabilities(...values) {
2
+ return values.some(value => value === false) ? false : values.every(value => value === true) ? true : undefined;
3
+ }
4
+ export function readOnlyCapabilities(capabilities) {
5
+ const inspection = Object.fromEntries([
6
+ "read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories",
7
+ "symlinks", "streamingRead",
8
+ ].filter(name => capabilities[name] !== undefined).map(name => [name, capabilities[name]]));
9
+ return Object.freeze({
10
+ ...inspection, readOnly: true, write: false, append: false, exclusiveCreate: false,
11
+ mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
12
+ rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
13
+ randomAccessWrite: false, hardlinks: false, permissions: false, timestamps: false,
14
+ atomicRename: false, streamingWrite: false,
15
+ });
16
+ }
17
+ export function quotaCapabilities(capabilities) {
18
+ const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
19
+ const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
20
+ const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
21
+ return Object.freeze({ ...rest,
22
+ ...(streamingWrite === undefined ? {} : { streamingWrite }),
23
+ ...(streamingAppend === undefined ? {} : { streamingAppend }),
24
+ });
25
+ }
@@ -2,6 +2,28 @@ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryCompariso
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  export declare class MemoryFileSystem implements FileSystem {
4
4
  readonly capabilities: Readonly<{
5
+ read: true;
6
+ stat: true;
7
+ readdir: true;
8
+ realpath: true;
9
+ access: true;
10
+ write: true;
11
+ append: true;
12
+ exclusiveCreate: true;
13
+ explicitDirectories: true;
14
+ implicitDirectories: false;
15
+ mkdir: true;
16
+ recursiveMkdir: true;
17
+ remove: true;
18
+ removeDirectory: true;
19
+ recursiveRemove: true;
20
+ rename: true;
21
+ copy: true;
22
+ exclusiveCopy: true;
23
+ readlink: true;
24
+ truncate: true;
25
+ streamingAppend: true;
26
+ randomAccessWrite: true;
5
27
  readOnly: false;
6
28
  symlinks: true;
7
29
  hardlinks: true;
@@ -48,6 +48,11 @@ const compareOwnedMemory = async (own, peer, options) => {
48
48
  };
49
49
  export class MemoryFileSystem {
50
50
  capabilities = Object.freeze({
51
+ read: true, stat: true, readdir: true, realpath: true, access: true,
52
+ write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
53
+ mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
54
+ rename: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
55
+ streamingAppend: true, randomAccessWrite: true,
51
56
  readOnly: false,
52
57
  symlinks: true,
53
58
  hardlinks: true,
@@ -9,6 +9,7 @@ export declare class MountFileSystem implements FileSystem {
9
9
  private readonly mounts;
10
10
  constructor(options: MountFileSystemOptions);
11
11
  private select;
12
+ capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
12
13
  private protected;
13
14
  private error;
14
15
  private operation;
@@ -1,6 +1,7 @@
1
1
  import { FsError, isFsError, toFsError } from "../../contracts/errors.js";
2
2
  import { readBytes } from "../../contracts/io.js";
3
3
  import { finishCleanup } from "../../contracts/cleanup.js";
4
+ import { readOnlyCapabilities } from "../capabilities.js";
4
5
  import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
5
6
  import { compareIdentity } from "./identity.js";
6
7
  import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
@@ -79,14 +80,32 @@ export class MountFileSystem {
79
80
  const streamingWrite = streaming("streamingWrite", "writeStream");
80
81
  const append = all("append") ? true
81
82
  : mounts.every(({ backend }) => backend.capabilities.append === false) ? false : undefined;
83
+ const common = (capability) => {
84
+ const optional = {
85
+ symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
86
+ };
87
+ const values = mounts.map(({ backend }) => {
88
+ if (backend.capabilities.readOnly === true
89
+ && !["read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories"].includes(capability))
90
+ return false;
91
+ const declared = backend.capabilities[capability];
92
+ return declared === true && optional[capability]?.some(method => typeof backend[method] !== "function") ? false : declared;
93
+ });
94
+ if (["rename", "copy", "exclusiveCopy"].includes(capability) && mounts.length > 1)
95
+ return undefined;
96
+ return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
97
+ };
98
+ const semantics = Object.fromEntries([
99
+ "read", "stat", "readdir", "realpath", "access",
100
+ "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
101
+ "remove", "removeDirectory", "recursiveRemove", "rename", "copy", "exclusiveCopy", "readlink", "truncate",
102
+ "streamingAppend", "randomAccessWrite", "symlinks", "hardlinks", "permissions", "timestamps",
103
+ ].map(capability => [capability, common(capability)]).filter(([, value]) => value !== undefined));
82
104
  this.capabilities = Object.freeze({
83
105
  get snapshotRmdir() { return mounts.some(({ backend }) => backend.capabilities.snapshotRmdir === true); },
84
106
  readOnly: all("readOnly"),
85
107
  ...(append === undefined ? {} : { append }),
86
- symlinks: all("symlinks", ["readlink", "symlink"]),
87
- hardlinks: all("hardlinks", ["link"]),
88
- permissions: all("permissions", ["chmod"]),
89
- timestamps: all("timestamps", ["utimes"]),
108
+ ...semantics,
90
109
  atomicRename: mounts.length === 1 && all("atomicRename"),
91
110
  ...(streamingRead === undefined ? {} : { streamingRead }),
92
111
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
@@ -95,6 +114,24 @@ export class MountFileSystem {
95
114
  select(path) {
96
115
  return this.mounts.find((mount) => within(mount.path, path));
97
116
  }
117
+ async capabilitiesFor(path, options = {}) {
118
+ return this.operation("capabilitiesFor", path, options, async () => {
119
+ const location = await this.resolve(path, options, { allowMissing: true });
120
+ const capabilities = await location.mount.backend.capabilitiesFor?.(location.local, options)
121
+ ?? location.mount.backend.capabilities;
122
+ if (location.synthetic)
123
+ return readOnlyCapabilities(capabilities);
124
+ if (this.mounts.length === 1 || capabilities.readOnly === true)
125
+ return Object.freeze({ ...capabilities });
126
+ const { rename: ignoredRename, copy: ignoredCopy, exclusiveCopy: ignoredExclusiveCopy, ...selected } = capabilities;
127
+ const cannotPublish = capabilities.write === false && capabilities.streamingWrite === false && capabilities.exclusiveCreate === false;
128
+ return Object.freeze({
129
+ ...selected,
130
+ ...(capabilities.copy === false && capabilities.exclusiveCopy === false && cannotPublish ? { copy: false } : {}),
131
+ ...(capabilities.exclusiveCopy === false && capabilities.exclusiveCreate === false && capabilities.streamingWrite === false ? { exclusiveCopy: false } : {}),
132
+ });
133
+ });
134
+ }
98
135
  protected(path) {
99
136
  return path === "/" || this.mounts.some((mount) => within(path, mount.path));
100
137
  }
@@ -1,4 +1,5 @@
1
1
  import { platform } from "#safe-fs-platform";
2
+ import { requireCapabilities } from "../capabilities.js";
2
3
  import { finishCleanup } from "../../contracts/cleanup.js";
3
4
  import { collectBytes, readBytes } from "../../contracts/io.js";
4
5
  import { FsError, toFsError } from "../../contracts/errors.js";
@@ -83,9 +84,35 @@ export class OverlayFileSystem {
83
84
  ? readable.every((capability) => capability === true) ? true : undefined : false;
84
85
  const append = !writable || this.#upper.capabilities.append === false ? false
85
86
  : this.#upper.capabilities.append === true ? true : undefined;
87
+ const upper = this.#upper.capabilities;
88
+ const mutation = requireCapabilities(writable, upper.mkdir, upper.rename, upper.remove, upper.exclusiveCreate, upper.timestamps, upper.permissions);
89
+ const effectiveAppend = requireCapabilities(append, mutation);
90
+ const effectiveStreamingWrite = requireCapabilities(streamingWrite, mutation);
91
+ const semantics = Object.fromEntries([
92
+ ...["read", "stat", "readdir", "realpath", "access"].map(capability => [capability,
93
+ requireCapabilities(upper[capability], this.#lower.capabilities[capability])]),
94
+ ["write", requireCapabilities(mutation, upper.write)],
95
+ ["exclusiveCreate", requireCapabilities(mutation, upper.exclusiveCreate)],
96
+ ["mkdir", requireCapabilities(mutation, upper.mkdir)],
97
+ ["recursiveMkdir", requireCapabilities(mutation, upper.recursiveMkdir)],
98
+ ["remove", requireCapabilities(mutation, upper.remove)],
99
+ ["removeDirectory", requireCapabilities(mutation, upper.removeDirectory)],
100
+ ["recursiveRemove", requireCapabilities(mutation, upper.recursiveRemove)],
101
+ ["rename", requireCapabilities(mutation, upper.rename, upper.write)],
102
+ ["copy", requireCapabilities(mutation, upper.write)],
103
+ ["exclusiveCopy", requireCapabilities(mutation, upper.exclusiveCreate)],
104
+ ["truncate", requireCapabilities(mutation, upper.truncate)],
105
+ ["streamingAppend", requireCapabilities(mutation, streamingWrite, upper.streamingAppend)],
106
+ ["randomAccessWrite", requireCapabilities(mutation, upper.randomAccessWrite)],
107
+ ["explicitDirectories", requireCapabilities(upper.explicitDirectories, this.#lower.capabilities.explicitDirectories)],
108
+ ].filter(([, value]) => value !== undefined));
86
109
  this.capabilities = Object.freeze({
87
- readOnly: !writable,
88
- ...(append === undefined ? {} : { append }),
110
+ ...semantics,
111
+ implicitDirectories: false,
112
+ readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
113
+ : upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
114
+ ...(upper.readOnly === undefined ? {} : { readOnly: upper.readOnly }),
115
+ ...(effectiveAppend === undefined ? {} : { append: effectiveAppend }),
89
116
  atomicRename: false,
90
117
  hardlinks: false,
91
118
  symlinks: writable && this.#upper.capabilities.symlinks === true
@@ -94,7 +121,7 @@ export class OverlayFileSystem {
94
121
  permissions: writable && this.#upper.capabilities.permissions === true && typeof this.#upper.chmod === "function",
95
122
  timestamps: writable && this.#upper.capabilities.timestamps === true && typeof this.#upper.utimes === "function",
96
123
  ...(streamingRead === undefined ? {} : { streamingRead }),
97
- ...(streamingWrite === undefined ? {} : { streamingWrite }),
124
+ ...(effectiveStreamingWrite === undefined ? {} : { streamingWrite: effectiveStreamingWrite }),
98
125
  });
99
126
  Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
100
127
  }
@@ -1,3 +1,4 @@
1
+ import { quotaCapabilities } from "../capabilities.js";
1
2
  export class FileSystemQuotaError extends Error {
2
3
  maxBytes;
3
4
  constructor(maxBytes) {
@@ -99,6 +100,10 @@ export function withFileSystemQuota(fs, options) {
99
100
  };
100
101
  return new Proxy(fs, {
101
102
  get(target, property) {
103
+ if (property === "capabilities")
104
+ return quotaCapabilities(target.capabilities);
105
+ if (property === "capabilitiesFor")
106
+ return async (path, fsOptions) => quotaCapabilities(await target.capabilitiesFor?.(path, fsOptions) ?? target.capabilities);
102
107
  const replacement = Reflect.get(mutations, property);
103
108
  if (typeof replacement === "function")
104
109
  return replacement;
@@ -4,6 +4,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
4
4
  #private;
5
5
  constructor(filesystem: FileSystem);
6
6
  get capabilities(): FileSystemCapabilities;
7
+ capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
7
8
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
8
9
  stat(path: string, options?: FsOptions): Promise<FileStat>;
9
10
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
@@ -2,6 +2,7 @@ import { ACCESS_MODES } from "../../contracts/filesystem.js";
2
2
  import { FsError } from "../../contracts/errors.js";
3
3
  import { readBytes } from "../../contracts/io.js";
4
4
  import { compareEntries, registerEntryView } from "../mount/comparison.js";
5
+ import { readOnlyCapabilities } from "../capabilities.js";
5
6
  function readOnly(syscall, path, dest) {
6
7
  throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
7
8
  }
@@ -26,7 +27,8 @@ export class ReadOnlyFileSystem {
26
27
  this.#filesystem = filesystem;
27
28
  registerEntryView(this, async (path) => ({ filesystem: this.#filesystem, path, readOnly: true }));
28
29
  const streamingRead = typeof filesystem.readStream === "function" ? filesystem.capabilities.streamingRead : false;
29
- this.#capabilities = Object.freeze({
30
+ this.#capabilities = readOnlyCapabilities({
31
+ ...filesystem.capabilities,
30
32
  readOnly: true,
31
33
  append: false,
32
34
  symlinks: filesystem.capabilities.symlinks === true && typeof filesystem.readlink === "function",
@@ -41,6 +43,10 @@ export class ReadOnlyFileSystem {
41
43
  get capabilities() {
42
44
  return this.#capabilities;
43
45
  }
46
+ async capabilitiesFor(path, options) {
47
+ const capabilities = await this.#filesystem.capabilitiesFor?.(path, options) ?? this.#filesystem.capabilities;
48
+ return readOnlyCapabilities(capabilities);
49
+ }
44
50
  async readFile(path, options) {
45
51
  return new Uint8Array(await this.#filesystem.readFile(path, options));
46
52
  }
@@ -80,6 +80,11 @@ function nativeError(error) {
80
80
  */
81
81
  export class RealFileSystem {
82
82
  capabilities = Object.freeze({
83
+ read: true, stat: true, readdir: true, realpath: true, access: true,
84
+ write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
85
+ mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
86
+ rename: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
87
+ streamingAppend: true, randomAccessWrite: true,
83
88
  readOnly: false, symlinks: true, hardlinks: true, permissions: true,
84
89
  timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true,
85
90
  });
@@ -22,6 +22,28 @@ export declare class S3RenameError extends FsError {
22
22
  }
23
23
  export declare class S3FileSystem implements FileSystem {
24
24
  readonly capabilities: Readonly<{
25
+ read: true;
26
+ stat: true;
27
+ readdir: true;
28
+ realpath: true;
29
+ access: true;
30
+ write: true;
31
+ explicitDirectories: true;
32
+ implicitDirectories: true;
33
+ mkdir: true;
34
+ recursiveMkdir: true;
35
+ remove: true;
36
+ removeDirectory: true;
37
+ recursiveRemove: true;
38
+ copy: true;
39
+ readlink: false;
40
+ exclusiveCopy: boolean;
41
+ append: boolean;
42
+ exclusiveCreate: boolean;
43
+ truncate: boolean;
44
+ rename: boolean;
45
+ streamingAppend: boolean;
46
+ randomAccessWrite: false;
25
47
  readOnly: boolean;
26
48
  symlinks: false;
27
49
  hardlinks: false;
@@ -104,6 +104,18 @@ export class S3FileSystem {
104
104
  this.maxStreamBytes = validateLimit(options.maxStreamBytes ?? 5_000_000_000, "maxStreamBytes", 0, 5_000_000_000);
105
105
  this.maxListEntries = validateLimit(options.maxListEntries ?? 100_000, "maxListEntries", 1);
106
106
  this.capabilities = Object.freeze({
107
+ read: true, stat: true, readdir: true, realpath: true, access: true,
108
+ write: true, explicitDirectories: true, implicitDirectories: true, mkdir: true, recursiveMkdir: true,
109
+ remove: true, removeDirectory: true, recursiveRemove: true, copy: true, readlink: false,
110
+ exclusiveCopy: options.transport.capabilities?.conditionalCopy === true,
111
+ append: options.transport.capabilities?.conditionalPut === true,
112
+ exclusiveCreate: options.transport.capabilities?.conditionalPut === true,
113
+ truncate: options.transport.capabilities?.conditionalPut === true,
114
+ rename: this.allowRename && options.transport.capabilities?.conditionalDelete === true
115
+ && (options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true),
116
+ streamingAppend: options.transport.capabilities?.streamingWrite === true
117
+ && options.transport.capabilities?.conditionalPut === true && typeof options.transport.putObjectStream === "function",
118
+ randomAccessWrite: false,
107
119
  readOnly: options.readOnly ?? false,
108
120
  symlinks: false, hardlinks: false, permissions: false,
109
121
  timestamps: options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true,
@@ -33,6 +33,28 @@ export interface WebDavFileSystemOptions {
33
33
  }
34
34
  export declare class WebDavFileSystem implements FileSystem {
35
35
  readonly capabilities: Readonly<{
36
+ read: true;
37
+ stat: true;
38
+ readdir: true;
39
+ realpath: true;
40
+ access: true;
41
+ write: true;
42
+ append: true;
43
+ exclusiveCreate: true;
44
+ explicitDirectories: true;
45
+ implicitDirectories: false;
46
+ mkdir: true;
47
+ recursiveMkdir: true;
48
+ remove: true;
49
+ recursiveRemove: true;
50
+ rename: true;
51
+ copy: true;
52
+ exclusiveCopy: true;
53
+ readlink: false;
54
+ truncate: false;
55
+ randomAccessWrite: false;
56
+ removeDirectory: boolean;
57
+ streamingAppend: boolean;
36
58
  symlinks: false;
37
59
  hardlinks: false;
38
60
  permissions: false;
@@ -140,6 +140,11 @@ function statusCode(element) {
140
140
  }
141
141
  export class WebDavFileSystem {
142
142
  capabilities = Object.freeze({
143
+ read: true, stat: true, readdir: true, realpath: true, access: true,
144
+ write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
145
+ mkdir: true, recursiveMkdir: true, remove: true, recursiveRemove: true, rename: true, copy: true,
146
+ exclusiveCopy: true, readlink: false, truncate: false, randomAccessWrite: false,
147
+ removeDirectory: false, streamingAppend: false,
143
148
  symlinks: false, hardlinks: false, permissions: false, timestamps: true,
144
149
  atomicRename: false, streamingRead: true, streamingWrite: true,
145
150
  });
@@ -200,6 +205,10 @@ export class WebDavFileSystem {
200
205
  }
201
206
  this.transport = options.fetch === globalThis.fetch ? options.fetch.bind(globalThis) : options.fetch;
202
207
  this.requestStreamSupport = options.requestStreamSupport ?? (options.fetch === globalThis.fetch ? "native" : false);
208
+ this.capabilities = Object.freeze({ ...this.capabilities,
209
+ removeDirectory: this.atomicEmptyDirectory !== undefined,
210
+ streamingAppend: this.requestStreamSupport !== false,
211
+ });
203
212
  this.maxResponseBytes = positive(options.maxResponseBytes ?? 64 * 1024 * 1024, "maxResponseBytes");
204
213
  this.maxXmlBytes = positive(options.maxXmlBytes ?? 2 * 1024 * 1024, "maxXmlBytes");
205
214
  this.maxEntries = positive(options.maxEntries ?? 10_000, "maxEntries");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",