@poe-platform/safe-fs 0.1.673 → 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 +11 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +15 -0
- package/dist/safe-fs/fs/capabilities.d.ts +1 -1
- package/dist/safe-fs/fs/capabilities.js +4 -2
- package/dist/safe-fs/fs/devices/index.d.ts +2 -1
- package/dist/safe-fs/fs/devices/index.js +9 -2
- package/dist/safe-fs/fs/mount/identity.js +12 -3
- package/dist/safe-fs/fs/mount/index.d.ts +2 -1
- package/dist/safe-fs/fs/mount/index.js +18 -3
- package/dist/safe-fs/fs/overlay/index.js +4 -2
- package/dist/safe-fs/fs/quota/index.js +1 -1
- package/dist/safe-fs/fs/readonly/index.js +3 -1
- package/dist/safe-fs/fs/scoped.js +9 -5
- package/package.json +1 -1
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"],
|
|
@@ -441,6 +441,13 @@ export class DeviceFileSystem {
|
|
|
441
441
|
throw new FsError("ENOTSUP", { path });
|
|
442
442
|
await this.#filesystem.rmdir(path, options);
|
|
443
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
|
+
}
|
|
444
451
|
async writeFileConditional(path, data, options) {
|
|
445
452
|
await this.#mutable(path, options, false);
|
|
446
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
|
|
12
|
+
if (!left || !right)
|
|
9
13
|
return "unknown";
|
|
10
|
-
|
|
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") {
|