@poe-platform/safe-fs 0.1.545 → 0.1.547
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/dist/safe-fs/contracts/filesystem.d.ts +53 -0
- package/dist/safe-fs/fs/capabilities.d.ts +3 -1
- package/dist/safe-fs/fs/capabilities.js +20 -2
- package/dist/safe-fs/fs/devices/index.d.ts +7 -1
- package/dist/safe-fs/fs/devices/index.js +48 -2
- package/dist/safe-fs/fs/memory/index.d.ts +12 -1
- package/dist/safe-fs/fs/memory/index.js +186 -1
- package/dist/safe-fs/fs/mount/index.d.ts +8 -1
- package/dist/safe-fs/fs/mount/index.js +106 -7
- package/dist/safe-fs/fs/overlay/index.js +3 -1
- package/dist/safe-fs/fs/quota/index.js +1 -1
- package/dist/safe-fs/fs/readonly/index.js +2 -1
- package/dist/safe-fs/fs/scoped.d.ts +2 -0
- package/dist/safe-fs/fs/scoped.js +20 -5
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ export interface FileStat {
|
|
|
11
11
|
readonly atimeMs: number;
|
|
12
12
|
readonly ctimeMs: number;
|
|
13
13
|
readonly birthtimeMs?: number;
|
|
14
|
+
readonly revision?: number;
|
|
14
15
|
readonly identityScope?: object | symbol;
|
|
15
16
|
readonly ino?: number;
|
|
16
17
|
readonly dev?: number;
|
|
@@ -51,6 +52,9 @@ export interface FileSystemCapabilities {
|
|
|
51
52
|
readonly permissions?: boolean;
|
|
52
53
|
readonly timestamps?: boolean;
|
|
53
54
|
readonly atomicRename?: boolean;
|
|
55
|
+
readonly atomicFileStaging?: boolean;
|
|
56
|
+
readonly atomicFileMutation?: boolean;
|
|
57
|
+
readonly atomicDirectoryMetadata?: boolean;
|
|
54
58
|
readonly atomicRenameNoReplace?: boolean;
|
|
55
59
|
readonly snapshotRmdir?: boolean;
|
|
56
60
|
readonly streamingRead?: boolean;
|
|
@@ -141,8 +145,57 @@ export interface ReadStreamOptions extends FsOptions {
|
|
|
141
145
|
readonly endExclusive?: number;
|
|
142
146
|
readonly chunkSize?: number;
|
|
143
147
|
}
|
|
148
|
+
export interface ConditionalWriteFileOptions extends FsOptions {
|
|
149
|
+
readonly parent: FileStat;
|
|
150
|
+
readonly expected: FileStat | null;
|
|
151
|
+
readonly append?: boolean;
|
|
152
|
+
readonly mode?: number;
|
|
153
|
+
}
|
|
154
|
+
export interface ConditionalRemoveFileOptions extends FsOptions {
|
|
155
|
+
readonly parent: FileStat;
|
|
156
|
+
readonly expected: FileStat;
|
|
157
|
+
}
|
|
158
|
+
export interface FileStagingEntry {
|
|
159
|
+
readonly path: string;
|
|
160
|
+
readonly stat: FileStat;
|
|
161
|
+
}
|
|
162
|
+
export interface FileStaging {
|
|
163
|
+
readonly parent: FileStagingEntry;
|
|
164
|
+
readonly directory: FileStagingEntry;
|
|
165
|
+
readonly file: FileStagingEntry;
|
|
166
|
+
}
|
|
167
|
+
export type StagedFileContent = {
|
|
168
|
+
readonly type: "file";
|
|
169
|
+
readonly data: Uint8Array;
|
|
170
|
+
} | {
|
|
171
|
+
readonly type: "symlink";
|
|
172
|
+
readonly target: string;
|
|
173
|
+
};
|
|
174
|
+
export interface CreateStagedFileOptions extends FsOptions {
|
|
175
|
+
readonly parent: FileStat;
|
|
176
|
+
readonly mode?: number;
|
|
177
|
+
readonly atimeMs?: number;
|
|
178
|
+
readonly mtimeMs?: number;
|
|
179
|
+
}
|
|
180
|
+
export interface PublishStagedFileOptions extends FsOptions {
|
|
181
|
+
readonly parent: FileStat;
|
|
182
|
+
readonly destination: FileStat | null;
|
|
183
|
+
}
|
|
184
|
+
export interface PrepareDirectoryOptions extends FsOptions {
|
|
185
|
+
readonly expected: FileStat | null;
|
|
186
|
+
readonly parent: FileStat;
|
|
187
|
+
readonly mode?: number;
|
|
188
|
+
readonly atimeMs?: number;
|
|
189
|
+
readonly mtimeMs?: number;
|
|
190
|
+
}
|
|
144
191
|
export interface FileSystem {
|
|
192
|
+
writeFileConditional?(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
193
|
+
removeFileConditional?(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
145
194
|
readonly capabilities: FileSystemCapabilities;
|
|
195
|
+
prepareDirectory?(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
196
|
+
createStagedFile?(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
197
|
+
publishStagedFile?(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
198
|
+
removeStagedFile?(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
146
199
|
openReadFile?(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
147
200
|
openResizeFile?(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
148
201
|
/** Missing targets reject ENOENT unless create is true; existing bytes survive refusals. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileReadHandle, FileResizeHandle, FileSystem, FileSystemCapabilities, OpenReadFileOptions, OpenResizeFileOptions } from "../contracts/filesystem.js";
|
|
1
|
+
import type { FsOptions, FileReadHandle, FileResizeHandle, FileSystem, FileSystemCapabilities, OpenReadFileOptions, OpenResizeFileOptions } from "../contracts/filesystem.js";
|
|
2
2
|
export declare function retainedReadCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
|
|
3
3
|
export declare function openRetainedReadFile(filesystem: FileSystem, path: string, options: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
4
4
|
export declare function requireCapabilities(...values: readonly (boolean | undefined)[]): boolean | undefined;
|
|
@@ -6,3 +6,5 @@ export declare function retainedResizeCapabilities(filesystem: FileSystem, capab
|
|
|
6
6
|
export declare function openRetainedResizeFile(filesystem: FileSystem, path: string, options: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
7
7
|
export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
8
8
|
export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
9
|
+
export declare function ownedMutationCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
|
|
10
|
+
export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "atomicFileMutation" | "atomicFileStaging" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
|
|
@@ -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, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
112
112
|
atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
|
|
113
113
|
});
|
|
114
114
|
}
|
|
@@ -116,8 +116,26 @@ 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,
|
|
119
|
+
return Object.freeze({ ...rest, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
120
120
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
121
121
|
...(streamingAppend === undefined ? {} : { streamingAppend }),
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
export function ownedMutationCapabilities(filesystem, capabilities = filesystem.capabilities) {
|
|
125
|
+
const unavailable = {};
|
|
126
|
+
if (capabilities.atomicFileMutation === true && (capabilities.readOnly === true || typeof filesystem.writeFileConditional !== "function" || typeof filesystem.removeFileConditional !== "function"))
|
|
127
|
+
unavailable.atomicFileMutation = false;
|
|
128
|
+
if (capabilities.atomicFileStaging === true && (capabilities.readOnly === true
|
|
129
|
+
|| typeof filesystem.createStagedFile !== "function" || typeof filesystem.publishStagedFile !== "function" || typeof filesystem.removeStagedFile !== "function"))
|
|
130
|
+
unavailable.atomicFileStaging = false;
|
|
131
|
+
if (capabilities.atomicDirectoryMetadata === true && (capabilities.readOnly === true || typeof filesystem.prepareDirectory !== "function"))
|
|
132
|
+
unavailable.atomicDirectoryMetadata = false;
|
|
133
|
+
return Object.keys(unavailable).length ? { ...capabilities, ...unavailable } : capabilities;
|
|
134
|
+
}
|
|
135
|
+
export async function requireOwnedMutation(filesystem, path, capability, options, create = false) {
|
|
136
|
+
options.signal?.throwIfAborted();
|
|
137
|
+
const capabilities = ownedMutationCapabilities(filesystem, await filesystem.capabilitiesFor?.(path, create ? { ...options, create: true } : options) ?? filesystem.capabilities);
|
|
138
|
+
options.signal?.throwIfAborted();
|
|
139
|
+
if (capabilities[capability] !== true)
|
|
140
|
+
throw new FsError("ENOTSUP", { path, syscall: capability });
|
|
141
|
+
}
|
|
@@ -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 } 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, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export declare class DeviceFileSystem implements FileSystem {
|
|
4
4
|
#private;
|
|
@@ -22,6 +22,12 @@ export declare class DeviceFileSystem implements FileSystem {
|
|
|
22
22
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
23
23
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
24
24
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
25
|
+
writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
26
|
+
removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
27
|
+
createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
28
|
+
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
29
|
+
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
30
|
+
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
25
31
|
rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
|
|
26
32
|
copyFile(source: string, destination: string, options?: CopyFileOptions): Promise<void>;
|
|
27
33
|
access(path: string, mode?: number, options?: FsOptions): Promise<void>;
|
|
@@ -3,7 +3,7 @@ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admissi
|
|
|
3
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
|
-
import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities } from "../capabilities.js";
|
|
6
|
+
import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
|
|
7
7
|
import { pathNamespace } from "../path-namespace.js";
|
|
8
8
|
const views = new WeakMap();
|
|
9
9
|
const deviceCapabilities = Object.freeze({
|
|
@@ -13,11 +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
|
+
atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
16
17
|
atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
|
|
17
18
|
});
|
|
18
19
|
function globalCapabilities(filesystem) {
|
|
19
20
|
const capabilities = { readOnly: false };
|
|
20
21
|
const optional = {
|
|
22
|
+
atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
21
23
|
streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
|
|
22
24
|
streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
23
25
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
|
|
@@ -128,7 +130,7 @@ export class DeviceFileSystem {
|
|
|
128
130
|
options.signal?.throwIfAborted();
|
|
129
131
|
const selected = query === undefined || query === null ? undefined : await Reflect.apply(query, this.#filesystem, [path, options]);
|
|
130
132
|
options.signal?.throwIfAborted();
|
|
131
|
-
const observed = selected ?? this.#filesystem.capabilities;
|
|
133
|
+
const observed = ownedMutationCapabilities(this.#filesystem, selected ?? this.#filesystem.capabilities);
|
|
132
134
|
options.signal?.throwIfAborted();
|
|
133
135
|
const capabilities = observed.retainedResize === true ? retainedResizeCapabilities(this.#filesystem, observed) : observed;
|
|
134
136
|
const unavailable = {};
|
|
@@ -351,6 +353,50 @@ export class DeviceFileSystem {
|
|
|
351
353
|
throw new FsError("ENOTSUP", { path });
|
|
352
354
|
await this.#filesystem.rmdir(path, options);
|
|
353
355
|
}
|
|
356
|
+
async writeFileConditional(path, data, options) {
|
|
357
|
+
await this.#mutable(path, options, false);
|
|
358
|
+
await requireOwnedMutation(this.#filesystem, path, "atomicFileMutation", options, options.expected === null);
|
|
359
|
+
if (!this.#filesystem.writeFileConditional)
|
|
360
|
+
throw new FsError("ENOTSUP", { path });
|
|
361
|
+
return this.#filesystem.writeFileConditional(path, data, options);
|
|
362
|
+
}
|
|
363
|
+
async removeFileConditional(path, options) {
|
|
364
|
+
await this.#mutable(path, options, false);
|
|
365
|
+
await requireOwnedMutation(this.#filesystem, path, "atomicFileMutation", options);
|
|
366
|
+
if (!this.#filesystem.removeFileConditional)
|
|
367
|
+
throw new FsError("ENOTSUP", { path });
|
|
368
|
+
await this.#filesystem.removeFileConditional(path, options);
|
|
369
|
+
}
|
|
370
|
+
async createStagedFile(path, name, content, options) {
|
|
371
|
+
await this.#mutable(path, options, false);
|
|
372
|
+
await requireOwnedMutation(this.#filesystem, path, "atomicFileStaging", options, true);
|
|
373
|
+
if (!this.#filesystem.createStagedFile)
|
|
374
|
+
throw new FsError("ENOTSUP", { path });
|
|
375
|
+
return this.#filesystem.createStagedFile(path, name, content, options);
|
|
376
|
+
}
|
|
377
|
+
async publishStagedFile(staging, destination, options) {
|
|
378
|
+
for (const path of [staging.directory.path, staging.file.path, destination])
|
|
379
|
+
await this.#mutable(path, options, false);
|
|
380
|
+
await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
|
|
381
|
+
if (!this.#filesystem.publishStagedFile)
|
|
382
|
+
throw new FsError("ENOTSUP", { path: destination });
|
|
383
|
+
await this.#filesystem.publishStagedFile(staging, destination, options);
|
|
384
|
+
}
|
|
385
|
+
async removeStagedFile(staging, options = {}) {
|
|
386
|
+
for (const path of [staging.directory.path, staging.file.path])
|
|
387
|
+
await this.#mutable(path, options, false);
|
|
388
|
+
await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
|
|
389
|
+
if (!this.#filesystem.removeStagedFile)
|
|
390
|
+
throw new FsError("ENOTSUP", { path: staging.directory.path });
|
|
391
|
+
await this.#filesystem.removeStagedFile(staging, options);
|
|
392
|
+
}
|
|
393
|
+
async prepareDirectory(path, options) {
|
|
394
|
+
await this.#mutable(path, options, false);
|
|
395
|
+
await requireOwnedMutation(this.#filesystem, path, "atomicDirectoryMetadata", options, options.expected === null);
|
|
396
|
+
if (!this.#filesystem.prepareDirectory)
|
|
397
|
+
throw new FsError("ENOTSUP", { path });
|
|
398
|
+
return this.#filesystem.prepareDirectory(path, options);
|
|
399
|
+
}
|
|
354
400
|
async rename(source, destination, options = {}) {
|
|
355
401
|
await this.#mutable(source, options, false);
|
|
356
402
|
await this.#mutable(destination, options, false);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
import { type MemoryFileSystemOptions } from "./limits.js";
|
|
4
4
|
export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
|
|
@@ -33,6 +33,9 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
33
33
|
permissions: true;
|
|
34
34
|
timestamps: true;
|
|
35
35
|
atomicRename: true;
|
|
36
|
+
atomicFileStaging: true;
|
|
37
|
+
atomicFileMutation: true;
|
|
38
|
+
atomicDirectoryMetadata: true;
|
|
36
39
|
streamingRead: true;
|
|
37
40
|
retainedRead: true;
|
|
38
41
|
readonly retainedResize: boolean;
|
|
@@ -70,6 +73,14 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
70
73
|
private writeData;
|
|
71
74
|
private writeAt;
|
|
72
75
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
76
|
+
private expectEntry;
|
|
77
|
+
private stagingLocations;
|
|
78
|
+
createStagedFile(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
79
|
+
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
80
|
+
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
81
|
+
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
82
|
+
writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
83
|
+
removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
73
84
|
writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
|
|
74
85
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
75
86
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
@@ -66,6 +66,8 @@ export class MemoryFileSystem {
|
|
|
66
66
|
permissions: true,
|
|
67
67
|
timestamps: true,
|
|
68
68
|
atomicRename: true,
|
|
69
|
+
atomicFileStaging: true, atomicFileMutation: true,
|
|
70
|
+
atomicDirectoryMetadata: true,
|
|
69
71
|
streamingRead: true,
|
|
70
72
|
retainedRead: true,
|
|
71
73
|
get retainedResize() { return stockRetainedResize(filesystem); },
|
|
@@ -115,7 +117,7 @@ export class MemoryFileSystem {
|
|
|
115
117
|
metadata(mode) {
|
|
116
118
|
const now = Date.now();
|
|
117
119
|
return {
|
|
118
|
-
mode, ino: this.nextInode++, nlink: 1, references: 0,
|
|
120
|
+
mode, ino: this.nextInode++, nlink: 1, references: 0, revision: 0,
|
|
119
121
|
atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now,
|
|
120
122
|
};
|
|
121
123
|
}
|
|
@@ -178,6 +180,7 @@ export class MemoryFileSystem {
|
|
|
178
180
|
this.fail("EINVAL", syscall, path);
|
|
179
181
|
}
|
|
180
182
|
changed(node) {
|
|
183
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
181
184
|
node.mtimeMs = node.ctimeMs = Date.now();
|
|
182
185
|
}
|
|
183
186
|
resolve(path, syscall, options = {}) {
|
|
@@ -259,6 +262,7 @@ export class MemoryFileSystem {
|
|
|
259
262
|
snapshot(node) {
|
|
260
263
|
return {
|
|
261
264
|
type: node.type,
|
|
265
|
+
...(Number.isSafeInteger(node.revision) ? { revision: node.revision } : {}),
|
|
262
266
|
preferredIoBlockSize: 4096,
|
|
263
267
|
size: node.type === "file" ? node.data.byteLength
|
|
264
268
|
: node.type === "symlink" ? new TextEncoder().encode(node.target).byteLength : 0,
|
|
@@ -405,6 +409,180 @@ export class MemoryFileSystem {
|
|
|
405
409
|
node.atimeMs = Date.now();
|
|
406
410
|
return new Uint8Array(node.data);
|
|
407
411
|
}
|
|
412
|
+
expectEntry(node, expected, path, unchanged = true) {
|
|
413
|
+
if (expected === null) {
|
|
414
|
+
if (node)
|
|
415
|
+
this.fail("EAGAIN", "fileStaging", path);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (!node)
|
|
419
|
+
this.fail("EAGAIN", "fileStaging", path);
|
|
420
|
+
const current = this.snapshot(node);
|
|
421
|
+
if (!expected.identityScope || expected.identityScope !== current.identityScope
|
|
422
|
+
|| !Number.isSafeInteger(expected.ino) || !Number.isSafeInteger(expected.dev))
|
|
423
|
+
this.fail("ENOTSUP", "fileStaging", path);
|
|
424
|
+
if (current.ino !== expected.ino || current.dev !== expected.dev || current.type !== expected.type)
|
|
425
|
+
this.fail("EAGAIN", "fileStaging", path);
|
|
426
|
+
if (unchanged) {
|
|
427
|
+
if (!Number.isSafeInteger(expected.revision) || !Number.isSafeInteger(current.revision))
|
|
428
|
+
this.fail("ENOTSUP", "fileStaging", path);
|
|
429
|
+
if (current.revision !== expected.revision || current.size !== expected.size || current.mode !== expected.mode
|
|
430
|
+
|| current.nlink !== expected.nlink || current.mtimeMs !== expected.mtimeMs || current.ctimeMs !== expected.ctimeMs)
|
|
431
|
+
this.fail("EAGAIN", "fileStaging", path);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
stagingLocations(staging) {
|
|
435
|
+
const parent = this.entry(staging.parent.path, "fileStaging");
|
|
436
|
+
this.expectEntry(parent.node, staging.parent.stat, staging.parent.path, false);
|
|
437
|
+
if (parent.node.type !== "directory")
|
|
438
|
+
this.fail("ENOTDIR", "fileStaging", staging.parent.path);
|
|
439
|
+
const directory = this.entry(staging.directory.path, "fileStaging");
|
|
440
|
+
this.expectEntry(directory.node, staging.directory.stat, staging.directory.path, false);
|
|
441
|
+
if (directory.parent !== parent.node || directory.node.type !== "directory" || (directory.node.mode & 0o777) !== 0o700)
|
|
442
|
+
this.fail("EAGAIN", "fileStaging", staging.directory.path);
|
|
443
|
+
const file = this.entry(staging.file.path, "fileStaging", true);
|
|
444
|
+
if (file.parent !== directory.node || !["file", "symlink"].includes(staging.file.stat.type))
|
|
445
|
+
this.fail("EAGAIN", "fileStaging", staging.file.path);
|
|
446
|
+
return { directory, file };
|
|
447
|
+
}
|
|
448
|
+
async createStagedFile(directoryPath, name, content, options) {
|
|
449
|
+
options.signal?.throwIfAborted();
|
|
450
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
|
|
451
|
+
this.fail("EINVAL", "createStagedFile", directoryPath);
|
|
452
|
+
const location = this.entry(directoryPath, "createStagedFile", true);
|
|
453
|
+
if (location.node)
|
|
454
|
+
this.fail("EEXIST", "createStagedFile", directoryPath);
|
|
455
|
+
this.expectEntry(location.parent, options.parent, directoryPath, false);
|
|
456
|
+
this.permission(location.parent, 3, "createStagedFile", directoryPath);
|
|
457
|
+
const mode = this.mode(options.mode, content.type === "file" ? 0o666 : 0o777, "createStagedFile", directoryPath);
|
|
458
|
+
for (const time of [options.atimeMs, options.mtimeMs])
|
|
459
|
+
if (time !== undefined && !Number.isFinite(time))
|
|
460
|
+
this.fail("EINVAL", "createStagedFile", directoryPath);
|
|
461
|
+
if (content.type === "file" && !(content.data instanceof Uint8Array))
|
|
462
|
+
throw new TypeError("Staged files require Uint8Array data");
|
|
463
|
+
if (content.type === "symlink")
|
|
464
|
+
this.validatePath(content.target, "createStagedFile");
|
|
465
|
+
const retained = (location.name.length + name.length) * 2 + (content.type === "symlink" ? content.target.length * 2 : 0);
|
|
466
|
+
this.ledger.reserve(retained, 4, "createStagedFile", directoryPath);
|
|
467
|
+
let allocation;
|
|
468
|
+
let directory;
|
|
469
|
+
let file;
|
|
470
|
+
try {
|
|
471
|
+
if (content.type === "file")
|
|
472
|
+
allocation = this.bytes(content.data, "createStagedFile", directoryPath);
|
|
473
|
+
directory = this.directory(0o700);
|
|
474
|
+
file = content.type === "file"
|
|
475
|
+
? { ...this.metadata(typeModes.file | mode), type: "file", data: allocation.data, allocation: allocation }
|
|
476
|
+
: { ...this.metadata(typeModes.symlink | mode), type: "symlink", target: content.target };
|
|
477
|
+
if (options.atimeMs !== undefined)
|
|
478
|
+
file.atimeMs = options.atimeMs;
|
|
479
|
+
if (options.mtimeMs !== undefined)
|
|
480
|
+
file.mtimeMs = options.mtimeMs;
|
|
481
|
+
directory.entries.set(name, file);
|
|
482
|
+
location.parent.entries.set(location.name, directory);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
allocation?.release();
|
|
486
|
+
this.ledger.release(retained, 4);
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
this.changed(location.parent);
|
|
490
|
+
const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
|
|
491
|
+
const receipt = (path, node) => Object.freeze({ path, stat: Object.freeze(this.snapshot(node)) });
|
|
492
|
+
return Object.freeze({ parent: receipt(parentPath, location.parent), directory: receipt(location.path, directory), file: receipt(`${location.path}/${name}`, file) });
|
|
493
|
+
}
|
|
494
|
+
async publishStagedFile(staging, destination, options) {
|
|
495
|
+
options.signal?.throwIfAborted();
|
|
496
|
+
const { directory, file } = this.stagingLocations(staging);
|
|
497
|
+
this.expectEntry(file.node, staging.file.stat, staging.file.path);
|
|
498
|
+
const target = this.entry(destination, "publishStagedFile", true);
|
|
499
|
+
this.expectEntry(target.parent, options.parent, destination, false);
|
|
500
|
+
this.expectEntry(target.node, options.destination, destination);
|
|
501
|
+
if (target.parent === directory.node || target.node === directory.node || target.node === file.node)
|
|
502
|
+
this.fail("EINVAL", "publishStagedFile", destination);
|
|
503
|
+
if (target.node && (target.node.type !== "file" || target.node.nlink !== 1))
|
|
504
|
+
this.fail("EAGAIN", "publishStagedFile", destination);
|
|
505
|
+
return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...options, noReplace: options.destination === null });
|
|
506
|
+
}
|
|
507
|
+
async removeStagedFile(staging, options = {}) {
|
|
508
|
+
options.signal?.throwIfAborted();
|
|
509
|
+
const { directory, file } = this.stagingLocations(staging);
|
|
510
|
+
if (file.node)
|
|
511
|
+
this.expectEntry(file.node, staging.file.stat, staging.file.path);
|
|
512
|
+
const node = directory.node;
|
|
513
|
+
if (node.entries.size !== (file.node ? 1 : 0))
|
|
514
|
+
this.fail("ENOTEMPTY", "removeStagedFile", staging.directory.path);
|
|
515
|
+
this.permission(directory.parent, 3, "removeStagedFile", staging.directory.path);
|
|
516
|
+
this.permission(node, 3, "removeStagedFile", staging.file.path);
|
|
517
|
+
directory.parent.entries.delete(directory.name);
|
|
518
|
+
if (file.node) {
|
|
519
|
+
node.entries.delete(file.name);
|
|
520
|
+
this.ledger.release(file.name.length * 2, 1);
|
|
521
|
+
file.node.nlink--;
|
|
522
|
+
file.node.ctimeMs = Date.now();
|
|
523
|
+
file.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, file.node.revision + 1);
|
|
524
|
+
this.releaseNode(file.node);
|
|
525
|
+
}
|
|
526
|
+
this.ledger.release(directory.name.length * 2, 1);
|
|
527
|
+
node.nlink = 0;
|
|
528
|
+
node.ctimeMs = Date.now();
|
|
529
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
530
|
+
this.releaseNode(node);
|
|
531
|
+
this.changed(directory.parent);
|
|
532
|
+
}
|
|
533
|
+
async prepareDirectory(path, options) {
|
|
534
|
+
options.signal?.throwIfAborted();
|
|
535
|
+
const location = this.entry(path, "prepareDirectory", true);
|
|
536
|
+
this.expectEntry(location.parent, options.parent, path, false);
|
|
537
|
+
this.expectEntry(location.node, options.expected, path, false);
|
|
538
|
+
const mode = options.mode === undefined ? undefined : this.mode(options.mode, 0, "prepareDirectory", path);
|
|
539
|
+
for (const time of [options.atimeMs, options.mtimeMs])
|
|
540
|
+
if (time !== undefined && !Number.isFinite(time))
|
|
541
|
+
this.fail("EINVAL", "prepareDirectory", path);
|
|
542
|
+
let node = location.node;
|
|
543
|
+
if (!node) {
|
|
544
|
+
this.permission(location.parent, 3, "prepareDirectory", path);
|
|
545
|
+
node = this.addNode(location.parent, location.name, () => this.directory(mode ?? 0o777), "prepareDirectory", path);
|
|
546
|
+
}
|
|
547
|
+
if (node.type !== "directory")
|
|
548
|
+
this.fail("ENOTDIR", "prepareDirectory", path);
|
|
549
|
+
if (mode !== undefined)
|
|
550
|
+
node.mode = typeModes.directory | mode;
|
|
551
|
+
if (options.atimeMs !== undefined)
|
|
552
|
+
node.atimeMs = options.atimeMs;
|
|
553
|
+
if (options.mtimeMs !== undefined)
|
|
554
|
+
node.mtimeMs = options.mtimeMs;
|
|
555
|
+
node.ctimeMs = Date.now();
|
|
556
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
557
|
+
return Object.freeze(this.snapshot(node));
|
|
558
|
+
}
|
|
559
|
+
async writeFileConditional(path, data, options) {
|
|
560
|
+
options.signal?.throwIfAborted();
|
|
561
|
+
const location = this.entry(path, "writeFileConditional", true);
|
|
562
|
+
this.expectEntry(location.parent, options.parent, path, false);
|
|
563
|
+
this.expectEntry(location.node, options.expected, path);
|
|
564
|
+
if (location.node && location.node.type !== "file")
|
|
565
|
+
this.fail("EINVAL", "writeFileConditional", path);
|
|
566
|
+
this.writeData(path, data, { ...(options.mode === undefined ? {} : { mode: options.mode }), flag: options.append ? "a" : "w" }, "writeFileConditional");
|
|
567
|
+
return Object.freeze(this.snapshot(location.parent.entries.get(location.name)));
|
|
568
|
+
}
|
|
569
|
+
async removeFileConditional(path, options) {
|
|
570
|
+
options.signal?.throwIfAborted();
|
|
571
|
+
const location = this.entry(path, "removeFileConditional", true);
|
|
572
|
+
this.expectEntry(location.parent, options.parent, path, false);
|
|
573
|
+
this.expectEntry(location.node, options.expected, path);
|
|
574
|
+
const node = location.node;
|
|
575
|
+
if (node.type !== "file")
|
|
576
|
+
this.fail("EINVAL", "removeFileConditional", path);
|
|
577
|
+
this.permission(location.parent, 3, "removeFileConditional", path);
|
|
578
|
+
location.parent.entries.delete(location.name);
|
|
579
|
+
this.ledger.release(location.name.length * 2, 1);
|
|
580
|
+
node.nlink--;
|
|
581
|
+
node.ctimeMs = Date.now();
|
|
582
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
583
|
+
this.releaseNode(node);
|
|
584
|
+
this.changed(location.parent);
|
|
585
|
+
}
|
|
408
586
|
async writeFile(path, data, options = {}) {
|
|
409
587
|
options.signal?.throwIfAborted();
|
|
410
588
|
this.writeData(path, data, options, "writeFile");
|
|
@@ -467,6 +645,7 @@ export class MemoryFileSystem {
|
|
|
467
645
|
this.ledger.release(location.name.length * 2, 1);
|
|
468
646
|
node.nlink = 0;
|
|
469
647
|
node.ctimeMs = Date.now();
|
|
648
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
470
649
|
this.releaseNode(node);
|
|
471
650
|
this.changed(location.parent);
|
|
472
651
|
}
|
|
@@ -508,6 +687,7 @@ export class MemoryFileSystem {
|
|
|
508
687
|
}
|
|
509
688
|
entry.nlink--;
|
|
510
689
|
entry.ctimeMs = Date.now();
|
|
690
|
+
entry.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, entry.revision + 1);
|
|
511
691
|
this.releaseNode(entry);
|
|
512
692
|
}
|
|
513
693
|
this.changed(location.parent);
|
|
@@ -554,6 +734,7 @@ export class MemoryFileSystem {
|
|
|
554
734
|
this.ledger.release(origin.name.length * 2, 1);
|
|
555
735
|
target.node.nlink--;
|
|
556
736
|
target.node.ctimeMs = Date.now();
|
|
737
|
+
target.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, target.node.revision + 1);
|
|
557
738
|
this.releaseNode(target.node);
|
|
558
739
|
}
|
|
559
740
|
else if (nameGrowth < 0)
|
|
@@ -561,6 +742,7 @@ export class MemoryFileSystem {
|
|
|
561
742
|
this.changed(origin.parent);
|
|
562
743
|
this.changed(target.parent);
|
|
563
744
|
node.ctimeMs = Date.now();
|
|
745
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
564
746
|
}
|
|
565
747
|
catch (error) {
|
|
566
748
|
if (error instanceof FsError)
|
|
@@ -636,6 +818,7 @@ export class MemoryFileSystem {
|
|
|
636
818
|
}
|
|
637
819
|
node.nlink++;
|
|
638
820
|
node.ctimeMs = Date.now();
|
|
821
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
639
822
|
this.changed(target.parent);
|
|
640
823
|
}
|
|
641
824
|
async chmod(path, mode, options = {}) {
|
|
@@ -644,6 +827,7 @@ export class MemoryFileSystem {
|
|
|
644
827
|
const node = this.resolve(path, "chmod").node;
|
|
645
828
|
node.mode = typeModes[node.type] | permissions;
|
|
646
829
|
node.ctimeMs = Date.now();
|
|
830
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
647
831
|
}
|
|
648
832
|
async utimes(path, atimeMs, mtimeMs, options = {}) {
|
|
649
833
|
options.signal?.throwIfAborted();
|
|
@@ -653,6 +837,7 @@ export class MemoryFileSystem {
|
|
|
653
837
|
node.atimeMs = atimeMs;
|
|
654
838
|
node.mtimeMs = mtimeMs;
|
|
655
839
|
node.ctimeMs = Date.now();
|
|
840
|
+
node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
|
|
656
841
|
}
|
|
657
842
|
async truncate(path, length = 0, options = {}) {
|
|
658
843
|
options.signal?.throwIfAborted();
|
|
@@ -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 } 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, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export interface MountFileSystemOptions {
|
|
4
4
|
readonly root: FileSystem;
|
|
@@ -31,6 +31,13 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
31
31
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
32
32
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
33
33
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
34
|
+
private localStaging;
|
|
35
|
+
writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
36
|
+
removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
37
|
+
createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
38
|
+
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
39
|
+
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
40
|
+
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
34
41
|
rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
|
|
35
42
|
copyFile(source: string, destination: string, options?: CopyFileOptions): Promise<void>;
|
|
36
43
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
@@ -1,7 +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 { openRetainedReadFile, openRetainedResizeFile, readOnlyCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
|
|
4
|
+
import { requireOwnedMutation, ownedMutationCapabilities, openRetainedReadFile, openRetainedResizeFile, readOnlyCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
|
|
5
5
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
6
6
|
import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
7
7
|
import { pathNamespace } from "../path-namespace.js";
|
|
@@ -21,9 +21,10 @@ function fail(code) {
|
|
|
21
21
|
throw new FsError(code);
|
|
22
22
|
}
|
|
23
23
|
function snapshotStat(stat) {
|
|
24
|
-
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
24
|
+
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
25
25
|
return {
|
|
26
26
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
27
|
+
...(revision === undefined ? {} : { revision }),
|
|
27
28
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
28
29
|
...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
|
|
29
30
|
...(birthtimeMs === undefined ? {} : { birthtimeMs }),
|
|
@@ -90,6 +91,7 @@ export class MountFileSystem {
|
|
|
90
91
|
: mounts.every(({ backend }) => backend.capabilities.append === false) ? false : undefined;
|
|
91
92
|
const common = (capability) => {
|
|
92
93
|
const optional = {
|
|
94
|
+
atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
93
95
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
|
|
94
96
|
descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
95
97
|
};
|
|
@@ -107,7 +109,7 @@ export class MountFileSystem {
|
|
|
107
109
|
return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
|
|
108
110
|
};
|
|
109
111
|
const semantics = Object.fromEntries([
|
|
110
|
-
"read", "stat", "readdir", "realpath", "access",
|
|
112
|
+
"atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access",
|
|
111
113
|
"write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
|
|
112
114
|
"remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
|
|
113
115
|
"streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "atomicResize", "symlinks", "hardlinks", "permissions", "timestamps",
|
|
@@ -132,8 +134,8 @@ export class MountFileSystem {
|
|
|
132
134
|
allowMissing: options.create ?? true,
|
|
133
135
|
...(options.create === undefined ? {} : { resizeCreate: options.create }),
|
|
134
136
|
});
|
|
135
|
-
const observed = await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
136
|
-
?? location.mount.backend.capabilities;
|
|
137
|
+
const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
138
|
+
?? location.mount.backend.capabilities);
|
|
137
139
|
const declared = observed.descriptorWriteStream === true
|
|
138
140
|
&& (typeof location.mount.backend.writeStream !== "function" || observed.readOnly === true || observed.streamingWrite === false)
|
|
139
141
|
? { ...observed, descriptorWriteStream: false } : observed;
|
|
@@ -214,11 +216,12 @@ export class MountFileSystem {
|
|
|
214
216
|
cause: error, syscall, path: globalPath(path), ...(dest === undefined ? {} : { dest: globalPath(dest) }),
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
|
-
async operation(syscall, path, options, action, dest) {
|
|
219
|
+
async operation(syscall, path, options, action, dest, preserveReceipt = false) {
|
|
218
220
|
try {
|
|
219
221
|
options.signal?.throwIfAborted();
|
|
220
222
|
const result = await action();
|
|
221
|
-
|
|
223
|
+
if (!preserveReceipt)
|
|
224
|
+
options.signal?.throwIfAborted();
|
|
222
225
|
return result;
|
|
223
226
|
}
|
|
224
227
|
catch (error) {
|
|
@@ -512,6 +515,102 @@ export class MountFileSystem {
|
|
|
512
515
|
await location.mount.backend.rm(location.local, options);
|
|
513
516
|
});
|
|
514
517
|
}
|
|
518
|
+
async localStaging(staging, options) {
|
|
519
|
+
const parent = await this.resolve(staging.parent.path, options, { followFinal: false, entry: true });
|
|
520
|
+
const directory = await this.resolve(staging.directory.path, options, { followFinal: false, entry: true });
|
|
521
|
+
const file = await this.resolve(staging.file.path, options, { followFinal: false, entry: true, allowMissing: true });
|
|
522
|
+
if (parent.mount !== directory.mount || directory.mount !== file.mount)
|
|
523
|
+
fail("EXDEV");
|
|
524
|
+
if (this.protected(directory.path) || this.protected(file.path))
|
|
525
|
+
fail("EBUSY");
|
|
526
|
+
for (const location of [directory, file])
|
|
527
|
+
this.mutable(location);
|
|
528
|
+
return { mount: directory.mount, staging: {
|
|
529
|
+
parent: { path: parent.local, stat: staging.parent.stat },
|
|
530
|
+
directory: { path: directory.local, stat: staging.directory.stat },
|
|
531
|
+
file: { path: file.local, stat: staging.file.stat },
|
|
532
|
+
} };
|
|
533
|
+
}
|
|
534
|
+
writeFileConditional(path, data, options) {
|
|
535
|
+
return this.operation("writeFileConditional", path, options, async () => {
|
|
536
|
+
const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
|
|
537
|
+
if (this.protected(location.path))
|
|
538
|
+
fail("EBUSY");
|
|
539
|
+
this.mutable(location);
|
|
540
|
+
const backend = location.mount.backend;
|
|
541
|
+
await requireOwnedMutation(backend, location.local, "atomicFileMutation", options, options.expected === null);
|
|
542
|
+
if (!backend.writeFileConditional)
|
|
543
|
+
fail("ENOTSUP");
|
|
544
|
+
return snapshotStat(await backend.writeFileConditional(location.local, data, options));
|
|
545
|
+
}, undefined, true);
|
|
546
|
+
}
|
|
547
|
+
removeFileConditional(path, options) {
|
|
548
|
+
return this.operation("removeFileConditional", path, options, async () => {
|
|
549
|
+
const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
|
|
550
|
+
if (this.protected(location.path))
|
|
551
|
+
fail("EBUSY");
|
|
552
|
+
this.mutable(location);
|
|
553
|
+
const backend = location.mount.backend;
|
|
554
|
+
await requireOwnedMutation(backend, location.local, "atomicFileMutation", options);
|
|
555
|
+
if (!backend.removeFileConditional)
|
|
556
|
+
fail("ENOTSUP");
|
|
557
|
+
await backend.removeFileConditional(location.local, options);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
createStagedFile(path, name, content, options) {
|
|
561
|
+
return this.operation("createStagedFile", path, options, async () => {
|
|
562
|
+
const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true, missingDirectory: true });
|
|
563
|
+
if (this.protected(location.path))
|
|
564
|
+
fail("EBUSY");
|
|
565
|
+
this.mutable(location);
|
|
566
|
+
const backend = location.mount.backend;
|
|
567
|
+
await requireOwnedMutation(backend, location.local, "atomicFileStaging", options, true);
|
|
568
|
+
if (!backend.createStagedFile)
|
|
569
|
+
fail("ENOTSUP");
|
|
570
|
+
const receipt = await backend.createStagedFile(location.local, name, content, options);
|
|
571
|
+
const map = (entry) => Object.freeze({
|
|
572
|
+
path: location.mount.path === "/" ? entry.path : `${location.mount.path}${entry.path === "/" ? "" : entry.path}`,
|
|
573
|
+
stat: Object.freeze(snapshotStat(entry.stat)),
|
|
574
|
+
});
|
|
575
|
+
return Object.freeze({ parent: map(receipt.parent), directory: map(receipt.directory), file: map(receipt.file) });
|
|
576
|
+
}, undefined, true);
|
|
577
|
+
}
|
|
578
|
+
publishStagedFile(staging, destination, options) {
|
|
579
|
+
return this.operation("publishStagedFile", staging.file.path, options, async () => {
|
|
580
|
+
const local = await this.localStaging(staging, options);
|
|
581
|
+
const target = await this.resolve(destination, options, { followFinal: false, entry: true, allowMissing: true });
|
|
582
|
+
if (this.protected(target.path))
|
|
583
|
+
fail("EBUSY");
|
|
584
|
+
if (local.mount !== target.mount)
|
|
585
|
+
fail("EXDEV");
|
|
586
|
+
this.mutable(target);
|
|
587
|
+
await requireOwnedMutation(local.mount.backend, local.staging.directory.path, "atomicFileStaging", options);
|
|
588
|
+
if (!local.mount.backend.publishStagedFile)
|
|
589
|
+
fail("ENOTSUP");
|
|
590
|
+
await local.mount.backend.publishStagedFile(local.staging, target.local, options);
|
|
591
|
+
}, destination);
|
|
592
|
+
}
|
|
593
|
+
removeStagedFile(staging, options = {}) {
|
|
594
|
+
return this.operation("removeStagedFile", staging.directory.path, options, async () => {
|
|
595
|
+
const local = await this.localStaging(staging, options);
|
|
596
|
+
await requireOwnedMutation(local.mount.backend, local.staging.directory.path, "atomicFileStaging", options);
|
|
597
|
+
if (!local.mount.backend.removeStagedFile)
|
|
598
|
+
fail("ENOTSUP");
|
|
599
|
+
await local.mount.backend.removeStagedFile(local.staging, options);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
prepareDirectory(path, options) {
|
|
603
|
+
return this.operation("prepareDirectory", path, options, async () => {
|
|
604
|
+
const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true, missingDirectory: true });
|
|
605
|
+
if (this.protected(location.path))
|
|
606
|
+
fail("EBUSY");
|
|
607
|
+
this.mutable(location);
|
|
608
|
+
await requireOwnedMutation(location.mount.backend, location.local, "atomicDirectoryMetadata", options, options.expected === null);
|
|
609
|
+
if (!location.mount.backend.prepareDirectory)
|
|
610
|
+
fail("ENOTSUP");
|
|
611
|
+
return snapshotStat(await location.mount.backend.prepareDirectory(location.local, options));
|
|
612
|
+
}, undefined, true);
|
|
613
|
+
}
|
|
515
614
|
rename(source, destination, options = {}) {
|
|
516
615
|
return this.operation("rename", source, options, async () => {
|
|
517
616
|
const origin = await this.resolve(source, options, { followFinal: false, entry: true });
|
|
@@ -8,12 +8,13 @@ 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, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
11
|
+
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
12
12
|
return {
|
|
13
13
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
14
14
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
15
15
|
...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
|
|
16
16
|
...(birthtimeMs === undefined ? {} : { birthtimeMs }),
|
|
17
|
+
...(revision === undefined ? {} : { revision }),
|
|
17
18
|
...(identityScope === undefined ? {} : { identityScope }),
|
|
18
19
|
...(ino === undefined ? {} : { ino }),
|
|
19
20
|
...(dev === undefined ? {} : { dev }),
|
|
@@ -113,6 +114,7 @@ export class OverlayFileSystem {
|
|
|
113
114
|
].filter(([, value]) => value !== undefined));
|
|
114
115
|
this.capabilities = Object.freeze({
|
|
115
116
|
...semantics,
|
|
117
|
+
atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
116
118
|
implicitDirectories: false,
|
|
117
119
|
readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
|
|
118
120
|
: upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
|
|
@@ -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 === "
|
|
319
|
+
if (property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
|
|
320
320
|
return undefined;
|
|
321
321
|
if (property === "capabilities")
|
|
322
322
|
return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs)));
|
|
@@ -9,9 +9,10 @@ function readOnly(syscall, path, dest) {
|
|
|
9
9
|
throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
10
10
|
}
|
|
11
11
|
function snapshotStat(stat) {
|
|
12
|
-
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
12
|
+
const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
13
13
|
return {
|
|
14
14
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
15
|
+
...(revision === undefined ? {} : { revision }),
|
|
15
16
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
16
17
|
...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
|
|
17
18
|
...(birthtimeMs === undefined ? {} : { birthtimeMs }),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
|
|
2
2
|
export declare function scopeFileSystem(filesystem: FileSystem, charge: () => void, signal: AbortSignal, cleanupCharge?: () => void): FileSystem;
|
|
3
3
|
export interface RetainedFileSystemCleanupView {
|
|
4
|
+
readonly removeFileConditional?: NonNullable<FileSystem["removeFileConditional"]>;
|
|
5
|
+
readonly removeStagedFile?: NonNullable<FileSystem["removeStagedFile"]>;
|
|
4
6
|
readonly lstat: FileSystem["lstat"];
|
|
5
7
|
readonly realpath: FileSystem["realpath"];
|
|
6
8
|
readonly rm: (path: string, options?: FsOptions) => Promise<void>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { FsError } from "../contracts/errors.js";
|
|
2
2
|
import { finishCleanup } from "../contracts/cleanup.js";
|
|
3
3
|
import { registerEntryView } from "./mount/comparison.js";
|
|
4
|
-
import { openRetainedResizeFile, retainedResizeCapabilities } from "./capabilities.js";
|
|
4
|
+
import { openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "./capabilities.js";
|
|
5
5
|
const originals = new WeakMap();
|
|
6
6
|
const operations = new Set([
|
|
7
|
+
"writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
|
|
7
8
|
"access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
|
|
8
9
|
"copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir",
|
|
9
10
|
"readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "stat", "symlink", "truncate", "utimes",
|
|
@@ -151,7 +152,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
151
152
|
},
|
|
152
153
|
get(_target, property) {
|
|
153
154
|
if (property === "capabilities")
|
|
154
|
-
return retainedResizeCapabilities(original);
|
|
155
|
+
return ownedMutationCapabilities(original, retainedResizeCapabilities(original));
|
|
155
156
|
const method = Reflect.get(original, property, original);
|
|
156
157
|
if (typeof method !== "function")
|
|
157
158
|
return method;
|
|
@@ -163,6 +164,15 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
163
164
|
const options = args.at(-1);
|
|
164
165
|
admit(options && typeof options === "object" && "signal" in options ? options : undefined);
|
|
165
166
|
}
|
|
167
|
+
if (["writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory"].includes(String(property)))
|
|
168
|
+
return (async () => {
|
|
169
|
+
const path = typeof args[0] === "string" ? args[0] : args[0].directory.path;
|
|
170
|
+
const options = args[property === "createStagedFile" ? 3 : property === "publishStagedFile" || property === "writeFileConditional" ? 2 : 1];
|
|
171
|
+
const create = property === "createStagedFile" || (property === "writeFileConditional" || property === "prepareDirectory") && options !== undefined && "expected" in options && options.expected === null;
|
|
172
|
+
await requireOwnedMutation(original, path, property === "prepareDirectory" ? "atomicDirectoryMetadata" : property === "writeFileConditional" || property === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", options ?? {}, create);
|
|
173
|
+
assertOpen(options);
|
|
174
|
+
return Reflect.apply(method, original, args);
|
|
175
|
+
})();
|
|
166
176
|
if (property === "compareEntry") {
|
|
167
177
|
const peer = args[1];
|
|
168
178
|
args[1] = originals.get(peer)?.filesystem ?? peer;
|
|
@@ -187,7 +197,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
187
197
|
return wrapResizeHandle(await openRetainedResizeFile(original, path, resizeOptions(options)));
|
|
188
198
|
}
|
|
189
199
|
: property === "capabilitiesFor"
|
|
190
|
-
? async (...args) => retainedResizeCapabilities(original, await dispatch(...args))
|
|
200
|
+
? async (...args) => ownedMutationCapabilities(original, retainedResizeCapabilities(original, await dispatch(...args)))
|
|
191
201
|
: property === "openReadFile"
|
|
192
202
|
? async (...args) => wrapHandle(await dispatch(...args))
|
|
193
203
|
: property === "readStream"
|
|
@@ -219,7 +229,8 @@ export function retainFileSystemCleanup(filesystem, cleanupCallback, options) {
|
|
|
219
229
|
let operations = 0;
|
|
220
230
|
let active = false;
|
|
221
231
|
let closing;
|
|
222
|
-
const invoke = (method,
|
|
232
|
+
const invoke = (method, input, settings) => {
|
|
233
|
+
const path = typeof input === "string" ? input : input.directory.path;
|
|
223
234
|
if (!active) {
|
|
224
235
|
const rejected = Promise.reject(new FsError("EBADF", { syscall: method, path, message: "cleanup callback is not active" }));
|
|
225
236
|
void rejected.catch(() => { });
|
|
@@ -237,7 +248,9 @@ export function retainFileSystemCleanup(filesystem, cleanupCallback, options) {
|
|
|
237
248
|
if (typeof operation !== "function")
|
|
238
249
|
throw new FsError("ENOTSUP", { syscall: method, path });
|
|
239
250
|
const parameters = method === "rm" ? { ...settings, recursive: false, force: false } : settings;
|
|
240
|
-
result =
|
|
251
|
+
result = method === "removeStagedFile" || method === "removeFileConditional"
|
|
252
|
+
? requireOwnedMutation(backing, path, method === "removeFileConditional" ? "atomicFileMutation" : "atomicFileStaging", settings ?? {}).then(() => Reflect.apply(operation, backing, [input, parameters]))
|
|
253
|
+
: Promise.resolve(Reflect.apply(operation, backing, [input, parameters]));
|
|
241
254
|
}
|
|
242
255
|
catch (error) {
|
|
243
256
|
result = Promise.reject(error);
|
|
@@ -249,6 +262,8 @@ export function retainFileSystemCleanup(filesystem, cleanupCallback, options) {
|
|
|
249
262
|
return result;
|
|
250
263
|
};
|
|
251
264
|
const view = Object.freeze(Object.assign(Object.create(null), {
|
|
265
|
+
...(typeof backing.removeFileConditional === "function" ? { removeFileConditional: invoke.bind(undefined, "removeFileConditional") } : {}),
|
|
266
|
+
...(typeof backing.removeStagedFile === "function" ? { removeStagedFile: invoke.bind(undefined, "removeStagedFile") } : {}),
|
|
252
267
|
lstat: invoke.bind(undefined, "lstat"),
|
|
253
268
|
realpath: invoke.bind(undefined, "realpath"),
|
|
254
269
|
rm: invoke.bind(undefined, "rm"),
|