@poe-platform/safe-fs 0.1.652 → 0.1.654

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.
@@ -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, open: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
119
+ return Object.freeze({ ...rest, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
120
120
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
121
121
  ...(streamingAppend === undefined ? {} : { streamingAppend }),
122
122
  });
@@ -6,6 +6,7 @@ export interface FileSystemQuotaOptions {
6
6
  }
7
7
  export declare class FileSystemQuotaError extends Error {
8
8
  readonly maxBytes: number;
9
+ readonly code = "ENOSPC";
9
10
  constructor(maxBytes: number);
10
11
  }
11
12
  export declare function withFileSystemQuota(fs: FileSystem, options: FileSystemQuotaOptions): FileSystem;
@@ -2,8 +2,10 @@ import { FsError } from "../../contracts/errors.js";
2
2
  import { finishCleanup } from "../../contracts/cleanup.js";
3
3
  import { openRetainedReadFile, openRetainedResizeFile, quotaCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
4
4
  import { admitDirectoryEntries } from "../directory-admission.js";
5
+ import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
5
6
  export class FileSystemQuotaError extends Error {
6
7
  maxBytes;
8
+ code = "ENOSPC";
7
9
  constructor(maxBytes) {
8
10
  super(`Filesystem quota exceeded (${maxBytes} bytes)`);
9
11
  this.maxBytes = maxBytes;
@@ -183,9 +185,87 @@ export function withFileSystemQuota(fs, options) {
183
185
  throw new FileSystemQuotaError(options.maxBytes);
184
186
  };
185
187
  const mutations = {
186
- async open(path, openOptions) {
187
- openOptions.signal?.throwIfAborted();
188
- throw new FsError("ENOTSUP", { syscall: "open", path });
188
+ open(path, openOptions) {
189
+ return openFileDescriptor(path, openOptions, {
190
+ positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
191
+ }, admitted => mutate(async () => {
192
+ admitted.signal?.throwIfAborted();
193
+ const open = fs.open;
194
+ const capabilities = admitted.creation === "exclusive" ? fs.capabilities : await fs.capabilitiesFor?.(path, admitted) ?? fs.capabilities;
195
+ admitted.signal?.throwIfAborted();
196
+ if (!open || capabilities.open === false)
197
+ throw new FsError("ENOTSUP", { syscall: "open", path });
198
+ admitted.signal?.throwIfAborted();
199
+ const descriptor = await open.call(fs, path, admitted);
200
+ try {
201
+ admitted.signal?.throwIfAborted();
202
+ const pinned = admitted.access === "read" ? undefined : { ...await descriptor.stat(admitted) };
203
+ admitted.signal?.throwIfAborted();
204
+ if (pinned && (pinned.type !== "file" || !completeIdentity(pinned))) {
205
+ throw new FsError("ENOTSUP", { syscall: "open", path, message: "quota requires complete retained file identity" });
206
+ }
207
+ if (pinned && (!Number.isSafeInteger(pinned.size) || pinned.size < 0)) {
208
+ throw new FsError("EIO", { syscall: "fstat", path, message: "invalid retained file size" });
209
+ }
210
+ const current = async (operationOptions) => {
211
+ const stat = { ...await descriptor.stat(operationOptions) };
212
+ operationOptions.signal?.throwIfAborted();
213
+ if (!pinned || stat.type !== "file" || !completeIdentity(stat)
214
+ || stat.identityScope !== pinned.identityScope || stat.dev !== pinned.dev || stat.ino !== pinned.ino
215
+ || !Number.isSafeInteger(stat.size) || stat.size < 0) {
216
+ throw new FsError("EIO", { syscall: "fstat", path, message: "invalid retained quota identity or size" });
217
+ }
218
+ return stat;
219
+ };
220
+ const admitSize = async (stat, nextBytes, operationOptions) => {
221
+ if (!Number.isSafeInteger(nextBytes) || nextBytes < 0)
222
+ throw new FsError("EFBIG", { syscall: "write", path });
223
+ const projected = await usedBytes(fs, scanLimits, operationOptions, { path, stat, delta: nextBytes - stat.size, retained: true });
224
+ operationOptions.signal?.throwIfAborted();
225
+ if (projected > options.maxBytes)
226
+ throw new FileSystemQuotaError(options.maxBytes);
227
+ };
228
+ const forwarded = forwardFileDescriptor(descriptor, (syscall, operationOptions, action) => syscall === "close" ? action() : mutate(async () => {
229
+ operationOptions.signal?.throwIfAborted();
230
+ return action();
231
+ }));
232
+ return {
233
+ ...forwarded,
234
+ write: (retained, buffer, position, operationOptions) => mutate(async () => {
235
+ operationOptions.signal?.throwIfAborted();
236
+ if (buffer.byteLength === 0)
237
+ return retained.write(buffer, position, operationOptions);
238
+ const stat = await current(operationOptions);
239
+ let offset = position;
240
+ if (offset === null) {
241
+ if (admitted.append)
242
+ offset = stat.size;
243
+ else {
244
+ if (forwarded.capabilities?.position !== true || !forwarded.getPosition) {
245
+ throw new FsError("ENOTSUP", { syscall: "getPosition", path });
246
+ }
247
+ offset = await retained.getPosition(operationOptions);
248
+ operationOptions.signal?.throwIfAborted();
249
+ }
250
+ }
251
+ if (!Number.isSafeInteger(offset) || offset < 0)
252
+ throw new FsError("EIO", { syscall: "getPosition", path });
253
+ await admitSize(stat, Math.max(stat.size, offset + buffer.byteLength), operationOptions);
254
+ return retained.write(buffer, position, operationOptions);
255
+ }),
256
+ truncate: (retained, length, operationOptions) => mutate(async () => {
257
+ operationOptions.signal?.throwIfAborted();
258
+ await admitSize(await current(operationOptions), length, operationOptions);
259
+ await retained.truncate(length, operationOptions);
260
+ }),
261
+ };
262
+ }
263
+ catch (error) {
264
+ await finishCleanup(() => descriptor.close(), true);
265
+ admitted.signal?.throwIfAborted();
266
+ throw error;
267
+ }
268
+ }));
189
269
  },
190
270
  openResizeFile(path, resizeOptions = {}) {
191
271
  return mutate(async () => {
@@ -320,14 +400,14 @@ export function withFileSystemQuota(fs, options) {
320
400
  // capabilities and methods without violating invariants on own properties.
321
401
  return new Proxy(Object.create(fs), {
322
402
  get(_target, property) {
323
- if (property === "removeEntryConditional" || property === "unlink" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
403
+ if (property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
324
404
  return undefined;
325
405
  if (property === "capabilities")
326
- return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs)));
406
+ return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }) });
327
407
  if (property === "capabilitiesFor")
328
408
  return async (path, fsOptions) => {
329
409
  const capabilities = await fs.capabilitiesFor?.(path, fsOptions) ?? fs.capabilities;
330
- return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)));
410
+ return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)), ...(typeof fs.open === "function" ? {} : { open: false }) });
331
411
  };
332
412
  if (property === "openReadFile")
333
413
  return (path, fsOptions = {}) => openRetainedReadFile(fs, path, fsOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.652",
3
+ "version": "0.1.654",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",