@poe-platform/safe-fs 0.1.672 → 0.1.674

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.
package/README.md CHANGED
@@ -64,12 +64,23 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
64
64
  | Remove an empty directory | Optional `rmdir`; never a recursive-delete fallback |
65
65
  | Links and metadata | Optional `readlink`, `symlink`, `link`, `chmod`, `utimes`, `truncate` |
66
66
  | Stream bytes | Optional `readStream`, `writeStream`, using async iterables of byte chunks |
67
+ | Publish immutable objects atomically | Optional `publishFileConditional` with opaque identity/version stats and authoritative compare-and-publish |
67
68
  | Compare backing entries | Optional `compareEntry`, returning `same`, `distinct`, or `unknown` |
68
69
 
69
70
  All required methods must exist, but a backend may reject an operation with `FsError`, such as `EROFS` for a write or `ENOTSUP` for unsupported semantics. Check optional methods and `capabilities` rather than assuming every backend behaves like a local disk. Errors expose `code` and may include `syscall`, `path`, `dest`, and `cause`.
70
71
 
71
72
  `createDeviceFileSystem(fs)` adds portable `/dev/null` whole-file, stream, and descriptor I/O. Descriptor reads return EOF and writes discard bytes; stat remains a zero-size character device and descriptor position remains zero. Truncating opens are accepted, exclusive creation fails with `EEXIST`, and descriptor resizing and synchronization are unsupported. Access modes, cancellation, and closed handles use the normal descriptor checks. With an authoritative object store, use `createDeviceFileSystem(withObjectFileDescriptors(fs, store))` so null-device I/O never acquires or publishes an object version; ordinary files retain conditional publication. The device wrapper must be outermost for this composition.
72
73
 
74
+ Immutable flat stores can supply `identityScope`, `opaqueIdentity` and an ABA-safe
75
+ `opaqueVersion` on file stats, then expose `atomicFilePublication: true` with
76
+ `publishFileConditional(path, source, { expected, parent, maxBytes, signal })`.
77
+ The host consumes bytes privately and atomically creates an absent binding
78
+ (`expected: null`) or replaces the observed generation. Failed producers,
79
+ uploads and stale writers leave the old file intact. This supports Safe Bash ZIP
80
+ creation and updates without inode numbers, staging directories or permission
81
+ APIs. Read the [conditional publication contract](src/contracts/conditional-publication.md)
82
+ before implementing the host operation; ordinary writes do not provide it.
83
+
73
84
  | Backend or wrapper | Use it for |
74
85
  | --- | --- |
75
86
  | `createMemoryFileSystem()` | Isolated, nonpersistent storage with links, permissions, timestamps, and streams |
