@poe-platform/safe-fs 0.1.538 → 0.1.540

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.
@@ -56,6 +56,7 @@ export interface FileSystemCapabilities {
56
56
  readonly streamingRead?: boolean;
57
57
  readonly retainedRead?: boolean;
58
58
  readonly retainedResize?: boolean;
59
+ readonly atomicResize?: boolean;
59
60
  readonly streamingWrite?: boolean;
60
61
  readonly descriptorWriteStream?: boolean;
61
62
  readonly [capability: string]: boolean | undefined;
@@ -79,6 +80,22 @@ export interface OpenResizeFileOptions extends FsOptions {
79
80
  readonly create?: boolean;
80
81
  readonly mode?: number;
81
82
  }
83
+ /** A single atomic read/compute/resize operation, never a caller-side stat/write pair.
84
+ * size and referenceSize use signed 64-bit byte integers (referenceSize is nonnegative).
85
+ * ioBlocks scales size by the target's preferred I/O block size before applying the
86
+ * modifier. Relative/min/max/round operations use referenceSize or the current size.
87
+ * Signed overflow rejects; negative final sizes clamp to zero. Providers enforce
88
+ * their size/quota/permission limits before allocation or publication. */
89
+ export interface FileResizeOperation {
90
+ readonly size: bigint;
91
+ readonly modifier: "absolute" | "relative" | "minimum" | "maximum" | "down" | "up";
92
+ readonly referenceSize?: bigint;
93
+ readonly ioBlocks?: boolean;
94
+ }
95
+ export interface FileResizeOptions extends FsOptions {
96
+ readonly create?: boolean;
97
+ readonly mode?: number;
98
+ }
82
99
  export interface FileResizeHandle {
83
100
  stat(options?: FsOptions): Promise<FileStat>;
84
101
  truncate(length: number, options?: FsOptions): Promise<void>;
@@ -128,6 +145,8 @@ export interface FileSystem {
128
145
  readonly capabilities: FileSystemCapabilities;
129
146
  openReadFile?(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
130
147
  openResizeFile?(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
148
+ /** Missing targets reject ENOENT unless create is true; existing bytes survive refusals. */
149
+ resizeFile?(path: string, operation: FileResizeOperation, options?: FileResizeOptions): Promise<void>;
131
150
  canonicalizeMissingTarget?(path: string, options?: FsOptions): string | undefined;
132
151
  capabilitiesFor?(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
133
152
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
@@ -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, retainedResize: false,
111
+ descriptorWriteStream: false, retainedResize: false, atomicResize: false,
112
112
  atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
113
113
  });
114
114
  }
@@ -116,7 +116,7 @@ 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,
119
+ return Object.freeze({ ...rest, descriptorWriteStream: false, atomicResize: false,
120
120
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
121
121
  ...(streamingAppend === undefined ? {} : { streamingAppend }),
122
122
  });
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  export declare class DeviceFileSystem implements FileSystem {
4
4
  #private;
@@ -13,6 +13,7 @@ export declare class DeviceFileSystem implements FileSystem {
13
13
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
14
14
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
15
15
  openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
16
+ resizeFile(path: string, operation: FileResizeOperation, options?: FileResizeOptions): Promise<void>;
16
17
  openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
17
18
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
18
19
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { FsError, isFsError } from "../../contracts/errors.js";
2
2
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
3
- import { compareEntries, registerEntryView } from "../mount/comparison.js";
3
+ import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
4
4
  import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
5
5
  import { deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
6
6
  import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities } from "../capabilities.js";
@@ -13,13 +13,13 @@ const deviceCapabilities = Object.freeze({
13
13
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
14
14
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
15
15
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
16
- atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true,
16
+ atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
17
17
  });
18
18
  function globalCapabilities(filesystem) {
19
19
  const capabilities = { readOnly: false };
20
20
  const optional = {
21
21
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
22
- streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"],
22
+ streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
23
23
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
24
24
  timestamps: ["utimes"], readlink: ["readlink"], truncate: ["truncate"], removeDirectory: ["rmdir"],
25
25
  };
@@ -84,6 +84,8 @@ export class DeviceFileSystem {
84
84
  stat: resolved === nullPath ? this.#nullStat : this.#directoryStat, readOnly: false };
85
85
  return { filesystem, path };
86
86
  });
87
+ // This internal view has no identity proof beyond its synthetic stats.
88
+ registerEntryAuthority(this, async () => "unknown");
87
89
  return view;
88
90
  }
89
91
  #resolve(path, options, followFinal = true, resizeCreate) {
@@ -139,6 +141,8 @@ export class DeviceFileSystem {
139
141
  }
140
142
  if (typeof this.#filesystem.openReadFile !== "function")
141
143
  unavailable.retainedRead = false;
144
+ if (observed.atomicResize === true && (typeof this.#filesystem.resizeFile !== "function" || observed.readOnly === true))
145
+ unavailable.atomicResize = false;
142
146
  const result = Object.keys(unavailable).some(key => capabilities[key] !== false)
143
147
  ? { ...capabilities, ...unavailable } : capabilities;
144
148
  options.signal?.throwIfAborted();
@@ -223,6 +227,19 @@ export class DeviceFileSystem {
223
227
  async close() { closed = true; },
224
228
  };
225
229
  }
230
+ async resizeFile(path, operation, options = {}) {
231
+ await this.#mutable(path, options);
232
+ const capabilities = await this.capabilitiesFor(path, options);
233
+ options.signal?.throwIfAborted();
234
+ if (capabilities.readOnly === true)
235
+ throw new FsError("EROFS", { syscall: "resizeFile", path });
236
+ const resize = this.#filesystem.resizeFile;
237
+ options.signal?.throwIfAborted();
238
+ if (capabilities.atomicResize !== true || typeof resize !== "function")
239
+ throw new FsError("ENOTSUP", { syscall: "resizeFile", path });
240
+ await Reflect.apply(resize, this.#filesystem, [path, operation, options]);
241
+ options.signal?.throwIfAborted();
242
+ }
226
243
  async openResizeFile(path, options = {}) {
227
244
  options.signal?.throwIfAborted();
228
245
  const resolution = this.#resolve(path, options, true, options.create ?? false);
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  export interface MountFileSystemOptions {
4
4
  readonly root: FileSystem;
@@ -12,6 +12,7 @@ export declare class MountFileSystem implements FileSystem {
12
12
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
13
13
  private protected;
14
14
  openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
15
+ resizeFile(path: string, operation: FileResizeOperation, options?: FileResizeOptions): Promise<void>;
15
16
  openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
16
17
  private error;
17
18
  private operation;
@@ -91,7 +91,7 @@ export class MountFileSystem {
91
91
  const common = (capability) => {
92
92
  const optional = {
93
93
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
94
- descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"],
94
+ descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
95
95
  };
96
96
  const values = mounts.map(({ backend }) => {
97
97
  if (backend.capabilities.readOnly === true
@@ -110,7 +110,7 @@ export class MountFileSystem {
110
110
  "read", "stat", "readdir", "realpath", "access",
111
111
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
112
112
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
113
- "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "symlinks", "hardlinks", "permissions", "timestamps",
113
+ "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "atomicResize", "symlinks", "hardlinks", "permissions", "timestamps",
114
114
  ].map(capability => [capability, common(capability)]).filter(([, value]) => value !== undefined));
115
115
  this.capabilities = Object.freeze({
116
116
  get snapshotRmdir() { return mounts.some(({ backend }) => backend.capabilities.snapshotRmdir === true); },
@@ -137,8 +137,10 @@ export class MountFileSystem {
137
137
  const declared = observed.descriptorWriteStream === true
138
138
  && (typeof location.mount.backend.writeStream !== "function" || observed.readOnly === true || observed.streamingWrite === false)
139
139
  ? { ...observed, descriptorWriteStream: false } : observed;
140
- const capabilities = location.synthetic ? { ...declared, retainedRead: false }
141
- : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, declared));
140
+ const resize = declared.atomicResize === true && (typeof location.mount.backend.resizeFile !== "function" || declared.readOnly === true)
141
+ ? { ...declared, atomicResize: false } : declared;
142
+ const capabilities = location.synthetic ? { ...resize, retainedRead: false }
143
+ : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, resize));
142
144
  if (location.synthetic)
143
145
  return readOnlyCapabilities(capabilities);
144
146
  if (this.mounts.length === 1 || capabilities.readOnly === true)
@@ -167,6 +169,23 @@ export class MountFileSystem {
167
169
  throw error ? this.error(error, "openReadFile", path, options) : error;
168
170
  }
169
171
  }
172
+ async resizeFile(path, operation, options = {}) {
173
+ return this.operation("resizeFile", path, options, async () => {
174
+ const location = await this.resolve(path, options, { allowMissing: options.create === true, resizeCreate: options.create ?? false });
175
+ if (location.synthetic)
176
+ fail("EROFS");
177
+ const backend = location.mount.backend;
178
+ const capabilities = await backend.capabilitiesFor?.(location.local, options) ?? backend.capabilities;
179
+ options.signal?.throwIfAborted();
180
+ if (capabilities.readOnly === true)
181
+ fail("EROFS");
182
+ const resize = backend.resizeFile;
183
+ options.signal?.throwIfAborted();
184
+ if (capabilities.atomicResize !== true || typeof resize !== "function")
185
+ fail("ENOTSUP");
186
+ await Reflect.apply(resize, backend, [location.local, operation, options]);
187
+ });
188
+ }
170
189
  async openResizeFile(path, options = {}) {
171
190
  try {
172
191
  options.signal?.throwIfAborted();
@@ -120,7 +120,7 @@ export class OverlayFileSystem {
120
120
  ...(effectiveAppend === undefined ? {} : { append: effectiveAppend }),
121
121
  atomicRename: false, atomicRenameNoReplace: false,
122
122
  descriptorWriteStream: false,
123
- retainedResize: false,
123
+ retainedResize: false, atomicResize: false,
124
124
  hardlinks: false,
125
125
  symlinks: writable && this.#upper.capabilities.symlinks === true
126
126
  && typeof this.#upper.symlink === "function" && typeof this.#upper.readlink === "function"
@@ -316,7 +316,7 @@ export function withFileSystemQuota(fs, options) {
316
316
  // capabilities and methods without violating invariants on own properties.
317
317
  return new Proxy(Object.create(fs), {
318
318
  get(_target, property) {
319
- if (property === "canonicalizeMissingTarget")
319
+ if (property === "canonicalizeMissingTarget" || property === "resizeFile")
320
320
  return undefined;
321
321
  if (property === "capabilities")
322
322
  return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs)));
@@ -6,7 +6,7 @@ const originals = new WeakMap();
6
6
  const operations = new Set([
7
7
  "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
8
8
  "copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir",
9
- "readlink", "realpath", "rename", "rm", "rmdir", "stat", "symlink", "truncate", "utimes",
9
+ "readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "stat", "symlink", "truncate", "utimes",
10
10
  "writeFile", "writeStream",
11
11
  ]);
12
12
  export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = charge) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.538",
3
+ "version": "0.1.540",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",