@poe-platform/safe-fs 0.1.726 → 0.1.728
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 +8 -1
- package/dist/safe-fs/contracts/errors.js +8 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +68 -2
- package/dist/safe-fs/contracts/io.js +183 -27
- package/dist/safe-fs/contracts/virtual-path.d.ts +3 -1
- package/dist/safe-fs/contracts/virtual-path.js +20 -1
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/capabilities.d.ts +1 -1
- package/dist/safe-fs/fs/capabilities.js +59 -21
- package/dist/safe-fs/fs/conditional-chmod.d.ts +7 -0
- package/dist/safe-fs/fs/conditional-chmod.js +19 -0
- package/dist/safe-fs/fs/descriptor.js +51 -25
- package/dist/safe-fs/fs/devices/index.d.ts +7 -2
- package/dist/safe-fs/fs/devices/index.js +210 -29
- package/dist/safe-fs/fs/devices/path.d.ts +3 -1
- package/dist/safe-fs/fs/devices/path.js +70 -12
- package/dist/safe-fs/fs/memory/index.d.ts +34 -48
- package/dist/safe-fs/fs/memory/index.js +1150 -235
- package/dist/safe-fs/fs/memory/ledger.d.ts +2 -0
- package/dist/safe-fs/fs/memory/ledger.js +15 -3
- package/dist/safe-fs/fs/memory/missing-target.js +21 -1
- package/dist/safe-fs/fs/mount/index.d.ts +11 -3
- package/dist/safe-fs/fs/mount/index.js +326 -15
- package/dist/safe-fs/fs/object-publication/index.js +79 -3
- package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
- package/dist/safe-fs/fs/overlay/index.js +22 -2
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +2 -1
- package/dist/safe-fs/fs/overlay/memory-publication.js +39 -2
- package/dist/safe-fs/fs/quota/index.js +1 -1
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +13 -0
- package/dist/safe-fs/fs/real/index.d.ts +6 -2
- package/dist/safe-fs/fs/real/index.js +78 -8
- package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
- package/dist/safe-fs/fs/s3/filesystem.js +48 -16
- package/dist/safe-fs/fs/s3/http/transport.js +20 -3
- package/dist/safe-fs/fs/s3/mock.js +19 -2
- package/dist/safe-fs/fs/s3/namespace.js +8 -2
- package/dist/safe-fs/fs/s3/registry.js +2 -0
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +261 -74
- package/dist/safe-fs/fs/staging-ancestry.d.ts +8 -0
- package/dist/safe-fs/fs/staging-ancestry.js +115 -0
- package/dist/safe-fs/fs/staging-cleanup.d.ts +5 -0
- package/dist/safe-fs/fs/staging-cleanup.js +63 -0
- package/dist/safe-fs/platform/browser.d.ts +5 -0
- package/dist/safe-fs/platform/browser.js +17 -0
- package/dist/safe-fs/platform/node.d.ts +5 -0
- package/dist/safe-fs/platform/node.js +28 -0
- package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
- package/dist/safe-fs/platform/transport-budget.js +4 -0
- package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
- package/dist/safe-fs/testing/object-io-metrics.js +29 -0
- package/dist/safe-fs/testing/object-publication.d.ts +3 -1
- package/dist/safe-fs/testing/object-publication.js +2 -2
- package/package.json +1 -1
|
@@ -7,7 +7,8 @@ export class MemoryLedger {
|
|
|
7
7
|
this.limits = limits;
|
|
8
8
|
}
|
|
9
9
|
get availableBytes() {
|
|
10
|
-
|
|
10
|
+
const max = this.limits.maxRetainedBytes;
|
|
11
|
+
return max === Infinity ? Infinity : max - this.retainedBytes;
|
|
11
12
|
}
|
|
12
13
|
fileSize(length, syscall, path) {
|
|
13
14
|
if (!Number.isSafeInteger(length) || length < 0 || length > this.limits.maxFileBytes) {
|
|
@@ -15,8 +16,12 @@ export class MemoryLedger {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
check(bytes, units, syscall, path) {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const maxRetained = this.limits.maxRetainedBytes;
|
|
20
|
+
const maxUnits = this.limits.maxMetadataUnits;
|
|
21
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 ||
|
|
22
|
+
(maxRetained !== Infinity && bytes > maxRetained - this.retainedBytes) ||
|
|
23
|
+
!Number.isSafeInteger(units) || units < 0 ||
|
|
24
|
+
(maxUnits !== Infinity && units > maxUnits - this.metadataUnits)) {
|
|
20
25
|
throw new FsError("ENOSPC", { syscall, path });
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -50,4 +55,11 @@ export class MemoryAllocation {
|
|
|
50
55
|
if (--this.references === 0)
|
|
51
56
|
this.ledger.release(this.data.byteLength, 0);
|
|
52
57
|
}
|
|
58
|
+
isReleased64() {
|
|
59
|
+
return this.references === 0 && this.data.byteLength === 64;
|
|
60
|
+
}
|
|
61
|
+
reuse() {
|
|
62
|
+
this.references = 1;
|
|
63
|
+
this.data.fill(0);
|
|
64
|
+
}
|
|
53
65
|
}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { FsError } from "../../contracts/errors.js";
|
|
2
|
+
function utf8ByteLength(value) {
|
|
3
|
+
let bytes = 0;
|
|
4
|
+
for (let i = 0; i < value.length; i++) {
|
|
5
|
+
const code = value.charCodeAt(i);
|
|
6
|
+
if (code < 0x80)
|
|
7
|
+
bytes += 1;
|
|
8
|
+
else if (code < 0x800)
|
|
9
|
+
bytes += 2;
|
|
10
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) {
|
|
11
|
+
bytes += 4;
|
|
12
|
+
i++;
|
|
13
|
+
}
|
|
14
|
+
else
|
|
15
|
+
bytes += 3;
|
|
16
|
+
}
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
function exceedsComponentByteLimit(value) {
|
|
20
|
+
return value.length > 85 && (value.length > 255 || utf8ByteLength(value) > 255);
|
|
21
|
+
}
|
|
2
22
|
function components(path, signal) {
|
|
3
23
|
const result = [];
|
|
4
24
|
let start = 0;
|
|
@@ -56,7 +76,7 @@ export function resolveMissingTarget(root, path, signal) {
|
|
|
56
76
|
first = false;
|
|
57
77
|
continue;
|
|
58
78
|
}
|
|
59
|
-
if (
|
|
79
|
+
if (exceedsComponentByteLimit(name))
|
|
60
80
|
return fail("ENAMETOOLONG");
|
|
61
81
|
const node = current.entries.get(name);
|
|
62
82
|
if (!node) {
|
|
@@ -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, ConditionalFilePublicationOptions, 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, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, 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 {
|
|
@@ -14,6 +14,11 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
14
14
|
constructor(options: MountFileSystemOptions);
|
|
15
15
|
private select;
|
|
16
16
|
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
17
|
+
private supportsStagingAncestry;
|
|
18
|
+
private supportsDirectoryValidation;
|
|
19
|
+
prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
|
|
20
|
+
prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
|
|
21
|
+
private prepareAncestry;
|
|
17
22
|
private protected;
|
|
18
23
|
openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
19
24
|
resizeFile(path: string, operation: FileResizeOperation, options?: FileResizeOptions): Promise<void>;
|
|
@@ -41,7 +46,10 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
41
46
|
private localStaging;
|
|
42
47
|
publishFileConditional(path: string, source: ByteSource, options: ConditionalFilePublicationOptions): Promise<FileStat>;
|
|
43
48
|
writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
44
|
-
removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions
|
|
49
|
+
removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions & {
|
|
50
|
+
readonly returnRemainingStat?: false | undefined;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
removeEntryConditional(path: string, options: ConditionalRemoveEntryReceiptOptions): Promise<void | FileStat>;
|
|
45
53
|
removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
46
54
|
removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
|
|
47
55
|
createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
@@ -56,7 +64,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
56
64
|
readlink(path: string, options?: FsOptions): Promise<string>;
|
|
57
65
|
symlink(target: string, path: string, options?: FsOptions): Promise<void>;
|
|
58
66
|
link(existingPath: string, newPath: string, options?: FsOptions): Promise<void>;
|
|
59
|
-
chmod(path: string, mode: number, options?:
|
|
67
|
+
chmod(path: string, mode: number, options?: ChmodOptions): Promise<void>;
|
|
60
68
|
utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
|
|
61
69
|
truncate(path: string, length?: number, options?: FsOptions): Promise<void>;
|
|
62
70
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
@@ -6,7 +6,10 @@ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admissi
|
|
|
6
6
|
import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
7
7
|
import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
|
|
8
8
|
import { pathNamespace } from "../path-namespace.js";
|
|
9
|
+
import { createStagingCleanup, snapshotStagingCreation } from "../staging-cleanup.js";
|
|
10
|
+
import { directoryAncestryPaths, inspectStagingBindings, runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "../staging-ancestry.js";
|
|
9
11
|
import { compareIdentity } from "./identity.js";
|
|
12
|
+
import { snapshotConditionalChmod } from "../conditional-chmod.js";
|
|
10
13
|
import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
|
|
11
14
|
const syntheticStat = Object.freeze({
|
|
12
15
|
type: "directory", size: 0, mode: 0o40555,
|
|
@@ -102,8 +105,10 @@ export class MountFileSystem {
|
|
|
102
105
|
const common = (capability) => {
|
|
103
106
|
const optional = {
|
|
104
107
|
open: ["open"],
|
|
108
|
+
retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
109
|
+
synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
105
110
|
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
106
|
-
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
111
|
+
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
107
112
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
|
|
108
113
|
descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
109
114
|
};
|
|
@@ -111,7 +116,8 @@ export class MountFileSystem {
|
|
|
111
116
|
if (backend.capabilities.readOnly === true
|
|
112
117
|
&& !["open", "versionedDescriptors", "read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories"].includes(capability))
|
|
113
118
|
return false;
|
|
114
|
-
const declared =
|
|
119
|
+
const declared = capability === "retainedStagingCleanup" || capability === "atomicEntryRemovalReceipt"
|
|
120
|
+
? ownedMutationCapabilities(backend)[capability] : backend.capabilities[capability];
|
|
115
121
|
if (capability === "descriptorWriteStream" && backend.capabilities.streamingWrite === false)
|
|
116
122
|
return false;
|
|
117
123
|
return declared === true && optional[capability]?.some(method => typeof backend[method] !== "function") ? false : declared;
|
|
@@ -121,6 +127,7 @@ export class MountFileSystem {
|
|
|
121
127
|
return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
|
|
122
128
|
};
|
|
123
129
|
const semantics = Object.fromEntries([
|
|
130
|
+
"guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt",
|
|
124
131
|
"trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
|
|
125
132
|
"write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
|
|
126
133
|
"remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
|
|
@@ -131,8 +138,13 @@ export class MountFileSystem {
|
|
|
131
138
|
readOnly: all("readOnly"),
|
|
132
139
|
...(append === undefined ? {} : { append }),
|
|
133
140
|
...semantics,
|
|
134
|
-
|
|
141
|
+
// Real ancestry (including synthetic mount parents) is path-dependent.
|
|
142
|
+
synchronousStagingResolution: mounts.some(({ backend }) => typeof backend.prepareStagingResolution === "function") ? undefined : false,
|
|
143
|
+
atomicStagingAncestry: mounts.some(({ backend }) => typeof backend.prepareDirectoryAncestry === "function"
|
|
144
|
+
&& typeof backend.publishStagedFile === "function" && (typeof backend.capabilitiesFor === "function"
|
|
145
|
+
|| backend.capabilities.guardedStagingPublication !== false && backend.capabilities.synchronousDirectoryValidation !== false)) ? undefined : false,
|
|
135
146
|
atomicRename: mounts.length === 1 && all("atomicRename"),
|
|
147
|
+
conditionalChmod: all("conditionalChmod", ["chmod", "prepareDirectoryAncestry"]),
|
|
136
148
|
...(streamingRead === undefined ? {} : { streamingRead }),
|
|
137
149
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
138
150
|
...(retainedRead === undefined ? {} : { retainedRead }),
|
|
@@ -143,9 +155,11 @@ export class MountFileSystem {
|
|
|
143
155
|
}
|
|
144
156
|
async capabilitiesFor(path, options = {}) {
|
|
145
157
|
return this.operation("capabilitiesFor", path, options, async () => {
|
|
158
|
+
if (options.stagingResolution === true && normalizePath(globalPath(path)) !== path)
|
|
159
|
+
fail("EINVAL");
|
|
146
160
|
const location = await this.resolve(path, options, {
|
|
147
161
|
allowMissing: options.create ?? true,
|
|
148
|
-
followFinal: options.create !== undefined || options.creation !== "exclusive",
|
|
162
|
+
followFinal: options.stagingResolution !== true && (options.create !== undefined || options.creation !== "exclusive"),
|
|
149
163
|
...(options.create === undefined ? {} : { resizeCreate: options.create }),
|
|
150
164
|
});
|
|
151
165
|
const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
@@ -155,7 +169,26 @@ export class MountFileSystem {
|
|
|
155
169
|
? { ...observed, descriptorWriteStream: false } : observed;
|
|
156
170
|
const resize = declared.atomicResize === true && (typeof location.mount.backend.resizeFile !== "function" || declared.readOnly === true)
|
|
157
171
|
? { ...declared, atomicResize: false } : declared;
|
|
158
|
-
const
|
|
172
|
+
const ancestry = options.stagingAncestry === true && location.path === normalizePath(globalPath(path))
|
|
173
|
+
&& await this.supportsStagingAncestry(location, resize, options);
|
|
174
|
+
const validation = options.stagingAncestry === true ? location.stat?.type === "directory"
|
|
175
|
+
&& location.path === normalizePath(globalPath(path)) && await this.supportsDirectoryValidation(location.path, options) : undefined;
|
|
176
|
+
let resolution;
|
|
177
|
+
if (options.stagingResolution === true) {
|
|
178
|
+
resolution = false;
|
|
179
|
+
if (within(location.mount.path, path) && typeof location.mount.backend.prepareStagingResolution === "function") {
|
|
180
|
+
const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
|
|
181
|
+
const declared = input === location.local ? resize : ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(input, options) ?? location.mount.backend.capabilities);
|
|
182
|
+
options.signal?.throwIfAborted();
|
|
183
|
+
resolution = declared.synchronousStagingResolution === true && await this.supportsStagingAncestry(location, resize, options);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const conditionalChmod = options.conditionalChmod === true
|
|
187
|
+
? resize.conditionalChmod === true && typeof location.mount.backend.chmod === "function"
|
|
188
|
+
&& await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
|
|
189
|
+
: this.capabilities.conditionalChmod;
|
|
190
|
+
const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, ...ordinary } = resize;
|
|
191
|
+
const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
|
|
159
192
|
const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
|
|
160
193
|
: retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
|
|
161
194
|
if (location.synthetic)
|
|
@@ -171,6 +204,147 @@ export class MountFileSystem {
|
|
|
171
204
|
});
|
|
172
205
|
});
|
|
173
206
|
}
|
|
207
|
+
async supportsStagingAncestry(target, capabilities, options) {
|
|
208
|
+
options.signal?.throwIfAborted();
|
|
209
|
+
if (target.synthetic || target.stat !== undefined && (target.stat.type !== "file" || compareIdentity(target.stat, target.stat) !== "same")
|
|
210
|
+
|| capabilities.atomicFileStaging !== true || capabilities.atomicStagingAncestry !== true
|
|
211
|
+
|| capabilities.guardedStagingPublication !== true || capabilities.readOnly === true)
|
|
212
|
+
return false;
|
|
213
|
+
const parent = target.path.slice(0, target.path.lastIndexOf("/")) || "/";
|
|
214
|
+
return this.supportsDirectoryValidation(parent, options);
|
|
215
|
+
}
|
|
216
|
+
async supportsDirectoryValidation(directory, options, conditionalChmod = false) {
|
|
217
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
218
|
+
for (const path of directoryAncestryPaths(directory)) {
|
|
219
|
+
const entry = await this.lookup(path, controls);
|
|
220
|
+
if (entry.synthetic || entry.stat?.type !== "directory" || compareIdentity(entry.stat, entry.stat) !== "same")
|
|
221
|
+
return false;
|
|
222
|
+
const backend = entry.mount.backend;
|
|
223
|
+
const declared = await backend.capabilitiesFor?.(entry.local, { ...controls, stagingAncestry: true }) ?? backend.capabilities;
|
|
224
|
+
options.signal?.throwIfAborted();
|
|
225
|
+
if (declared.synchronousDirectoryValidation !== true && !(conditionalChmod && declared.conditionalChmod === true)
|
|
226
|
+
|| typeof backend.prepareDirectoryAncestry !== "function")
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
prepareDirectoryAncestry(ancestors, options = {}) {
|
|
232
|
+
return this.operation("prepareDirectoryAncestry", ancestors.at(-1)?.path ?? "/", options, () => this.prepareAncestry(ancestors, options));
|
|
233
|
+
}
|
|
234
|
+
prepareStagingResolution(path, options = {}) {
|
|
235
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
236
|
+
return this.operation("prepareStagingResolution", path, controls, async () => {
|
|
237
|
+
if (normalizePath(globalPath(path)) !== path)
|
|
238
|
+
fail("EINVAL");
|
|
239
|
+
const observed = [];
|
|
240
|
+
const location = await this.resolve(path, controls, { followFinal: false, allowMissing: true, resolutionSteps: observed });
|
|
241
|
+
if (location.synthetic || !within(location.mount.path, path))
|
|
242
|
+
fail("ENOTSUP");
|
|
243
|
+
const backend = location.mount.backend;
|
|
244
|
+
const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
|
|
245
|
+
const declared = ownedMutationCapabilities(backend, await backend.capabilitiesFor?.(input, { ...controls, stagingResolution: true }) ?? backend.capabilities);
|
|
246
|
+
controls.signal?.throwIfAborted();
|
|
247
|
+
if (declared.synchronousStagingResolution !== true || !backend.prepareStagingResolution)
|
|
248
|
+
fail("ENOTSUP");
|
|
249
|
+
const prefix = directoryAncestryPaths(location.mount.path).map(ancestor => {
|
|
250
|
+
const entry = observed.find(step => step.path === ancestor);
|
|
251
|
+
if (!entry)
|
|
252
|
+
fail("ENOTSUP");
|
|
253
|
+
return entry;
|
|
254
|
+
});
|
|
255
|
+
const outerGuard = await this.prepareAncestry(prefix, controls);
|
|
256
|
+
const bound = snapshotStagingResolution(await backend.prepareStagingResolution(input, controls));
|
|
257
|
+
controls.signal?.throwIfAborted();
|
|
258
|
+
const mapPath = (local) => location.mount.path === "/" ? local : `${location.mount.path}${local === "/" ? "" : local}`;
|
|
259
|
+
const map = (entry) => ({ ...entry, path: mapPath(entry.path) });
|
|
260
|
+
const traversed = bound.traversed.map(map);
|
|
261
|
+
const localObserved = observed.filter(entry => within(location.mount.path, entry.path));
|
|
262
|
+
if (mapPath(bound.path) !== location.path || localObserved.length !== traversed.length)
|
|
263
|
+
fail("EAGAIN");
|
|
264
|
+
for (const [index, before] of localObserved.entries()) {
|
|
265
|
+
const after = traversed[index];
|
|
266
|
+
if (this.select(after.path) !== location.mount)
|
|
267
|
+
fail("EACCES");
|
|
268
|
+
if (before.path !== after.path || before.linkTarget !== after.linkTarget || before.stat.type !== after.stat.type
|
|
269
|
+
|| compareIdentity(before.stat, after.stat) !== "same"
|
|
270
|
+
|| before.stat.type !== "directory" && ["revision", "size", "mode", "nlink", "mtimeMs", "ctimeMs"].some(field => before.stat[field] !== after.stat[field]))
|
|
271
|
+
fail("EAGAIN");
|
|
272
|
+
}
|
|
273
|
+
const validate = () => {
|
|
274
|
+
controls.signal?.throwIfAborted();
|
|
275
|
+
runStagingGuard(outerGuard);
|
|
276
|
+
runStagingGuard(bound.validate);
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
const result = snapshotStagingResolution({ ...bound, path: mapPath(bound.path),
|
|
280
|
+
ancestors: [...prefix.slice(0, -1), ...bound.ancestors.map(map)], traversed: [...prefix.slice(0, -1), ...traversed], validate });
|
|
281
|
+
runStagingGuard(validate);
|
|
282
|
+
return result;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async prepareAncestry(ancestors, options, conditionalChmod = false) {
|
|
286
|
+
options.signal?.throwIfAborted();
|
|
287
|
+
try {
|
|
288
|
+
const entries = snapshotDirectoryAncestry(ancestors);
|
|
289
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
290
|
+
const groups = [];
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
const mount = this.select(entry.path);
|
|
293
|
+
let group = groups.at(-1);
|
|
294
|
+
if (group?.mount !== mount) {
|
|
295
|
+
group = { mount, entries: [] };
|
|
296
|
+
groups.push(group);
|
|
297
|
+
}
|
|
298
|
+
group.entries.push({ path: mount.path === "/" ? entry.path : entry.path.slice(mount.path.length) || "/", stat: entry.stat });
|
|
299
|
+
}
|
|
300
|
+
const guards = [];
|
|
301
|
+
for (const group of groups) {
|
|
302
|
+
controls.signal?.throwIfAborted();
|
|
303
|
+
const backend = group.mount.backend;
|
|
304
|
+
for (const entry of group.entries) {
|
|
305
|
+
let current;
|
|
306
|
+
try {
|
|
307
|
+
current = await backend.lstat(entry.path, controls);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
controls.signal?.throwIfAborted();
|
|
311
|
+
if (["ENOENT", "ENOTDIR", "ELOOP"].includes(toFsError(error).code))
|
|
312
|
+
fail("EAGAIN");
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
controls.signal?.throwIfAborted();
|
|
316
|
+
if (current.type !== "directory")
|
|
317
|
+
fail("EAGAIN");
|
|
318
|
+
const identity = compareIdentity(current, entry.stat);
|
|
319
|
+
if (identity === "unknown")
|
|
320
|
+
fail("ENOTSUP");
|
|
321
|
+
if (identity !== "same")
|
|
322
|
+
fail("EAGAIN");
|
|
323
|
+
const declared = await backend.capabilitiesFor?.(entry.path, { ...controls, stagingAncestry: true }) ?? backend.capabilities;
|
|
324
|
+
controls.signal?.throwIfAborted();
|
|
325
|
+
if (declared.synchronousDirectoryValidation !== true && !(conditionalChmod && declared.conditionalChmod === true))
|
|
326
|
+
fail("ENOTSUP");
|
|
327
|
+
}
|
|
328
|
+
if (typeof backend.prepareDirectoryAncestry !== "function")
|
|
329
|
+
fail("ENOTSUP");
|
|
330
|
+
const guard = await backend.prepareDirectoryAncestry(group.entries, controls);
|
|
331
|
+
controls.signal?.throwIfAborted();
|
|
332
|
+
if (typeof guard !== "function")
|
|
333
|
+
fail("ENOTSUP");
|
|
334
|
+
guards.push(guard);
|
|
335
|
+
}
|
|
336
|
+
return () => {
|
|
337
|
+
controls.signal?.throwIfAborted();
|
|
338
|
+
for (const guard of guards)
|
|
339
|
+
runStagingGuard(guard);
|
|
340
|
+
return true;
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
options.signal?.throwIfAborted();
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
174
348
|
protected(path) {
|
|
175
349
|
return path === "/" || this.mounts.some((mount) => within(path, mount.path));
|
|
176
350
|
}
|
|
@@ -324,23 +498,48 @@ export class MountFileSystem {
|
|
|
324
498
|
async resolve(path, options, settings = {}) {
|
|
325
499
|
let pending = this.components(path);
|
|
326
500
|
const stack = [await this.lookup("/", options)];
|
|
501
|
+
let recordedUnits = 0;
|
|
502
|
+
const observe = (entry, linkTarget) => {
|
|
503
|
+
if (!settings.resolutionSteps || !entry.stat)
|
|
504
|
+
return;
|
|
505
|
+
recordedUnits += entry.path.length + (linkTarget?.length ?? 0);
|
|
506
|
+
if (settings.resolutionSteps.length >= 4096 || recordedUnits > 1_048_576)
|
|
507
|
+
fail("EFBIG");
|
|
508
|
+
settings.resolutionSteps.push({ path: entry.path, stat: snapshotStat(entry.stat), ...(linkTarget === undefined ? {} : { linkTarget }) });
|
|
509
|
+
};
|
|
510
|
+
observe(stack[0]);
|
|
327
511
|
let boundary;
|
|
328
512
|
let verification;
|
|
329
513
|
let creationVerification;
|
|
514
|
+
let followedFinalLink = false;
|
|
330
515
|
const verified = async (location) => {
|
|
331
|
-
|
|
516
|
+
let inspection = !location.stat && settings.resizeCreate === true ? creationVerification : verification;
|
|
517
|
+
// A missing entry has no realpath. Verify its existing parent while
|
|
518
|
+
// leaving dangling final symlinks on the ordinary following path.
|
|
519
|
+
if (inspection && !location.stat && settings.allowMissing && !followedFinalLink && inspection.finalName === undefined) {
|
|
520
|
+
const separator = inspection.path.lastIndexOf("/");
|
|
521
|
+
const finalName = inspection.path.slice(separator + 1);
|
|
522
|
+
if (finalName && finalName !== "." && finalName !== "..") {
|
|
523
|
+
inspection = { ...inspection, path: inspection.path.slice(0, separator) || "/", finalName };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
332
526
|
if (inspection) {
|
|
333
527
|
const canonical = await inspection.mount.backend.realpath(inspection.path, options);
|
|
528
|
+
options.signal?.throwIfAborted();
|
|
529
|
+
if (!location.stat && inspection.finalName === undefined)
|
|
530
|
+
fail("ENOTSUP");
|
|
334
531
|
validatePath(canonical);
|
|
335
532
|
if (!canonical.startsWith("/") || normalizePath(canonical) !== canonical)
|
|
336
533
|
fail("EIO");
|
|
337
534
|
if (inspection.finalName !== undefined) {
|
|
338
535
|
try {
|
|
339
536
|
await inspection.mount.backend.lstat(`${inspection.path}/${inspection.finalName}`, options);
|
|
340
|
-
|
|
537
|
+
options.signal?.throwIfAborted();
|
|
538
|
+
if (!location.stat && settings.allowMissing && settings.followFinal !== false)
|
|
341
539
|
fail("ENOTSUP");
|
|
342
540
|
}
|
|
343
541
|
catch (error) {
|
|
542
|
+
options.signal?.throwIfAborted();
|
|
344
543
|
if (location.stat || toFsError(error).code !== "ENOENT")
|
|
345
544
|
throw error;
|
|
346
545
|
}
|
|
@@ -406,6 +605,8 @@ export class MountFileSystem {
|
|
|
406
605
|
if (next.stat?.type === "symlink" && (settings.followFinal !== false || !final)) {
|
|
407
606
|
if (++links > 40)
|
|
408
607
|
fail("ELOOP");
|
|
608
|
+
if (final)
|
|
609
|
+
followedFinalLink = true;
|
|
409
610
|
boundary ??= next.mount;
|
|
410
611
|
if (boundary !== next.mount)
|
|
411
612
|
fail("EACCES");
|
|
@@ -422,6 +623,7 @@ export class MountFileSystem {
|
|
|
422
623
|
}
|
|
423
624
|
const readlink = this.optional(next, "readlink", "symlinks");
|
|
424
625
|
const target = await readlink.call(next.mount.backend, next.local, options);
|
|
626
|
+
observe(next, target);
|
|
425
627
|
const targetParts = this.components(target);
|
|
426
628
|
if (settings.resizeCreate !== undefined && targetParts.at(-1)?.trailing && pending[0]?.trailing)
|
|
427
629
|
targetParts.pop();
|
|
@@ -446,6 +648,7 @@ export class MountFileSystem {
|
|
|
446
648
|
pending = [...targetParts, ...pending];
|
|
447
649
|
}
|
|
448
650
|
else {
|
|
651
|
+
observe(next);
|
|
449
652
|
stack.push(next);
|
|
450
653
|
}
|
|
451
654
|
}
|
|
@@ -672,6 +875,8 @@ export class MountFileSystem {
|
|
|
672
875
|
}, undefined, true);
|
|
673
876
|
}
|
|
674
877
|
removeEntryConditional(path, options) {
|
|
878
|
+
options = { ...options };
|
|
879
|
+
const requested = options.returnRemainingStat === true;
|
|
675
880
|
return this.operation("removeEntryConditional", path, options, async () => {
|
|
676
881
|
const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
|
|
677
882
|
if (this.protected(location.path))
|
|
@@ -679,10 +884,13 @@ export class MountFileSystem {
|
|
|
679
884
|
this.mutable(location);
|
|
680
885
|
const backend = location.mount.backend;
|
|
681
886
|
await requireOwnedMutation(backend, location.local, "atomicEntryRemoval", options);
|
|
887
|
+
if (requested)
|
|
888
|
+
await requireOwnedMutation(backend, location.local, "atomicEntryRemovalReceipt", options);
|
|
682
889
|
if (!backend.removeEntryConditional)
|
|
683
890
|
fail("ENOTSUP");
|
|
684
|
-
await backend.removeEntryConditional(location.local, options);
|
|
685
|
-
|
|
891
|
+
const receipt = await backend.removeEntryConditional(location.local, options);
|
|
892
|
+
return requested && receipt !== undefined ? Object.freeze(snapshotStat(receipt)) : undefined;
|
|
893
|
+
}, undefined, requested);
|
|
686
894
|
}
|
|
687
895
|
removeFileConditional(path, options) {
|
|
688
896
|
return this.operation("removeFileConditional", path, options, async () => {
|
|
@@ -710,7 +918,8 @@ export class MountFileSystem {
|
|
|
710
918
|
await backend.removeTreeConditional(location.local, options);
|
|
711
919
|
});
|
|
712
920
|
}
|
|
713
|
-
createStagedFile(path, name, content, options) {
|
|
921
|
+
async createStagedFile(path, name, content, options) {
|
|
922
|
+
options = snapshotStagingCreation(options, path);
|
|
714
923
|
return this.operation("createStagedFile", path, options, async () => {
|
|
715
924
|
const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true, missingDirectory: true });
|
|
716
925
|
if (this.protected(location.path))
|
|
@@ -718,6 +927,8 @@ export class MountFileSystem {
|
|
|
718
927
|
this.mutable(location);
|
|
719
928
|
const backend = location.mount.backend;
|
|
720
929
|
await requireOwnedMutation(backend, location.local, "atomicFileStaging", options, true);
|
|
930
|
+
if (options.retainCleanup === true)
|
|
931
|
+
await requireOwnedMutation(backend, location.local, "retainedStagingCleanup", options, true);
|
|
721
932
|
if (!backend.createStagedFile)
|
|
722
933
|
fail("ENOTSUP");
|
|
723
934
|
const receipt = await backend.createStagedFile(location.local, name, content, options);
|
|
@@ -725,23 +936,103 @@ export class MountFileSystem {
|
|
|
725
936
|
path: location.mount.path === "/" ? entry.path : `${location.mount.path}${entry.path === "/" ? "" : entry.path}`,
|
|
726
937
|
stat: Object.freeze(snapshotStat(entry.stat)),
|
|
727
938
|
});
|
|
728
|
-
|
|
939
|
+
try {
|
|
940
|
+
const staging = { parent: map(receipt.parent), directory: map(receipt.directory), file: map(receipt.file) };
|
|
941
|
+
if (!receipt.cleanup)
|
|
942
|
+
return Object.freeze(staging);
|
|
943
|
+
const backendCleanup = receipt.cleanup;
|
|
944
|
+
const cleanup = createStagingCleanup(staging.directory.path, controls => this.operation("removeStagedFile", staging.directory.path, controls, () => backendCleanup.remove(controls), undefined, true), () => backendCleanup.close());
|
|
945
|
+
return Object.freeze({ ...staging, cleanup });
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
await finishCleanup(() => receipt.cleanup?.close(), true);
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
729
951
|
}, undefined, true);
|
|
730
952
|
}
|
|
731
953
|
publishStagedFile(staging, destination, options) {
|
|
732
954
|
return this.operation("publishStagedFile", staging.file.path, options, async () => {
|
|
955
|
+
const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
|
|
956
|
+
const callerGuard = options.commitGuard;
|
|
957
|
+
let guard;
|
|
958
|
+
if (ancestors) {
|
|
959
|
+
const canonical = normalizePath(globalPath(destination));
|
|
960
|
+
const parent = canonical.slice(0, canonical.lastIndexOf("/")) || "/";
|
|
961
|
+
if (canonical !== destination || ancestors.at(-1)?.path !== parent)
|
|
962
|
+
fail("EINVAL");
|
|
963
|
+
guard = await this.prepareAncestry(ancestors, options);
|
|
964
|
+
runStagingGuard(guard);
|
|
965
|
+
}
|
|
733
966
|
const local = await this.localStaging(staging, options);
|
|
734
967
|
const target = await this.resolve(destination, options, { followFinal: false, entry: true, allowMissing: true });
|
|
968
|
+
if (ancestors) {
|
|
969
|
+
if (target.path !== destination)
|
|
970
|
+
fail("EAGAIN");
|
|
971
|
+
if (options.destination === null) {
|
|
972
|
+
if (target.stat !== undefined)
|
|
973
|
+
fail("EAGAIN");
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
if (target.stat === undefined || target.stat.type !== options.destination.type)
|
|
977
|
+
fail("EAGAIN");
|
|
978
|
+
const identity = compareIdentity(target.stat, options.destination);
|
|
979
|
+
if (identity === "unknown")
|
|
980
|
+
fail("ENOTSUP");
|
|
981
|
+
if (identity !== "same")
|
|
982
|
+
fail("EAGAIN");
|
|
983
|
+
}
|
|
984
|
+
}
|
|
735
985
|
if (this.protected(target.path))
|
|
736
986
|
fail("EBUSY");
|
|
737
987
|
if (local.mount !== target.mount)
|
|
738
988
|
fail("EXDEV");
|
|
739
989
|
this.mutable(target);
|
|
740
990
|
await requireOwnedMutation(local.mount.backend, local.staging.directory.path, "atomicFileStaging", options);
|
|
991
|
+
await requireOwnedMutation(local.mount.backend, target.local, "atomicFileStaging", options, options.destination === null);
|
|
741
992
|
if (!local.mount.backend.publishStagedFile)
|
|
742
993
|
fail("ENOTSUP");
|
|
743
|
-
|
|
744
|
-
|
|
994
|
+
if (ancestors || callerGuard !== undefined)
|
|
995
|
+
await requireOwnedMutation(local.mount.backend, target.local, "guardedStagingPublication", options, options.destination === null);
|
|
996
|
+
if (!ancestors) {
|
|
997
|
+
await local.mount.backend.publishStagedFile(local.staging, target.local, options);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const parent = target.path.slice(0, target.path.lastIndexOf("/")) || "/";
|
|
1001
|
+
if (target.path !== normalizePath(globalPath(destination)) || ancestors.at(-1)?.path !== parent)
|
|
1002
|
+
fail("EINVAL");
|
|
1003
|
+
try {
|
|
1004
|
+
const declared = await local.mount.backend.capabilitiesFor?.(target.local, { ...options, stagingAncestry: true, ...(options.destination === null ? { create: true } : {}) }) ?? local.mount.backend.capabilities;
|
|
1005
|
+
if (!await this.supportsStagingAncestry(target, declared, options))
|
|
1006
|
+
fail("ENOTSUP");
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
options.signal?.throwIfAborted();
|
|
1010
|
+
if (["ENOTSUP", "ENOENT", "ENOTDIR", "ELOOP"].includes(toFsError(error).code)) {
|
|
1011
|
+
await inspectStagingBindings(async (path) => {
|
|
1012
|
+
const entry = await this.lookup(path, options);
|
|
1013
|
+
if (!entry.stat)
|
|
1014
|
+
fail("ENOENT");
|
|
1015
|
+
return entry.stat;
|
|
1016
|
+
}, destination, options);
|
|
1017
|
+
}
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
const first = ancestors.findIndex(entry => entry.path === target.mount.path);
|
|
1021
|
+
if (first < 0)
|
|
1022
|
+
fail("EINVAL");
|
|
1023
|
+
const localAncestors = ancestors.slice(first).map(entry => ({
|
|
1024
|
+
path: target.mount.path === "/" ? entry.path : entry.path.slice(target.mount.path.length) || "/", stat: entry.stat,
|
|
1025
|
+
}));
|
|
1026
|
+
await local.mount.backend.publishStagedFile(local.staging, target.local, {
|
|
1027
|
+
...options, ancestors: localAncestors,
|
|
1028
|
+
commitGuard: () => {
|
|
1029
|
+
if (callerGuard !== undefined)
|
|
1030
|
+
runStagingGuard(callerGuard);
|
|
1031
|
+
runStagingGuard(guard);
|
|
1032
|
+
return true;
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
}, destination, true);
|
|
745
1036
|
}
|
|
746
1037
|
removeStagedFile(staging, options = {}) {
|
|
747
1038
|
return this.operation("removeStagedFile", staging.directory.path, options, async () => {
|
|
@@ -895,9 +1186,29 @@ export class MountFileSystem {
|
|
|
895
1186
|
}
|
|
896
1187
|
chmod(path, mode, options = {}) {
|
|
897
1188
|
return this.operation("chmod", path, options, async () => {
|
|
898
|
-
const
|
|
1189
|
+
const conditional = snapshotConditionalChmod(path, options);
|
|
1190
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
1191
|
+
const location = await this.resolve(path, controls, conditional ? { followFinal: false } : {});
|
|
899
1192
|
this.mutable(location);
|
|
900
|
-
|
|
1193
|
+
const chmod = this.optional(location, "chmod", "permissions");
|
|
1194
|
+
if (!conditional)
|
|
1195
|
+
return chmod.call(location.mount.backend, location.local, mode, controls);
|
|
1196
|
+
if (location.path !== path)
|
|
1197
|
+
fail("EAGAIN");
|
|
1198
|
+
const backend = location.mount.backend;
|
|
1199
|
+
const capabilities = await backend.capabilitiesFor?.(location.local, { ...controls, conditionalChmod: true }) ?? backend.capabilities;
|
|
1200
|
+
if (capabilities.conditionalChmod !== true)
|
|
1201
|
+
fail("ENOTSUP");
|
|
1202
|
+
const guard = await this.prepareAncestry(conditional.ancestors, controls, true);
|
|
1203
|
+
const ancestors = conditional.ancestors.filter(entry => this.select(entry.path) === location.mount).map(entry => ({
|
|
1204
|
+
path: location.mount.path === "/" ? entry.path : entry.path.slice(location.mount.path.length) || "/", stat: entry.stat,
|
|
1205
|
+
}));
|
|
1206
|
+
await chmod.call(backend, location.local, mode, { ...conditional, ancestors, commitGuard: () => {
|
|
1207
|
+
if (conditional.commitGuard)
|
|
1208
|
+
runStagingGuard(conditional.commitGuard);
|
|
1209
|
+
runStagingGuard(guard);
|
|
1210
|
+
return true;
|
|
1211
|
+
} });
|
|
901
1212
|
});
|
|
902
1213
|
}
|
|
903
1214
|
utimes(path, atimeMs, mtimeMs, options = {}) {
|