@@ -16,6 +16,10 @@ export interface FileStat {
16
16
  readonly birthtimeMs?: number;
17
17
  readonly revision?: number;
18
18
  readonly identityScope?: object | symbol;
19
+ /** Authoritative backing identity within identityScope; aliases share this key. */
20
+ readonly opaqueIdentity?: string;
21
+ /** ABA-safe namespace generation, changed by replacement and delete/recreate. */
22
+ readonly opaqueVersion?: string;
19
23
  readonly ino?: number;
20
24
  readonly dev?: number;
21
25
  readonly rdevMajor?: number;
@@ -61,6 +65,7 @@ export interface FileSystemCapabilities {
61
65
  readonly timestamps?: boolean;
62
66
  readonly atomicRename?: boolean;
63
67
  readonly atomicFileStaging?: boolean;
68
+ readonly atomicFilePublication?: boolean;
64
69
  readonly atomicFileMutation?: boolean;
65
70
  readonly atomicEntryRemoval?: boolean;
66
71
  readonly atomicTreeRemoval?: boolean;
@@ -199,7 +204,17 @@ export interface PrepareDirectoryOptions extends FsOptions {
199
204
  readonly atimeMs?: number;
200
205
  readonly mtimeMs?: number;
201
206
  }
207
+ export interface ConditionalFilePublicationOptions extends FsOptions {
208
+ readonly expected: FileStat | null;
209
+ readonly parent: FileStat;
210
+ readonly maxBytes: number;
211
+ readonly mode?: number;
212
+ readonly mtimeMs?: number;
213
+ }
202
214
  export interface FileSystem {
215
+ /** Consume the complete source privately, then atomically compare/publish.
216
+ * Failures before commit preserve the destination. No stat/write fallback. */
217
+ publishFileConditional?(path: string, source: ByteSource, options: ConditionalFilePublicationOptions): Promise<FileStat>;
203
218
  writeFileConditional?(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
204
219
  removeEntryConditional?(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
205
220
  removeTreeConditional?(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
@@ -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" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
10
+ export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "atomicFilePublication" | "atomicEntryRemoval" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
@@ -109,20 +109,22 @@ export function readOnlyCapabilities(capabilities) {
109
109
  rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
110
110
  randomAccessWrite: false, hardlinks: false, permissions: false, timestamps: false,
111
111
  descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
112
- atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
112
+ atomicFilePublication: false, atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
113
113
  });
114
114
  }
115
115
  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, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
119
+ return Object.freeze({ ...rest, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
120
120
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
121
121
  ...(streamingAppend === undefined ? {} : { streamingAppend }),
122
122
  });
123
123
  }
124
124
  export function ownedMutationCapabilities(filesystem, capabilities = filesystem.capabilities) {
125
125
  const unavailable = {};
126
+ if (capabilities.atomicFilePublication === true && (capabilities.readOnly === true || typeof filesystem.publishFileConditional !== "function"))
127
+ unavailable.atomicFilePublication = false;
126
128
  if (capabilities.atomicEntryRemoval === true && (capabilities.readOnly === true || typeof filesystem.removeEntryConditional !== "function"))
127
129
  unavailable.atomicEntryRemoval = false;
128
130
  if (capabilities.atomicTreeRemoval === true && (capabilities.readOnly === true || typeof filesystem.removeTreeConditional !== "function"))
@@ -1,5 +1,5 @@
1
1
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
3
3
  import type { ByteSource } from "../../contracts/io.js";
4
4
  export declare class DeviceFileSystem implements FileSystem {
5
5
  #private;
@@ -25,6 +25,7 @@ export declare class DeviceFileSystem implements FileSystem {
25
25
  rm(path: string, options?: RemoveOptions): Promise<void>;
26
26
  unlink(path: string, options?: FsOptions): Promise<void>;
27
27
  rmdir(path: string, options?: FsOptions): Promise<void>;
28
+ publishFileConditional(path: string, source: ByteSource, options: ConditionalFilePublicationOptions): Promise<FileStat>;
28
29
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
29
30
  removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
30
31
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
@@ -15,14 +15,14 @@ const deviceCapabilities = Object.freeze({
15
15
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
16
16
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
17
17
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
18
- open: true, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
18
+ open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
19
19
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
20
20
  });
21
21
  function globalCapabilities(filesystem) {
22
22
  const capabilities = { readOnly: false };
23
23
  const optional = {
24
24
  open: ["open"],
25
- atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
25
+ atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
26
26
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
27
27
  streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
28
28
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
@@ -119,7 +119,28 @@ export class DeviceFileSystem {
119
119
  return reserved(resolved) ? resolved : this.#filesystem.canonicalizeMissingTarget?.(path, options);
120
120
  }
121
121
  async capabilitiesFor(path, options = {}) {
122
- const resolved = await this.#resolve(path, options, true, options.create);
122
+ let resolved;
123
+ let selected;
124
+ try {
125
+ resolved = await this.#resolve(path, options, true, options.create);
126
+ }
127
+ catch (error) {
128
+ options.signal?.throwIfAborted();
129
+ if (options.create !== true || !isFsError(error, "ENOENT"))
130
+ throw error;
131
+ // Implicit prefixes need not exist before the authoritative backend opens
132
+ // a new file. Resolve device aliases without creating namespace entries.
133
+ resolved = await this.#resolve(path, options);
134
+ options.signal?.throwIfAborted();
135
+ if (reserved(resolved))
136
+ throw error;
137
+ const query = this.#filesystem.capabilitiesFor;
138
+ options.signal?.throwIfAborted();
139
+ selected = (query === undefined || query === null ? undefined : await Reflect.apply(query, this.#filesystem, [path, options])) ?? this.#filesystem.capabilities;
140
+ options.signal?.throwIfAborted();
141
+ if (selected.implicitDirectories !== true)
142
+ throw error;
143
+ }
123
144
  options.signal?.throwIfAborted();
124
145
  if (options.create !== undefined && (resolved === deviceDirectory || resolved === "/"))
125
146
  throw new FsError("EISDIR", { syscall: "capabilitiesFor", path });
@@ -129,9 +150,11 @@ export class DeviceFileSystem {
129
150
  return { ...deviceCapabilities, open: false, readdir: true, write: false, append: false,
130
151
  exclusiveCreate: false, streamingWrite: false, streamingAppend: false, independentWriteStreams: false, descriptorWriteStream: false,
131
152
  retainedRead: false, retainedResize: false, streamingRead: false, copy: false, exclusiveCopy: false };
132
- const query = this.#filesystem.capabilitiesFor;
133
- options.signal?.throwIfAborted();
134
- const selected = query === undefined || query === null ? undefined : await Reflect.apply(query, this.#filesystem, [path, options]);
153
+ if (selected === undefined) {
154
+ const query = this.#filesystem.capabilitiesFor;
155
+ options.signal?.throwIfAborted();
156
+ selected = query === undefined || query === null ? undefined : await Reflect.apply(query, this.#filesystem, [path, options]);
157
+ }
135
158
  options.signal?.throwIfAborted();
136
159
  const observed = ownedMutationCapabilities(this.#filesystem, selected ?? this.#filesystem.capabilities);
137
160
  options.signal?.throwIfAborted();
@@ -418,6 +441,13 @@ export class DeviceFileSystem {
418
441
  throw new FsError("ENOTSUP", { path });
419
442
  await this.#filesystem.rmdir(path, options);
420
443
  }
444
+ async publishFileConditional(path, source, options) {
445
+ await this.#mutable(path, options, false);
446
+ await requireOwnedMutation(this.#filesystem, path, "atomicFilePublication", options, options.expected === null);
447
+ if (!this.#filesystem.publishFileConditional)
448
+ throw new FsError("ENOTSUP", { path });
449
+ return this.#filesystem.publishFileConditional(path, source, options);
450
+ }
421
451
  async writeFileConditional(path, data, options) {
422
452
  await this.#mutable(path, options, false);
423
453
  await requireOwnedMutation(this.#filesystem, path, "atomicFileMutation", options, options.expected === null);
@@ -4,9 +4,18 @@ function complete(stat) {
4
4
  && typeof dev === "number" && Number.isSafeInteger(dev) && dev >= 0
5
5
  && typeof ino === "number" && Number.isSafeInteger(ino) && ino >= 0;
6
6
  }
7
+ function opaqueComplete(stat) {
8
+ return ((typeof stat.identityScope === "object" && stat.identityScope !== null) || typeof stat.identityScope === "symbol")
9
+ && typeof stat.opaqueIdentity === "string" && stat.opaqueIdentity.length > 0 && stat.opaqueIdentity.length <= 4096;
10
+ }
7
11
  export function compareIdentity(left, right) {
8
- if (!left || !right || !complete(left) || !complete(right))
12
+ if (!left || !right)
9
13
  return "unknown";
10
- return left.identityScope === right.identityScope && left.dev === right.dev && left.ino === right.ino
11
- ? "same" : "distinct";
14
+ if (complete(left) && complete(right))
15
+ return left.identityScope === right.identityScope && left.dev === right.dev && left.ino === right.ino ? "same" : "distinct";
16
+ if (opaqueComplete(left) && opaqueComplete(right))
17
+ return left.identityScope === right.identityScope && left.opaqueIdentity === right.opaqueIdentity ? "same" : "distinct";
18
+ if ((complete(left) || opaqueComplete(left)) && (complete(right) || opaqueComplete(right)) && left.identityScope !== right.identityScope)
19
+ return "distinct";
20
+ return "unknown";
12
21
  }
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } 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, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
4
4
  export interface MountFileSystemOptions {
@@ -35,6 +35,7 @@ export declare class MountFileSystem implements FileSystem {
35
35
  unlink(path: string, options?: FsOptions): Promise<void>;
36
36
  rm(path: string, options?: RemoveOptions): Promise<void>;
37
37
  private localStaging;
38
+ publishFileConditional(path: string, source: ByteSource, options: ConditionalFilePublicationOptions): Promise<FileStat>;
38
39
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
39
40
  removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
40
41
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
@@ -22,7 +22,7 @@ function fail(code) {
22
22
  throw new FsError(code);
23
23
  }
24
24
  function snapshotStat(stat) {
25
- const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
25
+ const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
26
26
  return {
27
27
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
28
28
  ...(revision === undefined ? {} : { revision }),
@@ -31,6 +31,8 @@ function snapshotStat(stat) {
31
31
  ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
32
32
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
33
33
  ...(identityScope === undefined ? {} : { identityScope }),
34
+ ...(opaqueIdentity === undefined ? {} : { opaqueIdentity }),
35
+ ...(opaqueVersion === undefined ? {} : { opaqueVersion }),
34
36
  ...(ino === undefined ? {} : { ino }),
35
37
  ...(dev === undefined ? {} : { dev }),
36
38
  ...(rdevMajor === undefined ? {} : { rdevMajor }),
@@ -96,7 +98,7 @@ export class MountFileSystem {
96
98
  const common = (capability) => {
97
99
  const optional = {
98
100
  open: ["open"],
99
- atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
101
+ atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
100
102
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
101
103
  descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
102
104
  };
@@ -114,7 +116,7 @@ export class MountFileSystem {
114
116
  return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
115
117
  };
116
118
  const semantics = Object.fromEntries([
117
- "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
119
+ "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
118
120
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
119
121
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
120
122
  "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "atomicResize", "symlinks", "hardlinks", "permissions", "timestamps",
@@ -581,6 +583,19 @@ export class MountFileSystem {
581
583
  file: { path: file.local, stat: staging.file.stat },
582
584
  } };
583
585
  }
586
+ publishFileConditional(path, source, options) {
587
+ return this.operation("publishFileConditional", path, options, async () => {
588
+ const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
589
+ if (this.protected(location.path))
590
+ fail("EBUSY");
591
+ this.mutable(location);
592
+ const backend = location.mount.backend;
593
+ await requireOwnedMutation(backend, location.local, "atomicFilePublication", options, options.expected === null);
594
+ if (!backend.publishFileConditional)
595
+ fail("ENOTSUP");
596
+ return snapshotStat(await backend.publishFileConditional(location.local, source, options));
597
+ }, undefined, true);
598
+ }
584
599
  writeFileConditional(path, data, options) {
585
600
  return this.operation("writeFileConditional", path, options, async () => {
586
601
  const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
@@ -8,7 +8,7 @@ import { compareIdentity } from "../mount/identity.js";
8
8
  import { compareEntries, registerEntryView } from "../mount/comparison.js";
9
9
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
10
10
  function snapshotStat(stat) {
11
- const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
11
+ const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
12
12
  return {
13
13
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
14
14
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
@@ -17,6 +17,8 @@ function snapshotStat(stat) {
17
17
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
18
18
  ...(revision === undefined ? {} : { revision }),
19
19
  ...(identityScope === undefined ? {} : { identityScope }),
20
+ ...(opaqueIdentity === undefined ? {} : { opaqueIdentity }),
21
+ ...(opaqueVersion === undefined ? {} : { opaqueVersion }),
20
22
  ...(ino === undefined ? {} : { ino }),
21
23
  ...(dev === undefined ? {} : { dev }),
22
24
  ...(rdevMajor === undefined ? {} : { rdevMajor }),
@@ -118,7 +120,7 @@ export class OverlayFileSystem {
118
120
  this.capabilities = Object.freeze({
119
121
  ...semantics,
120
122
  open: false,
121
- atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
123
+ atomicFilePublication: false, atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
122
124
  implicitDirectories: false,
123
125
  readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
124
126
  : upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
@@ -439,7 +439,7 @@ export function withFileSystemQuota(fs, options) {
439
439
  // capabilities and methods without violating invariants on own properties.
440
440
  return new Proxy(Object.create(fs), {
441
441
  get(_target, property) {
442
- if (property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
442
+ if (property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
443
443
  return undefined;
444
444
  if (property === "capabilities")
445
445
  return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }) });
@@ -11,7 +11,7 @@ function readOnly(syscall, path, dest) {
11
11
  throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
12
12
  }
13
13
  function snapshotStat(stat) {
14
- const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
14
+ const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
15
15
  return {
16
16
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
17
17
  ...(revision === undefined ? {} : { revision }),
@@ -20,6 +20,8 @@ function snapshotStat(stat) {
20
20
  ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
21
21
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
22
22
  ...(identityScope === undefined ? {} : { identityScope }),
23
+ ...(opaqueIdentity === undefined ? {} : { opaqueIdentity }),
24
+ ...(opaqueVersion === undefined ? {} : { opaqueVersion }),
23
25
  ...(ino === undefined ? {} : { ino }),
24
26
  ...(dev === undefined ? {} : { dev }),
25
27
  ...(rdevMajor === undefined ? {} : { rdevMajor }),
@@ -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", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
7
+ "publishFileConditional", "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,13 +198,17 @@ 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", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory"].includes(String(property)))
201
+ if (["publishFileConditional", "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
- const options = args[property === "createStagedFile" ? 3 : property === "publishStagedFile" || property === "writeFileConditional" ? 2 : 1];
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 === "removeTreeConditional" ? "atomicTreeRemoval" : property === "prepareDirectory" ? "atomicDirectoryMetadata" : property === "writeFileConditional" || property === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", options ?? {}, create);
204
+ const options = args[property === "createStagedFile" ? 3 : property === "publishFileConditional" || property === "publishStagedFile" || property === "writeFileConditional" ? 2 : 1];
205
+ const create = property === "createStagedFile" || (property === "publishFileConditional" || property === "writeFileConditional" || property === "prepareDirectory") && options !== undefined && "expected" in options && options.expected === null;
206
+ await requireOwnedMutation(original, path, property === "publishFileConditional" ? "atomicFilePublication" : property === "removeEntryConditional" ? "atomicEntryRemoval" : property === "removeTreeConditional" ? "atomicTreeRemoval" : property === "prepareDirectory" ? "atomicDirectoryMetadata" : property === "writeFileConditional" || property === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", options ?? {}, create);
207
207
  assertOpen(options);
208
+ if (property === "publishFileConditional") {
209
+ args[1] = wrapStream(args[1], options);
210
+ args[2] = resizeOptions(options ?? {});
211
+ }
208
212
  return Reflect.apply(method, original, args);
209
213
  })();
210
214
  if (property === "compareEntry") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.672",
3
+ "version": "0.1.674",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",