@effect-vfs/core 0.7.1 → 0.9.0
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 +21 -235
- package/dist/Caller.d.ts +2 -0
- package/dist/Caller.d.ts.map +1 -1
- package/dist/Caller.js +3 -1
- package/dist/LiveVolume.d.ts +69 -1
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +89 -12
- package/dist/Search.d.ts +730 -0
- package/dist/Search.d.ts.map +1 -0
- package/dist/Search.js +576 -0
- package/dist/SnapshotDelta.d.ts +234 -0
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +107 -0
- package/dist/VfsError.d.ts +3 -1
- package/dist/VfsError.d.ts.map +1 -1
- package/dist/VfsError.js +3 -1
- package/dist/VirtualFileSystem.d.ts +78 -2
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +68 -4
- package/dist/Watch.d.ts +18 -0
- package/dist/Watch.d.ts.map +1 -1
- package/dist/Watch.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/internal/callerWatch.d.ts +32 -0
- package/dist/internal/callerWatch.d.ts.map +1 -0
- package/dist/internal/callerWatch.js +184 -0
- package/dist/internal/confinement.d.ts +16 -0
- package/dist/internal/confinement.d.ts.map +1 -0
- package/dist/internal/confinement.js +79 -0
- package/dist/internal/directoryEntries.d.ts +6 -0
- package/dist/internal/directoryEntries.d.ts.map +1 -0
- package/dist/internal/directoryEntries.js +79 -0
- package/dist/internal/fixture.js +4 -4
- package/dist/internal/glob.d.ts +66 -0
- package/dist/internal/glob.d.ts.map +1 -0
- package/dist/internal/glob.js +248 -0
- package/dist/internal/image.d.ts +7 -1
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +9 -7
- package/dist/internal/liveImage.d.ts +1 -1
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +2 -10
- package/dist/internal/path.js +2 -2
- package/dist/internal/resolution.d.ts +139 -0
- package/dist/internal/resolution.d.ts.map +1 -0
- package/dist/internal/resolution.js +332 -0
- package/dist/internal/search.d.ts +24 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +78 -0
- package/dist/internal/searchContent.d.ts +77 -0
- package/dist/internal/searchContent.d.ts.map +1 -0
- package/dist/internal/searchContent.js +223 -0
- package/dist/internal/searchModel.d.ts +83 -0
- package/dist/internal/searchModel.d.ts.map +1 -0
- package/dist/internal/searchModel.js +23 -0
- package/dist/internal/snapshotDelta.d.ts +470 -1
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +59 -25
- package/dist/internal/snapshotMerge.d.ts +54 -0
- package/dist/internal/snapshotMerge.d.ts.map +1 -0
- package/dist/internal/snapshotMerge.js +959 -0
- package/dist/internal/tokenRegistry.d.ts +83 -0
- package/dist/internal/tokenRegistry.d.ts.map +1 -0
- package/dist/internal/tokenRegistry.js +146 -0
- package/dist/internal/virtualFileSystem.d.ts +32 -17
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +503 -736
- package/dist/internal/volumeLimits.d.ts +53 -0
- package/dist/internal/volumeLimits.d.ts.map +1 -0
- package/dist/internal/volumeLimits.js +63 -0
- package/dist/internal/volumeState.d.ts +2 -0
- package/dist/internal/volumeState.d.ts.map +1 -1
- package/dist/internal/volumeState.js +9 -0
- package/dist/internal/watchHub.d.ts +6 -2
- package/dist/internal/watchHub.d.ts.map +1 -1
- package/dist/internal/watchHub.js +31 -6
- package/package.json +9 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Arr from "effect/Array";
|
|
2
2
|
import * as ByteSize from "effect/ByteSize";
|
|
3
|
+
import * as Cause from "effect/Cause";
|
|
3
4
|
import * as Clock from "effect/Clock";
|
|
4
5
|
import * as Context from "effect/Context";
|
|
5
6
|
import * as Crypto from "effect/Crypto";
|
|
@@ -13,18 +14,20 @@ import * as Option from "effect/Option";
|
|
|
13
14
|
import * as Predicate from "effect/Predicate";
|
|
14
15
|
import * as Result from "effect/Result";
|
|
15
16
|
import * as Schema from "effect/Schema";
|
|
16
|
-
import * as Scope from "effect/Scope";
|
|
17
17
|
import * as Semaphore from "effect/Semaphore";
|
|
18
18
|
import * as Stream from "effect/Stream";
|
|
19
19
|
import { BytePath } from "../BytePath.js";
|
|
20
|
-
import { CallerId, MkdirOptions,
|
|
20
|
+
import { CallerId, MkdirOptions, OpenEntryOptions, OpenOptions, RemoveOptions, RootCallerOptions, SetattrOptions, SymlinkOptions, WalkOptions, WriteFileOptions } from "../Caller.js";
|
|
21
21
|
import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
|
|
22
22
|
import { Mode, OwnerUpdate, Times } from "../Metadata.js";
|
|
23
|
-
import { isEntry,
|
|
23
|
+
import { isEntry, Target } from "../Target.js";
|
|
24
24
|
import { make as makeError } from "../VfsError.js";
|
|
25
|
-
import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation
|
|
26
|
-
import { WatchOptions } from "../Watch.js";
|
|
25
|
+
import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
|
|
26
|
+
import { CallerWatchOptions, WatchOptions } from "../Watch.js";
|
|
27
27
|
import { sameBytes } from "./bytes.js";
|
|
28
|
+
import * as CallerWatch from "./callerWatch.js";
|
|
29
|
+
import * as Confinement from "./confinement.js";
|
|
30
|
+
import * as DirectoryEntries from "./directoryEntries.js";
|
|
28
31
|
import { argumentFailure, decodeConfiguration, errorPath, fsFailure, imageFailure, OpContext, retargetFailure, VfsError } from "./errors.js";
|
|
29
32
|
import { KeySecret, VolumeEpoch } from "./hex128.js";
|
|
30
33
|
import { hmacSha256, sameTag } from "./hmac.js";
|
|
@@ -33,10 +36,13 @@ import * as InodeTable from "./inodeTable.js";
|
|
|
33
36
|
import * as LiveImage from "./liveImage.js";
|
|
34
37
|
import * as MetadataDomain from "./metadata.js";
|
|
35
38
|
import { compareOverlay } from "./overlayDiff.js";
|
|
36
|
-
import {
|
|
39
|
+
import { inputBytes, isAttachedBytes, isDotComponent, joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
|
|
40
|
+
import * as Resolution from "./resolution.js";
|
|
37
41
|
import { VolumeTestSeams } from "./testSeams.js";
|
|
42
|
+
import * as TokenRegistry from "./tokenRegistry.js";
|
|
38
43
|
import { makeTurnstile } from "./turnstile.js";
|
|
39
|
-
import
|
|
44
|
+
import * as Limits from "./volumeLimits.js";
|
|
45
|
+
import { byEntryName, directoryMetadata, emptyState, getNode, Ino, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
40
46
|
import * as WatchHub from "./watchHub.js";
|
|
41
47
|
const EXECUTE = 0o1;
|
|
42
48
|
const WRITE = 0o2;
|
|
@@ -45,11 +51,23 @@ const ANY_EXECUTE = 0o111;
|
|
|
45
51
|
const SET_ID_BITS = 0o6000;
|
|
46
52
|
const STICKY_BIT = 0o1000;
|
|
47
53
|
const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
|
|
54
|
+
/** @internal */
|
|
55
|
+
export const memoryCommitProvider = Object.freeze({
|
|
56
|
+
mode: "memory",
|
|
57
|
+
commit: () => Effect.succeed("committed"),
|
|
58
|
+
shutdown: Effect.void
|
|
59
|
+
});
|
|
48
60
|
// An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
|
|
49
61
|
const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
|
|
50
62
|
// TODO(#186): Preserve the cause of a provider defect or interruption.
|
|
51
63
|
if (Exit.isFailure(committed)) {
|
|
52
|
-
|
|
64
|
+
if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
|
|
65
|
+
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
66
|
+
}
|
|
67
|
+
const failure = Cause.findErrorOption(committed.cause);
|
|
68
|
+
return Option.isSome(failure)
|
|
69
|
+
? { available: !cleanup, failure: failure.value }
|
|
70
|
+
: { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
53
71
|
}
|
|
54
72
|
if (committed.value === "rejected") {
|
|
55
73
|
return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
|
|
@@ -64,7 +82,7 @@ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
|
|
|
64
82
|
const isMode = Schema.is(Mode);
|
|
65
83
|
const isNatural = Schema.is(Schema.Natural);
|
|
66
84
|
const isLength = Schema.is(SetattrOptions.fields.size.schema);
|
|
67
|
-
const inGroup =
|
|
85
|
+
const inGroup = Resolution.inGroup;
|
|
68
86
|
const isTimestamp = Schema.is(MetadataDomain.Timestamp);
|
|
69
87
|
const isSeekMode = Schema.is(SeekMode);
|
|
70
88
|
const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
|
|
@@ -79,7 +97,6 @@ const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessPrope
|
|
|
79
97
|
const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
|
|
80
98
|
const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
|
|
81
99
|
const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
|
|
82
|
-
const encoder = new TextEncoder();
|
|
83
100
|
// Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
|
|
84
101
|
// status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
|
|
85
102
|
// skips it, so reads at one instant (a frozen or coarse clock) store nothing.
|
|
@@ -98,14 +115,16 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
|
|
|
98
115
|
finished: undefined,
|
|
99
116
|
entries: base.entries,
|
|
100
117
|
usedBytes: base.usedBytes,
|
|
118
|
+
renames: [],
|
|
101
119
|
events: [],
|
|
102
120
|
after: [],
|
|
103
121
|
removed: []
|
|
104
122
|
});
|
|
105
|
-
const
|
|
123
|
+
const lookup = (ino) => {
|
|
106
124
|
const draft = MutableRef.get(cell);
|
|
107
125
|
return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
|
|
108
|
-
}
|
|
126
|
+
};
|
|
127
|
+
const get = (ino) => Effect.sync(() => lookup(ino));
|
|
109
128
|
const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
|
|
110
129
|
const put = (node, changed) => Effect.sync(() => {
|
|
111
130
|
MutableRef.update(cell, (draft) => {
|
|
@@ -116,6 +135,7 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
|
|
|
116
135
|
});
|
|
117
136
|
return {
|
|
118
137
|
revision,
|
|
138
|
+
lookup,
|
|
119
139
|
get,
|
|
120
140
|
put: (node) => put(node, true),
|
|
121
141
|
putQuiet: (node) => put(node, false),
|
|
@@ -153,6 +173,9 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
|
|
|
153
173
|
addUsedBytes: (amount) => Effect.sync(() => {
|
|
154
174
|
MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
|
|
155
175
|
}),
|
|
176
|
+
rename: (rename) => Effect.sync(() => {
|
|
177
|
+
MutableRef.get(cell).renames.push(rename);
|
|
178
|
+
}),
|
|
156
179
|
publish: (event) => Effect.sync(() => {
|
|
157
180
|
MutableRef.get(cell).events.push(event);
|
|
158
181
|
}),
|
|
@@ -189,23 +212,15 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
|
|
|
189
212
|
return finished;
|
|
190
213
|
}),
|
|
191
214
|
actions: Effect.sync(() => {
|
|
192
|
-
const { events, after, removed } = MutableRef.get(cell);
|
|
193
|
-
return { events, after, removed };
|
|
215
|
+
const { events, after, removed, renames } = MutableRef.get(cell);
|
|
216
|
+
return { events, after, removed, renames };
|
|
194
217
|
})
|
|
195
218
|
};
|
|
196
219
|
})
|
|
197
220
|
}) {
|
|
198
221
|
}
|
|
199
|
-
const objectReferences = new WeakMap();
|
|
200
|
-
const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
|
|
201
|
-
const files = new WeakMap();
|
|
202
|
-
const handles = new WeakMap();
|
|
203
222
|
const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
|
|
204
|
-
const withEntries = (directory,
|
|
205
|
-
const entries = new Map(directory.entries);
|
|
206
|
-
edit(entries);
|
|
207
|
-
return { ...directory, entries };
|
|
208
|
-
};
|
|
223
|
+
const withEntries = (directory, entries) => ({ ...directory, entries });
|
|
209
224
|
const withoutLink = (links, parent, name) => {
|
|
210
225
|
const index = links.findIndex((link) => link.parent === parent && link.name === name);
|
|
211
226
|
return index < 0 ? links : [...links.slice(0, index), ...links.slice(index + 1)];
|
|
@@ -247,29 +262,17 @@ const randomHex128 = Effect.gen(function* () {
|
|
|
247
262
|
return Hex.encode(yield* crypto.randomBytes(16));
|
|
248
263
|
}).pipe(Effect.orDie);
|
|
249
264
|
/** @internal */
|
|
250
|
-
export const makeVolume = Effect.fnUntraced(function* (source,
|
|
265
|
+
export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, options, durability = "memory-only") {
|
|
251
266
|
const services = yield* Effect.context();
|
|
252
267
|
// These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
|
|
253
268
|
const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
|
|
254
269
|
const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
recovered.maxEntries = live.limits.maxEntries;
|
|
260
|
-
if (live.limits.maxBytes !== undefined)
|
|
261
|
-
recovered.maxBytes = ByteSize.bytes(live.limits.maxBytes);
|
|
262
|
-
if (live.limits.maxFileBytes !== undefined)
|
|
263
|
-
recovered.maxFileBytes = ByteSize.bytes(live.limits.maxFileBytes);
|
|
264
|
-
if (live.limits.maxPathBytes !== undefined)
|
|
265
|
-
recovered.maxPathBytes = ByteSize.bytes(live.limits.maxPathBytes);
|
|
266
|
-
restoredOptions = recovered;
|
|
267
|
-
}
|
|
268
|
-
const decoded = yield* Effect.fromResult(decodeConfiguration(VolumeOptions, restoredOptions === undefined ? {} : restoredOptions, "make"));
|
|
269
|
-
const settings = { ...decoded };
|
|
270
|
-
const identity = settings.identity === undefined
|
|
270
|
+
const decoded = yield* Effect.fromResult(Limits.configuration(options));
|
|
271
|
+
const requested = decoded.limits;
|
|
272
|
+
const limits = live === undefined ? requested : yield* Effect.fromResult(Limits.fromStored(live.limits, requested));
|
|
273
|
+
const identity = live?.identity ?? (decoded.identity === undefined
|
|
271
274
|
? VolumeIdentity.make(yield* randomHex128)
|
|
272
|
-
:
|
|
275
|
+
: decoded.identity);
|
|
273
276
|
const incarnation = VolumeIncarnation.make(yield* randomHex128);
|
|
274
277
|
const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
|
|
275
278
|
const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
|
|
@@ -288,24 +291,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
288
291
|
: Effect.fail(op.fail("InvalidArgument"));
|
|
289
292
|
});
|
|
290
293
|
const volumeIdentity = Symbol();
|
|
291
|
-
const tokens = new Map();
|
|
292
294
|
const directoryHolds = new Map();
|
|
293
|
-
const
|
|
294
|
-
volume: volumeIdentity,
|
|
295
|
-
scope: undefined,
|
|
296
|
-
closed: false,
|
|
297
|
-
ino: undefined,
|
|
298
|
-
offset: 0n,
|
|
299
|
-
access,
|
|
300
|
-
append
|
|
301
|
-
});
|
|
302
|
-
const makeDirectoryReference = (ino) => ({
|
|
303
|
-
volume: volumeIdentity,
|
|
304
|
-
scope: undefined,
|
|
305
|
-
closed: false,
|
|
306
|
-
ino
|
|
307
|
-
});
|
|
308
|
-
const maxPendingOperations = settings.maxPendingOperations ?? 64;
|
|
295
|
+
const maxPendingOperations = limits.maxPendingOperations;
|
|
309
296
|
const permits = maxPendingOperations + 1;
|
|
310
297
|
const gate = Semaphore.makeUnsafe(permits);
|
|
311
298
|
// The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
|
|
@@ -322,91 +309,79 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
322
309
|
onNone: () => op.fail("VolumeBusy"),
|
|
323
310
|
onSome: Effect.succeed
|
|
324
311
|
})));
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
inodes: InodeTable.set(InodeTable.empty(), ROOT_INO, root),
|
|
328
|
-
open: new Map(),
|
|
329
|
-
nextInode: Ino(2),
|
|
330
|
-
revision: 1n,
|
|
331
|
-
entries: 0,
|
|
332
|
-
usedBytes: 0n
|
|
333
|
-
};
|
|
334
|
-
const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(MAX_FILE_BYTES)));
|
|
335
|
-
const limits = Object.freeze({
|
|
336
|
-
maxBytes: settings.maxBytes,
|
|
337
|
-
maxFileBytes: ByteSize.bytes(maxFileBytes),
|
|
338
|
-
maxEntries: settings.maxEntries,
|
|
339
|
-
maxPathBytes: settings.maxPathBytes,
|
|
340
|
-
maxPendingOperations: settings.maxPendingOperations ?? 64,
|
|
341
|
-
maxWatchEvents: settings.maxWatchEvents ?? 256
|
|
342
|
-
});
|
|
312
|
+
let state = emptyState(initialTime);
|
|
313
|
+
const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
|
|
343
314
|
if (Predicate.isTagged("Restored")(source)) {
|
|
344
315
|
const restored = yield* reachableValue(source.value);
|
|
345
316
|
if (restored.largestFile > maxFileBytes) {
|
|
346
317
|
return yield* imageFailure("snapshot", "LimitExceeded", { field: "maxFileBytes" });
|
|
347
318
|
}
|
|
348
|
-
if ((
|
|
349
|
-
(
|
|
319
|
+
if ((limits.maxEntries !== undefined && restored.state.entries > limits.maxEntries) ||
|
|
320
|
+
(limits.maxBytes !== undefined && restored.state.usedBytes > ByteSize.toBigInt(limits.maxBytes))) {
|
|
350
321
|
return yield* imageFailure("snapshot", "LimitExceeded", { field: "volume" });
|
|
351
322
|
}
|
|
352
323
|
state = restored.state;
|
|
353
324
|
}
|
|
354
325
|
if (live !== undefined)
|
|
355
326
|
state = live.value;
|
|
356
|
-
const initialImage = captureInitial === undefined
|
|
357
|
-
? undefined
|
|
358
|
-
: yield* captureInitial(state, { identity, epoch, keySecret }, limits);
|
|
359
327
|
const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
|
|
328
|
+
// Select the operation's namespace at execution time; never retain it across operations.
|
|
329
|
+
const authorityView = Effect.map(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.lookup : (ino) => getNode(state, ino));
|
|
330
|
+
const registry = TokenRegistry.make(volumeIdentity, view);
|
|
331
|
+
const referenceFor = registry.referenceFor;
|
|
332
|
+
const makeFileReference = registry.file;
|
|
333
|
+
const makeDirectoryReference = registry.directory;
|
|
360
334
|
let available = true;
|
|
361
335
|
const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
|
|
362
336
|
const watchCoordinate = (effect) => changing(effect);
|
|
363
|
-
const watchHub = yield* WatchHub.make(watchCoordinate,
|
|
337
|
+
const watchHub = yield* WatchHub.make(watchCoordinate, limits.maxWatchEvents);
|
|
364
338
|
const install = Effect.fnUntraced(function* (finished) {
|
|
365
339
|
const before = state;
|
|
366
340
|
state = yield* finished.finish;
|
|
367
341
|
const actions = yield* finished.actions;
|
|
368
342
|
for (const ino of actions.removed)
|
|
369
|
-
|
|
343
|
+
registry.forget(ino);
|
|
370
344
|
for (const apply of actions.after)
|
|
371
345
|
apply();
|
|
372
346
|
const after = state;
|
|
373
|
-
watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), {
|
|
347
|
+
watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), {
|
|
348
|
+
before,
|
|
349
|
+
after,
|
|
350
|
+
renames: actions.renames
|
|
351
|
+
});
|
|
374
352
|
});
|
|
375
353
|
const transition = Effect.fnUntraced(function* (change) {
|
|
376
354
|
const current = yield* Draft.make(state);
|
|
377
355
|
const value = yield* Effect.provideService(change, Draft, current);
|
|
378
356
|
return [value, current];
|
|
379
357
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
yield*
|
|
383
|
-
yield* install(current);
|
|
384
|
-
});
|
|
385
|
-
// Offers the finished draft's value to the store before installing it. A rejected candidate is discarded and
|
|
386
|
-
// the volume stays available; an uncertain answer stops the volume. A change that is a cleanup runs its
|
|
387
|
-
// release even when the store refuses, since the handle must not stay open.
|
|
388
|
-
const committed = Effect.fnUntraced(function* (op, provider, finished, onStorageFailure) {
|
|
389
|
-
if (yield* finished.unchanged)
|
|
358
|
+
// Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
|
|
359
|
+
const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
|
|
360
|
+
if ((yield* finished.unchanged) || (cleanup && !available))
|
|
390
361
|
return yield* install(finished);
|
|
391
362
|
const next = yield* finished.finish;
|
|
392
|
-
|
|
393
|
-
yield* provider.prepare(next);
|
|
394
|
-
const answer = yield* offerCommit(provider, op.operation, next, onStorageFailure !== undefined);
|
|
363
|
+
const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
|
|
395
364
|
if (!answer.available)
|
|
396
365
|
available = false;
|
|
397
366
|
if (answer.failure !== undefined) {
|
|
398
|
-
if (
|
|
399
|
-
yield*
|
|
367
|
+
if (cleanup)
|
|
368
|
+
yield* install(finished);
|
|
400
369
|
return yield* answer.failure;
|
|
401
370
|
}
|
|
402
371
|
yield* install(finished);
|
|
403
372
|
});
|
|
373
|
+
const applyCleanup = Effect.fnUntraced(function* (change) {
|
|
374
|
+
const current = yield* Draft.make(state);
|
|
375
|
+
yield* Effect.provideService(change(current), Draft, current);
|
|
376
|
+
yield* committed(OpContext.make("close"), current, true);
|
|
377
|
+
});
|
|
404
378
|
const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
|
|
405
|
-
// Permit waits stay interruptible.
|
|
406
|
-
//
|
|
407
|
-
const coordinated = (op, effect,
|
|
408
|
-
|
|
409
|
-
|
|
379
|
+
// Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
|
|
380
|
+
// interruptible until the candidate is ready. Every provider commit and installation is masked.
|
|
381
|
+
const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
|
|
382
|
+
const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
|
|
383
|
+
return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
|
|
384
|
+
}))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
410
385
|
const coordinatedRead = (op, effect) => admit(op, observing(Effect.andThen(checkAvailable(op.operation), effect))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
411
386
|
const refreshAccess = Effect.fnUntraced(function* (read) {
|
|
412
387
|
if (refreshDue(read)) {
|
|
@@ -423,9 +398,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
423
398
|
? coordinated(op, Effect.flatMap(read, refreshAccess))
|
|
424
399
|
: Effect.succeed(observed.value));
|
|
425
400
|
const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
|
|
426
|
-
const shutdown = commitProvider ===
|
|
401
|
+
const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
|
|
427
402
|
available = false;
|
|
428
|
-
}));
|
|
403
|
+
}), commitProvider.shutdown)));
|
|
429
404
|
const pathOf = (get, ino) => {
|
|
430
405
|
const names = [];
|
|
431
406
|
let node = get(ino);
|
|
@@ -456,10 +431,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
456
431
|
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
457
432
|
});
|
|
458
433
|
const entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
|
|
459
|
-
const publishEntry = (_tag, parent, name, ino)
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
434
|
+
const publishEntry = Effect.fnUntraced(function* (_tag, parent, name, ino) {
|
|
435
|
+
const before = state;
|
|
436
|
+
const draft = yield* Draft;
|
|
437
|
+
yield* draft.publish((installed) => {
|
|
438
|
+
const prefix = pathOf((at) => getNode(installed, at), parent) ?? pathOf((at) => getNode(before, at), parent);
|
|
439
|
+
return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino, name }];
|
|
440
|
+
});
|
|
441
|
+
});
|
|
463
442
|
const publishNode = Effect.fnUntraced(function* (ino) {
|
|
464
443
|
const draft = yield* Draft;
|
|
465
444
|
const target = yield* draft.get(ino);
|
|
@@ -524,16 +503,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
524
503
|
includes: () => true,
|
|
525
504
|
rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
|
|
526
505
|
};
|
|
527
|
-
const scopeRoot = (scope, op)
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
return Effect.fail(op.fail("InvalidReference"));
|
|
531
|
-
if (known.volume !== volumeIdentity)
|
|
532
|
-
return Effect.fail(op.fail("ForeignReference"));
|
|
533
|
-
const node = getNode(state, known.ino);
|
|
534
|
-
return node === undefined || node.metadata.nlink === 0
|
|
535
|
-
? Effect.fail(op.fail("StaleReference"))
|
|
536
|
-
: Effect.succeed(known.ino);
|
|
506
|
+
const scopeRoot = Effect.fnUntraced(function* (scope, op) {
|
|
507
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(scope, "reference"), "reference", op);
|
|
508
|
+
return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
|
|
537
509
|
});
|
|
538
510
|
// A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
|
|
539
511
|
// each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
|
|
@@ -566,10 +538,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
566
538
|
yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
|
|
567
539
|
return { snapshot, observation: yield* observeChanges(captured) };
|
|
568
540
|
});
|
|
569
|
-
const addressable = (ino) => {
|
|
570
|
-
const node = getNode(state, ino);
|
|
571
|
-
return node !== undefined && !(node.kind === "directory" && node.metadata.nlink === 0);
|
|
572
|
-
};
|
|
573
541
|
const keyTag = (ino) => {
|
|
574
542
|
const message = new Uint8Array(40);
|
|
575
543
|
message.set(identityBytes);
|
|
@@ -577,15 +545,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
577
545
|
new DataView(message.buffer).setBigUint64(32, ino);
|
|
578
546
|
return hmacSha256(keySecretBytes, message).subarray(0, 16);
|
|
579
547
|
};
|
|
580
|
-
const referenceFor = (ino) => {
|
|
581
|
-
const existing = tokens.get(ino);
|
|
582
|
-
if (existing !== undefined)
|
|
583
|
-
return existing;
|
|
584
|
-
const reference = Object.freeze({ [ObjectReferenceId]: true });
|
|
585
|
-
objectReferences.set(reference, { volume: volumeIdentity, ino });
|
|
586
|
-
tokens.set(ino, reference);
|
|
587
|
-
return reference;
|
|
588
|
-
};
|
|
589
548
|
const holdDirectory = (ino) => {
|
|
590
549
|
directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
|
|
591
550
|
};
|
|
@@ -598,32 +557,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
598
557
|
directoryHolds.delete(ino);
|
|
599
558
|
const node = getNode(state, ino);
|
|
600
559
|
if (node?.kind === "directory" && node.metadata.nlink === 0) {
|
|
601
|
-
yield*
|
|
560
|
+
yield* applyCleanup((d) => d.remove(ino));
|
|
602
561
|
}
|
|
603
562
|
});
|
|
604
|
-
const releaseDirectory =
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
reference
|
|
608
|
-
if (ino !== undefined)
|
|
609
|
-
yield* unholdDirectory(ino);
|
|
610
|
-
});
|
|
611
|
-
const finalizeDirectory = (reference) => Effect.uninterruptible(Effect.suspend(() => reference.closed
|
|
612
|
-
? Effect.void
|
|
613
|
-
: coordinatedCleanup(releaseDirectory(reference))));
|
|
614
|
-
const closeReleasedScope = (reference) => Effect.suspend(() => !reference.closed || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
|
|
615
|
-
const permitted = (node, identity) => {
|
|
616
|
-
const metadata = node.metadata;
|
|
617
|
-
const shift = metadata.uid === identity.uid ?
|
|
618
|
-
6
|
|
619
|
-
: inGroup(identity, metadata.gid)
|
|
620
|
-
? 3
|
|
621
|
-
: 0;
|
|
622
|
-
return (metadata.mode >> shift) & (READ | WRITE | EXECUTE);
|
|
563
|
+
const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
|
|
564
|
+
const finalizeDirectory = (reference) => {
|
|
565
|
+
const close = coordinatedCleanup(releaseDirectory(reference));
|
|
566
|
+
return TokenRegistry.finalize(reference, close, Effect.ignore(close));
|
|
623
567
|
};
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
568
|
+
const closeReleasedScope = TokenRegistry.closeReleasedScope;
|
|
569
|
+
const permitted = Resolution.permitted;
|
|
570
|
+
const authorize = Resolution.authorize;
|
|
627
571
|
const reclaim = Effect.fnUntraced(function* (d, ino) {
|
|
628
572
|
const node = yield* d.get(ino);
|
|
629
573
|
if (node === undefined)
|
|
@@ -644,7 +588,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
644
588
|
});
|
|
645
589
|
const atEntryLimit = Effect.gen(function* () {
|
|
646
590
|
const draft = yield* Draft;
|
|
647
|
-
return
|
|
591
|
+
return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
|
|
648
592
|
});
|
|
649
593
|
const reserveEntry = Effect.fnUntraced(function* (op) {
|
|
650
594
|
const draft = yield* Draft;
|
|
@@ -653,10 +597,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
653
597
|
});
|
|
654
598
|
const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
|
|
655
599
|
const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
|
|
656
|
-
if (
|
|
600
|
+
if (limits.maxBytes === undefined)
|
|
657
601
|
return;
|
|
658
602
|
const draft = yield* Draft;
|
|
659
|
-
if (bytes > ByteSize.toBigInt(
|
|
603
|
+
if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
|
|
660
604
|
return yield* op.fail("NoSpace");
|
|
661
605
|
}
|
|
662
606
|
});
|
|
@@ -670,7 +614,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
670
614
|
mtimeNs: now,
|
|
671
615
|
ctimeNs: now
|
|
672
616
|
}
|
|
673
|
-
}, (entries
|
|
617
|
+
}, DirectoryEntries.set(parent.entries, name, child.ino)));
|
|
674
618
|
if (child.kind === "directory")
|
|
675
619
|
yield* d.put({ ...child, parent: parent.ino, name });
|
|
676
620
|
else {
|
|
@@ -692,60 +636,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
692
636
|
}
|
|
693
637
|
yield* reclaim(d, child.ino);
|
|
694
638
|
});
|
|
695
|
-
const
|
|
696
|
-
const ino = ref.ino;
|
|
697
|
-
if (ino !== undefined) {
|
|
698
|
-
const d = yield* Draft;
|
|
699
|
-
yield* d.release(ino);
|
|
700
|
-
yield* reclaim(d, ino);
|
|
701
|
-
}
|
|
702
|
-
ref.ino = undefined;
|
|
703
|
-
ref.closed = true;
|
|
704
|
-
});
|
|
705
|
-
const releaseOpenFile = (ref) => coordinatedCleanup(Effect.suspend(() => ref.closed ? Effect.void : applyDirect(() => releaseFile(ref))));
|
|
706
|
-
// Explicit close and scope cleanup share one release. A release whose commit fails still completes as
|
|
707
|
-
// cleanup, so the handle never stays open; only an explicit close reports the failure. A close refused
|
|
708
|
-
// admission never entered the volume, so it leaves the handle open for a retry.
|
|
709
|
-
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, Effect.gen(function* () {
|
|
710
|
-
const ino = ref.ino;
|
|
711
|
-
if (ino !== undefined) {
|
|
712
|
-
const d = yield* Draft;
|
|
713
|
-
yield* d.release(ino);
|
|
714
|
-
yield* reclaim(d, ino);
|
|
715
|
-
}
|
|
639
|
+
const releaseFileInode = Effect.fnUntraced(function* (ino) {
|
|
716
640
|
const d = yield* Draft;
|
|
717
|
-
yield* d.
|
|
718
|
-
|
|
719
|
-
ref.closed = true;
|
|
720
|
-
});
|
|
721
|
-
})), () => applyDirect(() => releaseFile(ref))).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
|
|
722
|
-
// Keyed on the file rather than the closed flag, so a rerun releases an open that published after a first run.
|
|
723
|
-
// Cleanup is uninterruptible and does not need admission, so an interrupted or busy scope close still releases.
|
|
724
|
-
const finalizeFile = (ref) => Effect.uninterruptible(Effect.suspend(() => ref.ino === undefined
|
|
725
|
-
? Effect.sync(() => {
|
|
726
|
-
ref.closed = true;
|
|
727
|
-
})
|
|
728
|
-
: Effect.ignore(closeFile(ref, OpContext.make("close"), Effect.void)).pipe(Effect.andThen(Effect.suspend(() => ref.closed ? Effect.void : releaseOpenFile(ref))))));
|
|
729
|
-
// Acquires a handle into its own scope, forked from the caller's. The finalizer is registered before waiting,
|
|
730
|
-
// since a closed scope runs a new finalizer at once and the permit is not reentrant. A scope that closes
|
|
731
|
-
// before or during acquisition interrupts it, and releasing here, under the permit, keeps the acquisition
|
|
732
|
-
// from outliving a finalizer that already ran. A scope that closes while the acquisition commits interrupts it
|
|
733
|
-
// too. Every exit without a handle reruns the finalizer, which may have run before the commit published and
|
|
734
|
-
// which closing an already closed scope would not run again.
|
|
735
|
-
const acquireHandle = Effect.fnUntraced(function* (reference, coordinate, acquire, releaseAcquired, finalize) {
|
|
736
|
-
const scope = yield* Scope.fork(yield* Effect.scope);
|
|
737
|
-
reference.scope = scope;
|
|
738
|
-
yield* Scope.addFinalizer(scope, finalize);
|
|
739
|
-
const closed = () => Predicate.isTagged(scope.state, "Closed");
|
|
740
|
-
return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquire).pipe(Effect.tap(() => Effect.suspend(() => {
|
|
741
|
-
if (!closed())
|
|
742
|
-
return Effect.void;
|
|
743
|
-
return Effect.andThen(releaseAcquired(), Effect.interrupt);
|
|
744
|
-
})))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalize, Scope.close(scope, Exit.void))));
|
|
641
|
+
yield* d.release(ino);
|
|
642
|
+
yield* reclaim(d, ino);
|
|
745
643
|
});
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
644
|
+
const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
|
|
645
|
+
const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
|
|
646
|
+
// Admission refusal leaves the handle open. Every admitted close shares the registry release,
|
|
647
|
+
// and a storage failure still completes cleanup while only explicit close reports the failure.
|
|
648
|
+
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, TokenRegistry.release(ref, releaseFileInode, (action) => Effect.flatMap(Draft, (draft) => draft.afterInstall(action)))), true).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
|
|
649
|
+
const finalizeFile = (ref) => TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref));
|
|
650
|
+
const acquireHandle = TokenRegistry.acquire;
|
|
651
|
+
const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
|
|
749
652
|
const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
|
|
750
653
|
const d = yield* Draft;
|
|
751
654
|
yield* d.addUsedBytes(BigInt(data.length - file.data.length));
|
|
@@ -779,11 +682,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
779
682
|
yield* publishNode(file.ino);
|
|
780
683
|
});
|
|
781
684
|
const fileHandle = (ref) => {
|
|
782
|
-
const get = Effect.fnUntraced(function* (op, access) {
|
|
783
|
-
const
|
|
685
|
+
const get = Effect.fnUntraced(function* (op, access, cleanup = false) {
|
|
686
|
+
const ino = TokenRegistry.inode(ref);
|
|
687
|
+
const node = ino === undefined ? undefined : yield* view(ino);
|
|
784
688
|
if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
|
|
785
689
|
(access === "write" && ref.access === "read"))
|
|
786
690
|
return yield* op.fail("InvalidHandle");
|
|
691
|
+
if (!cleanup && ref.authority !== undefined)
|
|
692
|
+
yield* ref.authority(node, op);
|
|
787
693
|
return node;
|
|
788
694
|
});
|
|
789
695
|
const read = (maximum, position) => {
|
|
@@ -835,9 +741,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
835
741
|
return yield* writeOp.fail("FileTooLarge");
|
|
836
742
|
const start = Number(offset);
|
|
837
743
|
const draft = yield* Draft;
|
|
838
|
-
const free =
|
|
744
|
+
const free = limits.maxBytes === undefined
|
|
839
745
|
? BigInt(maxFileBytes)
|
|
840
|
-
: ByteSize.toBigInt(
|
|
746
|
+
: ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
|
|
841
747
|
const maximumEnd = BigInt(file.data.length) + free;
|
|
842
748
|
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
843
749
|
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
@@ -865,19 +771,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
865
771
|
const closeOp = OpContext.make("close");
|
|
866
772
|
const handle = Object.freeze({
|
|
867
773
|
[FileHandleId]: true,
|
|
868
|
-
read: Effect.
|
|
774
|
+
read: Effect.fnUntraced(function* (maximum) {
|
|
869
775
|
return (yield* read(maximum)).bytes;
|
|
870
776
|
}),
|
|
871
|
-
pread: Effect.
|
|
777
|
+
pread: Effect.fnUntraced(function* (maximum, offset) {
|
|
872
778
|
return yield* read(maximum, offset);
|
|
873
779
|
}),
|
|
874
|
-
write: Effect.
|
|
780
|
+
write: Effect.fnUntraced(function* (bytes) {
|
|
875
781
|
return yield* write(bytes);
|
|
876
782
|
}),
|
|
877
|
-
pwrite: Effect.
|
|
783
|
+
pwrite: Effect.fnUntraced(function* (bytes, offset) {
|
|
878
784
|
return yield* write(bytes, offset);
|
|
879
785
|
}),
|
|
880
|
-
seek: Effect.
|
|
786
|
+
seek: Effect.fnUntraced(function* (offset, mode) {
|
|
881
787
|
const op = OpContext.make("seek");
|
|
882
788
|
return yield* coordinatedRead(op, Effect.uninterruptible(Effect.gen(function* () {
|
|
883
789
|
const file = yield* get(op);
|
|
@@ -902,43 +808,66 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
902
808
|
return next;
|
|
903
809
|
})));
|
|
904
810
|
}),
|
|
905
|
-
truncate: Effect.
|
|
811
|
+
truncate: Effect.fnUntraced(function* (length) {
|
|
906
812
|
const op = OpContext.make("truncate");
|
|
907
813
|
return yield* coordinated(op, Effect.gen(function* () {
|
|
908
814
|
yield* resize(yield* get(op, "write"), length, op);
|
|
909
815
|
}));
|
|
910
816
|
}),
|
|
911
|
-
stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata))
|
|
912
|
-
sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp))))
|
|
913
|
-
close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref))
|
|
817
|
+
stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata)),
|
|
818
|
+
sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))),
|
|
819
|
+
close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp, undefined, true))).pipe(Effect.ensuring(closeReleasedScope(ref)))
|
|
914
820
|
});
|
|
915
|
-
|
|
821
|
+
registry.registerFile(handle, ref);
|
|
916
822
|
return handle;
|
|
917
823
|
};
|
|
918
|
-
const createCaller = (reference, identity, umask) => {
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
return yield* op.fail("ForeignReference");
|
|
929
|
-
}
|
|
930
|
-
const node = yield* view(known.ino);
|
|
931
|
-
if (node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)) {
|
|
932
|
-
return yield* op.fail("StaleReference");
|
|
933
|
-
}
|
|
934
|
-
return node;
|
|
824
|
+
const createCaller = (reference, identity, umask, roots = [], inherited = []) => {
|
|
825
|
+
const boundary = roots.length === 0 ? undefined : Confinement.make(authorityView, roots);
|
|
826
|
+
inherited = inherited.filter((chain, index) => !((chain.length <= roots.length && chain.every((ino, at) => roots[at] === ino)) ||
|
|
827
|
+
inherited.slice(0, index).some((other) => other.length === chain.length && chain.every((ino, at) => other[at] === ino))));
|
|
828
|
+
const additional = inherited.map((chain) => Confinement.make(authorityView, chain));
|
|
829
|
+
const checkAuthority = Effect.fnUntraced(function* (node, op) {
|
|
830
|
+
if (boundary !== undefined)
|
|
831
|
+
yield* boundary.check(node, op);
|
|
832
|
+
for (const authority of additional)
|
|
833
|
+
yield* authority.check(node, op);
|
|
935
834
|
});
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
835
|
+
const inheritedFor = (target) => Target.$is("Handle")(target)
|
|
836
|
+
? registry.handleBoundaries(target.handle)
|
|
837
|
+
: Target.$is("Path")(target) && target.relativeTo !== undefined
|
|
838
|
+
? registry.handleBoundaries(target.relativeTo)
|
|
839
|
+
: [];
|
|
840
|
+
const boundariesFor = (target) => [...(roots.length > 0 ? [roots] : []), ...inherited, ...(target === undefined ? [] : inheritedFor(target))];
|
|
841
|
+
const assignAuthority = (ref, target) => {
|
|
842
|
+
const chains = boundariesFor(target).filter((chain, index, all) => !all.some((other, at) => at !== index && other.length >= chain.length && chain.every((ino, offset) => other[offset] === ino) &&
|
|
843
|
+
(other.length > chain.length || at < index)));
|
|
844
|
+
if (chains.length === 0)
|
|
845
|
+
return;
|
|
846
|
+
ref.boundaries = chains;
|
|
847
|
+
const authorities = chains.map((chain) => Confinement.make(authorityView, chain));
|
|
848
|
+
ref.authority = Effect.fnUntraced(function* (node, op) {
|
|
849
|
+
for (const authority of authorities)
|
|
850
|
+
yield* authority.check(node, op, true);
|
|
851
|
+
});
|
|
852
|
+
};
|
|
853
|
+
const callerPath = boundary?.path ?? pathOfCurrent;
|
|
854
|
+
const resolver = Resolution.make({
|
|
855
|
+
caller: reference,
|
|
856
|
+
confinement: boundary,
|
|
857
|
+
additional: additional.length === 0 ? undefined : additional,
|
|
858
|
+
get: view,
|
|
859
|
+
authorityView,
|
|
860
|
+
identity,
|
|
861
|
+
maxPathBytes: limits.maxPathBytes,
|
|
862
|
+
registry
|
|
941
863
|
});
|
|
864
|
+
const prepare = resolver.prepare;
|
|
865
|
+
const prepareEntry = resolver.prepareEntry;
|
|
866
|
+
const entryName = Resolution.entryName;
|
|
867
|
+
const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
|
|
868
|
+
const resolveDirectory = resolver.directory;
|
|
869
|
+
const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
|
|
870
|
+
const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
|
|
942
871
|
const directoryNow = Effect.fnUntraced(function* (ino) {
|
|
943
872
|
const node = yield* view(ino);
|
|
944
873
|
if (node?.kind !== "directory") {
|
|
@@ -1008,141 +937,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1008
937
|
revision: 0n
|
|
1009
938
|
};
|
|
1010
939
|
});
|
|
1011
|
-
const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
|
|
1012
|
-
const pathOp = op.at(path.input);
|
|
1013
|
-
const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
|
|
1014
|
-
if (reference.ino === undefined) {
|
|
1015
|
-
return yield* pathOp.fail("ClosedCaller");
|
|
1016
|
-
}
|
|
1017
|
-
let current = path.absolute ? (yield* nodeNow(ROOT_INO)) : yield* callerDirectory(pathOp);
|
|
1018
|
-
if (!path.absolute && referencedBase !== undefined) {
|
|
1019
|
-
current = referencedBase;
|
|
1020
|
-
yield* authorize(current, identity, EXECUTE, op);
|
|
1021
|
-
}
|
|
1022
|
-
else if (!path.absolute && base !== undefined) {
|
|
1023
|
-
const target = handles.get(base);
|
|
1024
|
-
if (target === undefined) {
|
|
1025
|
-
return yield* pathOp.fail("InvalidHandle");
|
|
1026
|
-
}
|
|
1027
|
-
if (target.volume !== volumeIdentity) {
|
|
1028
|
-
return yield* pathOp.fail("ForeignHandle");
|
|
1029
|
-
}
|
|
1030
|
-
const directory = target.ino === undefined ? undefined : (yield* view(target.ino));
|
|
1031
|
-
if (directory?.kind !== "directory") {
|
|
1032
|
-
return yield* pathOp.fail("InvalidHandle");
|
|
1033
|
-
}
|
|
1034
|
-
current = directory;
|
|
1035
|
-
yield* authorize(current, identity, EXECUTE, pathOp);
|
|
1036
|
-
}
|
|
1037
|
-
if (current.metadata.nlink === 0) {
|
|
1038
|
-
return yield* pathOp.fail("NotFound");
|
|
1039
|
-
}
|
|
1040
|
-
let work = path;
|
|
1041
|
-
let parent;
|
|
1042
|
-
let name;
|
|
1043
|
-
let traversals = 0;
|
|
1044
|
-
// Leading components that came from a symbolic link's target. Only the components the caller wrote are
|
|
1045
|
-
// created, so a dangling link stays missing, as it does for mkdir -p.
|
|
1046
|
-
let linked = 0;
|
|
1047
|
-
for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
|
|
1048
|
-
if (current.kind !== "directory") {
|
|
1049
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1050
|
-
}
|
|
1051
|
-
yield* authorize(current, identity, EXECUTE, pathOp);
|
|
1052
|
-
const component = work.components[index];
|
|
1053
|
-
if (component === undefined)
|
|
1054
|
-
break;
|
|
1055
|
-
if (component === DOT_HEX)
|
|
1056
|
-
continue;
|
|
1057
|
-
if (component === DOT_DOT_HEX) {
|
|
1058
|
-
current = yield* directoryNow(current.parent);
|
|
1059
|
-
parent = undefined;
|
|
1060
|
-
name = undefined;
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
parent = current;
|
|
1064
|
-
name = component;
|
|
1065
|
-
const childIno = current.entries.get(component);
|
|
1066
|
-
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1067
|
-
if (child === undefined) {
|
|
1068
|
-
if (allowMissing && index === work.components.length - 1) {
|
|
1069
|
-
return { node: undefined, parent, name };
|
|
1070
|
-
}
|
|
1071
|
-
if (createMissing !== undefined && index >= linked) {
|
|
1072
|
-
current = yield* createMissing(current, component, index === work.components.length - 1);
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
return yield* pathOp.fail("NotFound");
|
|
1076
|
-
}
|
|
1077
|
-
if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
|
|
1078
|
-
if (child.target.length === 0) {
|
|
1079
|
-
return yield* pathOp.fail("NotFound");
|
|
1080
|
-
}
|
|
1081
|
-
if (++traversals > MAX_SYMLINK_TRAVERSALS) {
|
|
1082
|
-
return yield* pathOp.fail("SymlinkLoop");
|
|
1083
|
-
}
|
|
1084
|
-
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
1085
|
-
const remaining = work.components.length - index - 1;
|
|
1086
|
-
if (settings.maxPathBytes !== undefined &&
|
|
1087
|
-
ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
|
|
1088
|
-
return yield* pathOp.fail("PathTooLong");
|
|
1089
|
-
}
|
|
1090
|
-
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
1091
|
-
expansion.set(child.target);
|
|
1092
|
-
expansion.set(suffix, child.target.length);
|
|
1093
|
-
const expanded = preparePath(ownedPath(expansion), op.operation, settings.maxPathBytes);
|
|
1094
|
-
// The expansion is synthetic: its per-component limits are the caller's to hear about,
|
|
1095
|
-
// but the path in the error has to be the one the caller passed in, so the
|
|
1096
|
-
// expansion's own failure is not kept as the cause.
|
|
1097
|
-
if (Result.isFailure(expanded)) {
|
|
1098
|
-
return yield* pathOp.fail(expanded.failure.code);
|
|
1099
|
-
}
|
|
1100
|
-
work = expanded.success;
|
|
1101
|
-
linked = work.components.length - remaining + Math.max(0, linked - index - 1);
|
|
1102
|
-
if (work.absolute)
|
|
1103
|
-
current = yield* nodeNow(ROOT_INO);
|
|
1104
|
-
index = -1;
|
|
1105
|
-
}
|
|
1106
|
-
else
|
|
1107
|
-
current = child;
|
|
1108
|
-
}
|
|
1109
|
-
if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
|
|
1110
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1111
|
-
}
|
|
1112
|
-
return { node: current, parent, name };
|
|
1113
|
-
});
|
|
1114
|
-
const resolveNode = Effect.fnUntraced(function* (path, base, op, options) {
|
|
1115
|
-
const pathOp = op.at(path.input);
|
|
1116
|
-
const result = yield* lookup(path, base, op, options);
|
|
1117
|
-
if (result.node === undefined) {
|
|
1118
|
-
return yield* pathOp.fail("NotFound");
|
|
1119
|
-
}
|
|
1120
|
-
return result.node;
|
|
1121
|
-
});
|
|
1122
|
-
const locate = Effect.fnUntraced(function* (path, base, op, options) {
|
|
1123
|
-
const pathOp = op.at(path.input);
|
|
1124
|
-
const result = yield* lookup(path, base, op, options);
|
|
1125
|
-
const node = result.node;
|
|
1126
|
-
if (node === undefined) {
|
|
1127
|
-
return yield* pathOp.fail("NotFound");
|
|
1128
|
-
}
|
|
1129
|
-
if (node.kind !== "directory") {
|
|
1130
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1131
|
-
}
|
|
1132
|
-
return node;
|
|
1133
|
-
});
|
|
1134
|
-
const ResolvedEntry = {
|
|
1135
|
-
fromPath: Effect.fnUntraced(function* (path, base, op) {
|
|
1136
|
-
const parent = yield* locate(path, base, op, { parentOnly: true });
|
|
1137
|
-
return {
|
|
1138
|
-
parent: parent.ino,
|
|
1139
|
-
name: path.components.at(-1),
|
|
1140
|
-
trailingSlash: path.trailingSlash,
|
|
1141
|
-
addressing: "path",
|
|
1142
|
-
op: op.at(path.input)
|
|
1143
|
-
};
|
|
1144
|
-
})
|
|
1145
|
-
};
|
|
1146
940
|
const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
|
|
1147
941
|
const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
|
|
1148
942
|
? Effect.fail(op.fail("NotPermitted"))
|
|
@@ -1220,6 +1014,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1220
1014
|
: Effect.void;
|
|
1221
1015
|
const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
|
|
1222
1016
|
const parent = yield* directoryNow(entry.parent);
|
|
1017
|
+
if (boundary !== undefined && parent.ino === boundary.root &&
|
|
1018
|
+
(entry.name === undefined || isDotComponent(entry.name)))
|
|
1019
|
+
return yield* entry.op.fail("NotPermitted");
|
|
1223
1020
|
if (isDotComponent(entry.name)) {
|
|
1224
1021
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
|
|
1225
1022
|
}
|
|
@@ -1270,13 +1067,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1270
1067
|
}
|
|
1271
1068
|
else {
|
|
1272
1069
|
entryOp = op.at(prepared.path.input);
|
|
1273
|
-
const resolved = yield*
|
|
1070
|
+
const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
|
|
1071
|
+
? { path: prepared.path.input }
|
|
1072
|
+
: { path: prepared.path.input, relativeTo: prepared.base }), {
|
|
1073
|
+
kind: "Node",
|
|
1274
1074
|
createMissing: (parent, name, final) => Effect.flatMap(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
|
|
1275
1075
|
if (!revisionsBefore.has(parent.ino))
|
|
1276
1076
|
revisionsBefore.set(parent.ino, made.directory.before);
|
|
1277
1077
|
return directoryNow(made.child);
|
|
1278
1078
|
})
|
|
1279
|
-
});
|
|
1079
|
+
}, op);
|
|
1280
1080
|
node = resolved.node;
|
|
1281
1081
|
}
|
|
1282
1082
|
if (node?.kind !== "directory")
|
|
@@ -1323,7 +1123,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1323
1123
|
mtimeNs: now,
|
|
1324
1124
|
ctimeNs: now
|
|
1325
1125
|
}
|
|
1326
|
-
}, (entries
|
|
1126
|
+
}, DirectoryEntries.remove(parent.entries, name)));
|
|
1327
1127
|
yield* publishEntry("Remove", parent.ino, name, child.ino);
|
|
1328
1128
|
yield* detach(child, parent.ino, name, now);
|
|
1329
1129
|
yield* d.addEntries(-1);
|
|
@@ -1332,8 +1132,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1332
1132
|
// Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
|
|
1333
1133
|
// entry, and each verb reports it with its own code. Search permission comes before the lookup and write
|
|
1334
1134
|
// permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
|
|
1135
|
+
const protectsRoot = (parent, name) => boundary !== undefined &&
|
|
1136
|
+
((parent.ino === boundary.root && (name === undefined || isDotComponent(name))) ||
|
|
1137
|
+
(name === "2e2e" && parent.parent === boundary.root));
|
|
1335
1138
|
const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
|
|
1336
1139
|
const parent = yield* directoryNow(entry.parent);
|
|
1140
|
+
if (protectsRoot(parent, entry.name))
|
|
1141
|
+
return yield* entry.op.fail("NotPermitted");
|
|
1337
1142
|
if (isDotComponent(entry.name)) {
|
|
1338
1143
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
|
|
1339
1144
|
}
|
|
@@ -1342,6 +1147,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1342
1147
|
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1343
1148
|
if (child === undefined)
|
|
1344
1149
|
return yield* entry.op.fail("NotFound");
|
|
1150
|
+
if (child.ino === boundary?.root)
|
|
1151
|
+
return yield* entry.op.fail("NotPermitted");
|
|
1345
1152
|
const rejected = beforeWrite?.(child);
|
|
1346
1153
|
if (rejected !== undefined)
|
|
1347
1154
|
return yield* rejected;
|
|
@@ -1379,6 +1186,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1379
1186
|
const sameDirectory = source.parent === destination.parent;
|
|
1380
1187
|
const sourceName = source.name;
|
|
1381
1188
|
const destinationName = destination.name;
|
|
1189
|
+
if (protectsRoot(sourceDirectory, sourceName) || protectsRoot(destinationDirectory, destinationName))
|
|
1190
|
+
return yield* source.op.fail("NotPermitted");
|
|
1382
1191
|
if (isDotComponent(sourceName) || isDotComponent(destinationName)) {
|
|
1383
1192
|
return yield* source.op.fail("InvalidArgument");
|
|
1384
1193
|
}
|
|
@@ -1394,6 +1203,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1394
1203
|
return yield* source.op.fail("NotFound");
|
|
1395
1204
|
const replacedIno = destinationDirectory.entries.get(destinationName);
|
|
1396
1205
|
const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
|
|
1206
|
+
if (boundary !== undefined && (child.ino === boundary.root || replaced?.ino === boundary.root)) {
|
|
1207
|
+
return yield* source.op.fail("NotPermitted");
|
|
1208
|
+
}
|
|
1397
1209
|
// A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
|
|
1398
1210
|
// source is one, as Linux allows.
|
|
1399
1211
|
if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
|
|
@@ -1442,6 +1254,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1442
1254
|
}
|
|
1443
1255
|
const now = yield* timestamp(op);
|
|
1444
1256
|
const d = yield* Draft;
|
|
1257
|
+
yield* d.rename({
|
|
1258
|
+
ino: child.ino,
|
|
1259
|
+
from: { parent: source.parent, name: sourceName },
|
|
1260
|
+
to: { parent: destination.parent, name: destinationName }
|
|
1261
|
+
});
|
|
1445
1262
|
yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
|
|
1446
1263
|
yield* d.put(withEntries({
|
|
1447
1264
|
...sourceDirectory,
|
|
@@ -1451,7 +1268,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1451
1268
|
mtimeNs: now,
|
|
1452
1269
|
ctimeNs: now
|
|
1453
1270
|
}
|
|
1454
|
-
}, (entries
|
|
1271
|
+
}, DirectoryEntries.remove(sourceDirectory.entries, sourceName)));
|
|
1455
1272
|
const destinationNow = yield* directoryNow(destination.parent);
|
|
1456
1273
|
yield* d.put(withEntries({
|
|
1457
1274
|
...destinationNow,
|
|
@@ -1461,7 +1278,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1461
1278
|
mtimeNs: now,
|
|
1462
1279
|
ctimeNs: now
|
|
1463
1280
|
}
|
|
1464
|
-
}, (entries
|
|
1281
|
+
}, DirectoryEntries.set(destinationNow.entries, destinationName, child.ino)));
|
|
1465
1282
|
const moved = yield* nodeNow(child.ino);
|
|
1466
1283
|
if (moved.kind === "directory") {
|
|
1467
1284
|
yield* d.put({
|
|
@@ -1545,9 +1362,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1545
1362
|
const ino = opened();
|
|
1546
1363
|
if (ino === undefined)
|
|
1547
1364
|
return;
|
|
1548
|
-
|
|
1549
|
-
yield* draft.release(ino);
|
|
1550
|
-
yield* reclaim(draft, ino);
|
|
1365
|
+
yield* releaseFileInode(ino);
|
|
1551
1366
|
});
|
|
1552
1367
|
const acquireOpenedFile = (ref, op, acquire) => {
|
|
1553
1368
|
let opened;
|
|
@@ -1555,75 +1370,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1555
1370
|
opened = ino;
|
|
1556
1371
|
}), releasePending(() => opened), finalizeFile(ref));
|
|
1557
1372
|
};
|
|
1558
|
-
const prepare = (input, op) => preparePath(input, op.operation, settings.maxPathBytes);
|
|
1559
|
-
const handleNode = Effect.fnUntraced(function* (handle, op) {
|
|
1560
|
-
if (reference.ino === undefined)
|
|
1561
|
-
return yield* op.fail("ClosedCaller");
|
|
1562
|
-
const ref = isFileHandle(handle) ? files.get(handle) : handles.get(handle);
|
|
1563
|
-
if (ref === undefined)
|
|
1564
|
-
return yield* op.fail("InvalidHandle");
|
|
1565
|
-
if (ref.volume !== volumeIdentity)
|
|
1566
|
-
return yield* op.fail("ForeignHandle");
|
|
1567
|
-
const node = ref.ino === undefined ? undefined : (yield* view(ref.ino));
|
|
1568
|
-
if (node === undefined)
|
|
1569
|
-
return yield* op.fail("InvalidHandle");
|
|
1570
|
-
return node;
|
|
1571
|
-
});
|
|
1572
|
-
// A path prepares before the walk, so a malformed input fails before any lookup, and names the path on
|
|
1573
|
-
// every failure. A reference or handle names none. The verb's `followFinalSymlink` is the default a
|
|
1574
|
-
// path's own setting overrides; `final` makes the verb's setting win, for a verb such as `readLink` whose
|
|
1575
|
-
// meaning fixes it.
|
|
1576
|
-
const resolveTarget = (target, op, options) => Target.$match(target, {
|
|
1577
|
-
Path: ({ followFinalSymlink, path, relativeTo }) => Effect.flatMap(Effect.fromResult(prepare(path, op)), (prepared) => Effect.map(resolveNode(prepared, relativeTo, op, {
|
|
1578
|
-
followFinalSymlink: options?.final === true
|
|
1579
|
-
? options.followFinalSymlink ?? true
|
|
1580
|
-
: followFinalSymlink ?? options?.followFinalSymlink ?? true
|
|
1581
|
-
}), (node) => ({ node, op: op.at(path) }))),
|
|
1582
|
-
Reference: ({ reference }) => Effect.map(referencedNode(reference, op), (node) => ({ node, op })),
|
|
1583
|
-
Handle: ({ handle }) => Effect.map(handleNode(handle, op), (node) => ({ node, op }))
|
|
1584
|
-
});
|
|
1585
|
-
const resolveDirectory = Effect.fnUntraced(function* (target, op) {
|
|
1586
|
-
const resolved = yield* resolveTarget(target, op);
|
|
1587
|
-
if (resolved.node.kind !== "directory")
|
|
1588
|
-
return yield* resolved.op.fail("NotDirectory");
|
|
1589
|
-
return { node: resolved.node, op: resolved.op };
|
|
1590
|
-
});
|
|
1591
|
-
// The directory an entry names a child of. A removed directory that a handle still holds takes no new
|
|
1592
|
-
// children, as on Linux, where creating through a descriptor of a removed directory fails with ENOENT.
|
|
1593
|
-
const entryDirectory = Effect.fnUntraced(function* (target, op) {
|
|
1594
|
-
const directory = yield* resolveDirectory(target, op);
|
|
1595
|
-
if (directory.node.metadata.nlink === 0)
|
|
1596
|
-
return yield* directory.op.fail("NotFound");
|
|
1597
|
-
return directory;
|
|
1598
|
-
});
|
|
1599
1373
|
const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
|
|
1600
|
-
const entryName = (input, op) => {
|
|
1601
|
-
if (Predicate.isString(input) && !isWellFormed(input))
|
|
1602
|
-
return Result.fail(op.fail("InvalidPathEncoding"));
|
|
1603
|
-
const bytes = Predicate.isString(input) ? encoder.encode(input) : input;
|
|
1604
|
-
if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
|
|
1605
|
-
bytes.includes(SLASH_BYTE))
|
|
1606
|
-
return Result.fail(op.fail("InvalidArgument"));
|
|
1607
|
-
return Result.succeed(Hex.encode(new Uint8Array(bytes)));
|
|
1608
|
-
};
|
|
1609
|
-
const prepareEntry = (input, op) => isEntry(input)
|
|
1610
|
-
? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
|
|
1611
|
-
: isTarget(input)
|
|
1612
|
-
? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
|
|
1613
|
-
: Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
|
|
1614
|
-
const resolveEntry = Effect.fnUntraced(function* (prepared, op) {
|
|
1615
|
-
if (prepared.kind === "path") {
|
|
1616
|
-
return yield* ResolvedEntry.fromPath(prepared.path, prepared.base, op);
|
|
1617
|
-
}
|
|
1618
|
-
const directory = yield* entryDirectory(prepared.directory, op);
|
|
1619
|
-
return {
|
|
1620
|
-
parent: directory.node.ino,
|
|
1621
|
-
name: prepared.name,
|
|
1622
|
-
trailingSlash: false,
|
|
1623
|
-
addressing: "entry",
|
|
1624
|
-
op
|
|
1625
|
-
};
|
|
1626
|
-
});
|
|
1627
1374
|
const asTarget = (input) => Target.of(input);
|
|
1628
1375
|
const acquireDirectory = Effect.fnUntraced(function* (input, op) {
|
|
1629
1376
|
const target = asTarget(input);
|
|
@@ -1631,8 +1378,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1631
1378
|
return yield* acquireHandle(acquired, (acquire) => coordinatedRead(op, Effect.uninterruptible(acquire)), Effect.gen(function* () {
|
|
1632
1379
|
const directory = yield* resolveDirectory(target, op);
|
|
1633
1380
|
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
1381
|
+
assignAuthority(acquired, target);
|
|
1382
|
+
const candidates = acquired.boundaries ?? [];
|
|
1383
|
+
if (candidates.length === 1)
|
|
1384
|
+
acquired.namespaceRoots = candidates[0];
|
|
1385
|
+
let current = directory.node;
|
|
1386
|
+
while (candidates.length > 1) {
|
|
1387
|
+
const nearest = candidates.find((chain) => chain.at(-1) === current.ino);
|
|
1388
|
+
if (nearest !== undefined) {
|
|
1389
|
+
acquired.namespaceRoots = nearest;
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
if (current.ino === ROOT_INO)
|
|
1393
|
+
break;
|
|
1394
|
+
current = yield* directoryNow(current.parent);
|
|
1395
|
+
}
|
|
1634
1396
|
holdDirectory(directory.node.ino);
|
|
1635
|
-
acquired
|
|
1397
|
+
TokenRegistry.publish(acquired, directory.node.ino);
|
|
1636
1398
|
return acquired;
|
|
1637
1399
|
}),
|
|
1638
1400
|
// Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
|
|
@@ -1655,31 +1417,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1655
1417
|
const follow = target.followFinalSymlink ?? true;
|
|
1656
1418
|
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1657
1419
|
const path = yield* Effect.fromResult(prepared);
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
const resolved = yield* lookup(path, target.relativeTo, op, {
|
|
1666
|
-
followFinalSymlink: follow,
|
|
1667
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1668
|
-
});
|
|
1669
|
-
const parent = resolved.parent;
|
|
1670
|
-
if (parent === undefined)
|
|
1671
|
-
return yield* pathOp.fail("IsDirectory");
|
|
1672
|
-
const name = resolved.name;
|
|
1673
|
-
if (isDotComponent(name))
|
|
1674
|
-
return yield* pathOp.fail("IsDirectory");
|
|
1675
|
-
if (resolved.node === undefined && path.trailingSlash)
|
|
1420
|
+
const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
|
|
1421
|
+
kind: "OrCreate",
|
|
1422
|
+
create: chosen.create ?? "never",
|
|
1423
|
+
finalSymlink: follow ? "follow" : "preserve"
|
|
1424
|
+
}, op);
|
|
1425
|
+
const { parent, name } = resolved;
|
|
1426
|
+
if (resolved.kind === "missing" && path.trailingSlash)
|
|
1676
1427
|
return yield* pathOp.fail("IsDirectory");
|
|
1677
1428
|
yield* authorize(parent, identity, EXECUTE, pathOp);
|
|
1678
|
-
const file = resolved.node;
|
|
1679
|
-
if (file !== undefined && chosen.create === "exclusive")
|
|
1680
|
-
return yield* pathOp.fail("AlreadyExists");
|
|
1429
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1681
1430
|
const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
|
|
1682
1431
|
opened(result.ino);
|
|
1432
|
+
assignAuthority(acquired, target);
|
|
1683
1433
|
return fileHandle(acquired);
|
|
1684
1434
|
}));
|
|
1685
1435
|
});
|
|
@@ -1703,6 +1453,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1703
1453
|
yield* draft.retain(node.ino);
|
|
1704
1454
|
yield* bindFile(acquired, node.ino);
|
|
1705
1455
|
opened(node.ino);
|
|
1456
|
+
assignAuthority(acquired, target);
|
|
1706
1457
|
return fileHandle(acquired);
|
|
1707
1458
|
}));
|
|
1708
1459
|
});
|
|
@@ -1719,64 +1470,51 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1719
1470
|
}
|
|
1720
1471
|
if (chosen.exactMode && chosen.mode === undefined)
|
|
1721
1472
|
return yield* op.fail("InvalidArgument");
|
|
1722
|
-
const relativePath = preparePath(ownedPath(nameBytes(name)), op.operation, undefined);
|
|
1723
1473
|
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
1724
1474
|
const expected = chosen.expected;
|
|
1725
1475
|
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1476
|
+
const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
|
|
1477
|
+
kind: "OrCreate",
|
|
1478
|
+
create: chosen.create ?? "never",
|
|
1479
|
+
finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
|
|
1480
|
+
beforeFollow: Effect.fnUntraced(function* (_, direct) {
|
|
1481
|
+
if (expected !== undefined) {
|
|
1482
|
+
let expectedIno;
|
|
1483
|
+
if (expected !== null) {
|
|
1484
|
+
const observed = yield* Effect.result(referencedNode(expected, op));
|
|
1485
|
+
if (Result.isFailure(observed)) {
|
|
1486
|
+
return yield* op.fail("VolumeBusy", { cause: observed.failure });
|
|
1487
|
+
}
|
|
1488
|
+
expectedIno = observed.success.ino;
|
|
1489
|
+
}
|
|
1490
|
+
if (direct?.ino !== expectedIno)
|
|
1491
|
+
return yield* op.fail("VolumeBusy");
|
|
1738
1492
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
return yield* op.fail("VolumeBusy");
|
|
1743
|
-
}
|
|
1744
|
-
if (chosen.expectedChild === null) {
|
|
1745
|
-
if (direct !== undefined)
|
|
1746
|
-
return yield* op.fail("StaleReference");
|
|
1747
|
-
}
|
|
1748
|
-
else if (chosen.expectedChild !== undefined) {
|
|
1749
|
-
const expectedChild = chosen.expectedChild;
|
|
1750
|
-
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
1751
|
-
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
1752
|
-
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
1753
|
-
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
1754
|
-
return yield* op.fail("StaleReference");
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
if (direct !== undefined && chosen.create === "exclusive")
|
|
1758
|
-
return yield* op.fail("AlreadyExists");
|
|
1759
|
-
let file = direct;
|
|
1760
|
-
let mutationParent = parent.ino;
|
|
1761
|
-
let mutationName = name;
|
|
1762
|
-
if (file?.kind === "symlink" && chosen.followFinalSymlink !== false) {
|
|
1763
|
-
const resolved = yield* lookup(yield* Effect.fromResult(relativePath), undefined, op, {
|
|
1764
|
-
followFinalSymlink: true,
|
|
1765
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1766
|
-
}, parent);
|
|
1767
|
-
file = resolved.node;
|
|
1768
|
-
if (file === undefined) {
|
|
1769
|
-
if (resolved.parent === undefined || resolved.name === undefined) {
|
|
1770
|
-
return yield* op.fail("IsDirectory");
|
|
1493
|
+
if (chosen.expectedChild === null) {
|
|
1494
|
+
if (direct !== undefined)
|
|
1495
|
+
return yield* op.fail("StaleReference");
|
|
1771
1496
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1497
|
+
else if (chosen.expectedChild !== undefined) {
|
|
1498
|
+
const expectedChild = chosen.expectedChild;
|
|
1499
|
+
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
1500
|
+
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
1501
|
+
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
1502
|
+
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
1503
|
+
return yield* op.fail("StaleReference");
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
})
|
|
1507
|
+
}, op);
|
|
1508
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1509
|
+
const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
|
|
1510
|
+
const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
|
|
1776
1511
|
const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
|
|
1777
1512
|
opened(result.ino);
|
|
1778
1513
|
return {
|
|
1779
|
-
handle:
|
|
1514
|
+
handle: (() => {
|
|
1515
|
+
assignAuthority(acquired, entry.directory);
|
|
1516
|
+
return fileHandle(acquired);
|
|
1517
|
+
})(),
|
|
1780
1518
|
reference: referenceFor(result.ino),
|
|
1781
1519
|
created: result.created,
|
|
1782
1520
|
directory: result.directory
|
|
@@ -1828,7 +1566,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1828
1566
|
const prepared = prepare(target.path, op);
|
|
1829
1567
|
if (Result.isFailure(prepared))
|
|
1830
1568
|
return Effect.undefined;
|
|
1831
|
-
return
|
|
1569
|
+
return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
|
|
1832
1570
|
return entry.name !== undefined && parent?.kind === "directory" &&
|
|
1833
1571
|
parent.entries.get(entry.name) === root.ino
|
|
1834
1572
|
? anchorFrame(parent, entry.name, root)
|
|
@@ -1843,10 +1581,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1843
1581
|
// through a reference or a handle is held by the object it resolved to, as a descriptor holds it.
|
|
1844
1582
|
// It steps down the chain in a loop, so a tree of any depth reaches its frames without deepening the stack.
|
|
1845
1583
|
const reachFrame = (frame, at) => Effect.gen(function* () {
|
|
1584
|
+
if (boundary !== undefined)
|
|
1585
|
+
yield* boundary.alive(at);
|
|
1846
1586
|
const chain = [];
|
|
1847
1587
|
for (let link = frame; link !== undefined; link = link.up)
|
|
1848
1588
|
chain.push(link);
|
|
1849
1589
|
let node = yield* view((chain.at(-1) ?? frame).parent);
|
|
1590
|
+
if (node !== undefined)
|
|
1591
|
+
yield* checkAuthority(node, at);
|
|
1850
1592
|
for (let index = chain.length - 1; index >= 0; index--) {
|
|
1851
1593
|
const link = chain[index];
|
|
1852
1594
|
if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
@@ -1868,27 +1610,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1868
1610
|
let bytes = 0n;
|
|
1869
1611
|
let failure;
|
|
1870
1612
|
const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
|
|
1871
|
-
if (reference
|
|
1613
|
+
if (TokenRegistry.inode(reference) === undefined)
|
|
1872
1614
|
return Effect.fail(op.fail("ClosedCaller"));
|
|
1873
1615
|
const at = op.at(locate(frame.path));
|
|
1874
1616
|
return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
|
|
1875
1617
|
? Effect.succeed([])
|
|
1876
1618
|
: Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
|
|
1877
1619
|
}));
|
|
1878
|
-
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1879
1620
|
const admit = (frame) => {
|
|
1880
|
-
|
|
1881
|
-
return exceeded(frame, "maxDepth");
|
|
1882
|
-
if (plan.maxEntries !== undefined && ++entries > plan.maxEntries)
|
|
1883
|
-
return exceeded(frame, "maxEntries");
|
|
1621
|
+
entries++;
|
|
1884
1622
|
bytes += frame.bytes;
|
|
1885
|
-
|
|
1886
|
-
return exceeded(frame, "maxBytes");
|
|
1887
|
-
return undefined;
|
|
1623
|
+
return plan.limit(frame, { entries, bytes }, "entry");
|
|
1888
1624
|
};
|
|
1889
1625
|
const step = Effect.gen(function* () {
|
|
1626
|
+
// Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
|
|
1627
|
+
// oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
|
|
1890
1628
|
if (failure !== undefined)
|
|
1891
|
-
return yield* failure;
|
|
1629
|
+
return yield* Effect.fail(failure);
|
|
1892
1630
|
if (pending === undefined)
|
|
1893
1631
|
pending = (yield* first).reverse();
|
|
1894
1632
|
const out = [];
|
|
@@ -1910,9 +1648,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1910
1648
|
out.push(frame);
|
|
1911
1649
|
}
|
|
1912
1650
|
if (unlisted) {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1651
|
+
const rejected = plan.limit(frame, { entries, bytes }, "directory");
|
|
1652
|
+
if (rejected !== undefined)
|
|
1653
|
+
return yield* stop(rejected);
|
|
1916
1654
|
const listed = yield* Effect.result(list(frame));
|
|
1917
1655
|
if (Result.isFailure(listed))
|
|
1918
1656
|
return yield* stop(listed.failure);
|
|
@@ -1937,13 +1675,15 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1937
1675
|
// failure, which names the entry's path under the one the caller gave. Removing an empty directory reveals nothing, so only a directory with entries must be readable, and
|
|
1938
1676
|
// no permission is changed to make one so. With `force`, the entry itself going missing leaves nothing to
|
|
1939
1677
|
// empty.
|
|
1940
|
-
const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force) {
|
|
1678
|
+
const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force, guard) {
|
|
1941
1679
|
const prefix = prepared.kind === "path" ? prepared.path.bytes : nameBytes(prepared.name);
|
|
1942
1680
|
const locate = (path) => ownedPath(joinPath(prefix, path));
|
|
1943
|
-
const
|
|
1681
|
+
const targetGuard = guard.pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.void));
|
|
1682
|
+
const listable = (directory, at) => Effect.andThen(targetGuard, directory.entries.size === 0 ? Effect.void : authorize(directory, identity, READ, at));
|
|
1944
1683
|
let anchor;
|
|
1945
1684
|
let targetAt = preparedOp(prepared, op);
|
|
1946
1685
|
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
1686
|
+
yield* guard;
|
|
1947
1687
|
const entry = yield* resolveEntry(prepared, op);
|
|
1948
1688
|
const parent = yield* directoryNow(entry.parent);
|
|
1949
1689
|
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
@@ -1962,6 +1702,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1962
1702
|
const removeFrame = (frame) => {
|
|
1963
1703
|
const at = op.at(locate(frame.path));
|
|
1964
1704
|
return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
|
|
1705
|
+
yield* targetGuard;
|
|
1965
1706
|
if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
|
|
1966
1707
|
return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
|
|
1967
1708
|
}
|
|
@@ -1976,7 +1717,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1976
1717
|
}, op));
|
|
1977
1718
|
})));
|
|
1978
1719
|
};
|
|
1979
|
-
const plan = { order: "post",
|
|
1720
|
+
const plan = { order: "post", limit: () => undefined };
|
|
1980
1721
|
return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
|
|
1981
1722
|
});
|
|
1982
1723
|
const rootOp = OpContext.make("root");
|
|
@@ -1985,12 +1726,99 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1985
1726
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1986
1727
|
return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
|
|
1987
1728
|
});
|
|
1729
|
+
const openDispatch = Effect.fnUntraced(function* (input, options) {
|
|
1730
|
+
const op = OpContext.make("open");
|
|
1731
|
+
if (isEntry(input))
|
|
1732
|
+
return yield* openEntry(input, options, op);
|
|
1733
|
+
const target = asTarget(input);
|
|
1734
|
+
if (Target.$is("Path")(target))
|
|
1735
|
+
return yield* openPath(target, options, op);
|
|
1736
|
+
return yield* openNode(target, options, op);
|
|
1737
|
+
});
|
|
1738
|
+
function open(input, options) {
|
|
1739
|
+
return openDispatch(input, options);
|
|
1740
|
+
}
|
|
1741
|
+
const removeDispatch = Effect.fnUntraced(function* (input, options) {
|
|
1742
|
+
const op = OpContext.make("remove");
|
|
1743
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1744
|
+
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
1745
|
+
const imported = prepared.kind === "entry"
|
|
1746
|
+
? inheritedFor(prepared.directory)
|
|
1747
|
+
: prepared.base === undefined
|
|
1748
|
+
? []
|
|
1749
|
+
: registry.handleBoundaries(prepared.base);
|
|
1750
|
+
const guard = chosen.expected === undefined && imported.length === 0 ? Effect.void : Effect.gen(function* () {
|
|
1751
|
+
const entry = yield* resolveEntry(prepared, op);
|
|
1752
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1753
|
+
if (protectsRoot(parent, entry.name))
|
|
1754
|
+
return yield* entry.op.fail("NotPermitted");
|
|
1755
|
+
if (chosen.expected === undefined)
|
|
1756
|
+
return;
|
|
1757
|
+
const expected = yield* registry.referenceIdentity(chosen.expected, op);
|
|
1758
|
+
const expectedNode = yield* view(expected);
|
|
1759
|
+
if (expectedNode !== undefined && expectedNode.metadata.nlink > 0)
|
|
1760
|
+
yield* checkAuthority(expectedNode, op);
|
|
1761
|
+
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
1762
|
+
const found = entry.name === undefined ? parent.ino : parent.entries.get(entry.name);
|
|
1763
|
+
if (found === undefined)
|
|
1764
|
+
return yield* entry.op.fail("NotFound");
|
|
1765
|
+
if (found !== expected)
|
|
1766
|
+
return yield* entry.op.fail("VolumeBusy", { field: "expected" });
|
|
1767
|
+
});
|
|
1768
|
+
const once = coordinated(op, Effect.andThen(guard, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op))));
|
|
1769
|
+
const target = (effect) => chosen.force === true
|
|
1770
|
+
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
1771
|
+
: effect;
|
|
1772
|
+
const removed = yield* Effect.result(target(once));
|
|
1773
|
+
if (Result.isSuccess(removed))
|
|
1774
|
+
return removed.success;
|
|
1775
|
+
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
1776
|
+
return yield* removed.failure;
|
|
1777
|
+
yield* emptyDirectory(prepared, op, chosen.force === true, guard);
|
|
1778
|
+
return yield* target(once);
|
|
1779
|
+
});
|
|
1780
|
+
function remove(input, options) {
|
|
1781
|
+
return removeDispatch(input, options);
|
|
1782
|
+
}
|
|
1988
1783
|
const caller = Object.freeze({
|
|
1989
1784
|
[CallerId]: true,
|
|
1990
|
-
|
|
1785
|
+
limits,
|
|
1786
|
+
watch: Effect.fnUntraced(function* (input, options) {
|
|
1787
|
+
const op = OpContext.make("watch");
|
|
1788
|
+
const decoded = yield* Effect.fromResult(decodeConfiguration(CallerWatchOptions, options ?? {}, "watch"));
|
|
1789
|
+
const target = asTarget(input);
|
|
1790
|
+
const seams = yield* VolumeTestSeams;
|
|
1791
|
+
const select = Effect.gen(function* () {
|
|
1792
|
+
yield* checkAvailable("watch");
|
|
1793
|
+
const resolved = yield* resolveTarget(target, op);
|
|
1794
|
+
yield* authorize(resolved.node, identity, READ, resolved.op);
|
|
1795
|
+
if (resolved.node.metadata.nlink === 0)
|
|
1796
|
+
return yield* resolved.op.fail("NotFound");
|
|
1797
|
+
if (decoded.alias === "resolved" && resolved.kind === "node" && resolved.node.kind !== "directory") {
|
|
1798
|
+
return yield* op.fail("InvalidArgument");
|
|
1799
|
+
}
|
|
1800
|
+
const selection = {
|
|
1801
|
+
roots,
|
|
1802
|
+
additionalRoots: [...inherited, ...inheritedFor(target)],
|
|
1803
|
+
target: resolved.node.ino,
|
|
1804
|
+
recursive: decoded.recursive ?? true
|
|
1805
|
+
};
|
|
1806
|
+
return CallerWatch.make(decoded.alias === "resolved" && resolved.kind === "entry"
|
|
1807
|
+
? { ...selection, selected: { parent: resolved.parent.ino, name: resolved.name } }
|
|
1808
|
+
: selection);
|
|
1809
|
+
});
|
|
1810
|
+
return Stream.map(yield* admit(op, watchHub.subscribe(select, seams.afterSubscribe)), (event) => event.change);
|
|
1811
|
+
}),
|
|
1812
|
+
root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
|
|
1991
1813
|
? Effect.fail(rootOp.fail("ClosedCaller"))
|
|
1992
|
-
: Effect.
|
|
1993
|
-
|
|
1814
|
+
: Effect.gen(function* () {
|
|
1815
|
+
const node = yield* view(boundary?.root ?? ROOT_INO);
|
|
1816
|
+
if (node === undefined)
|
|
1817
|
+
return yield* rootOp.fail("ClosedCaller");
|
|
1818
|
+
yield* checkAuthority(node, rootOp);
|
|
1819
|
+
return referenceFor(node.ino);
|
|
1820
|
+
}))),
|
|
1821
|
+
lookup: Effect.fnUntraced(function* (input) {
|
|
1994
1822
|
const op = OpContext.make("lookup");
|
|
1995
1823
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1996
1824
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
@@ -2006,21 +1834,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2006
1834
|
return referenceFor(child);
|
|
2007
1835
|
}));
|
|
2008
1836
|
}),
|
|
2009
|
-
parent: Effect.
|
|
1837
|
+
parent: Effect.fnUntraced(function* (input) {
|
|
2010
1838
|
const op = OpContext.make("parent");
|
|
2011
1839
|
const target = asTarget(input);
|
|
2012
1840
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2013
1841
|
const directory = yield* resolveDirectory(target, op);
|
|
2014
1842
|
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
2015
|
-
return referenceFor(directory.node.
|
|
1843
|
+
return referenceFor(directory.node.ino === boundary?.root ||
|
|
1844
|
+
inheritedFor(target).some((chain) => chain.at(-1) === directory.node.ino)
|
|
1845
|
+
? directory.node.ino
|
|
1846
|
+
: directory.node.parent);
|
|
2016
1847
|
}));
|
|
2017
1848
|
}),
|
|
2018
|
-
stat: Effect.
|
|
1849
|
+
stat: Effect.fnUntraced(function* (input) {
|
|
2019
1850
|
const op = OpContext.make("stat");
|
|
2020
1851
|
const target = asTarget(input);
|
|
2021
1852
|
return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => withMetadata(resolved.node)));
|
|
2022
1853
|
}),
|
|
2023
|
-
readDirectory: Effect.
|
|
1854
|
+
readDirectory: Effect.fnUntraced(function* (input) {
|
|
2024
1855
|
const op = OpContext.make("readDirectory");
|
|
2025
1856
|
const target = asTarget(input);
|
|
2026
1857
|
return yield* accessing(op, Effect.gen(function* () {
|
|
@@ -2036,7 +1867,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2036
1867
|
const target = asTarget(input);
|
|
2037
1868
|
const prefix = Target.$is("Path")(target) ? Result.getOrUndefined(inputBytes(target.path)) : undefined;
|
|
2038
1869
|
const locate = (path) => ownedPath(prefix === undefined ? path : joinPath(prefix, path));
|
|
2039
|
-
const
|
|
1870
|
+
const authorities = inheritedFor(target).map((chain) => Confinement.make(authorityView, chain));
|
|
1871
|
+
const readable = Effect.fnUntraced(function* (directory, at) {
|
|
1872
|
+
for (const authority of authorities)
|
|
1873
|
+
yield* authority.check(directory, at, true);
|
|
1874
|
+
yield* authorize(directory, identity, READ, at);
|
|
1875
|
+
});
|
|
2040
1876
|
return Stream.unwrap(Effect.gen(function* () {
|
|
2041
1877
|
const chosen = yield* decodeWalkOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(Target.$is("Path")(target) ? op.at(target.path) : op, cause)));
|
|
2042
1878
|
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
@@ -2044,11 +1880,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2044
1880
|
yield* authorize(directory.node, identity, READ, directory.op);
|
|
2045
1881
|
return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
2046
1882
|
}));
|
|
1883
|
+
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1884
|
+
const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
|
|
2047
1885
|
const plan = {
|
|
2048
1886
|
order: chosen.order ?? "pre",
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
1887
|
+
limit: (frame, progress, phase) => {
|
|
1888
|
+
if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
|
|
1889
|
+
return exceeded(frame, "maxDepth");
|
|
1890
|
+
if (phase === "directory")
|
|
1891
|
+
return undefined;
|
|
1892
|
+
if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
|
|
1893
|
+
return exceeded(frame, "maxEntries");
|
|
1894
|
+
}
|
|
1895
|
+
return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
|
|
1896
|
+
}
|
|
2052
1897
|
};
|
|
2053
1898
|
return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
|
|
2054
1899
|
path: ownedPath(frame.path),
|
|
@@ -2058,9 +1903,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2058
1903
|
kind: frame.kind,
|
|
2059
1904
|
depth: frame.depth
|
|
2060
1905
|
}));
|
|
2061
|
-
}))
|
|
1906
|
+
}));
|
|
2062
1907
|
},
|
|
2063
|
-
readLink: Effect.
|
|
1908
|
+
readLink: Effect.fnUntraced(function* (input) {
|
|
2064
1909
|
const op = OpContext.make("readLink");
|
|
2065
1910
|
const target = asTarget(input);
|
|
2066
1911
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
@@ -2070,29 +1915,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2070
1915
|
return new Uint8Array(resolved.node.target);
|
|
2071
1916
|
}));
|
|
2072
1917
|
}),
|
|
2073
|
-
realPath: Effect.
|
|
1918
|
+
realPath: Effect.fnUntraced(function* (input) {
|
|
2074
1919
|
const op = OpContext.make("realPath");
|
|
2075
1920
|
const target = asTarget(input);
|
|
2076
1921
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2077
1922
|
if (Target.$is("Path")(target)) {
|
|
2078
1923
|
const pathOp = op.at(target.path);
|
|
2079
|
-
const result = yield*
|
|
2080
|
-
const directory = result.node
|
|
2081
|
-
|
|
1924
|
+
const result = yield* resolveTarget(target, op, { final: true });
|
|
1925
|
+
const directory = result.node.kind === "directory"
|
|
1926
|
+
? result.node
|
|
1927
|
+
: result.kind === "entry"
|
|
1928
|
+
? result.parent
|
|
1929
|
+
: undefined;
|
|
1930
|
+
const prefix = directory === undefined ? SLASH_HEX : yield* callerPath(directory.ino);
|
|
2082
1931
|
if (prefix === undefined)
|
|
2083
1932
|
return yield* pathOp.fail("NotFound");
|
|
2084
|
-
if (result.node
|
|
1933
|
+
if (result.node.kind === "directory" || result.kind === "node")
|
|
2085
1934
|
return ownedPath(nameBytes(prefix));
|
|
2086
1935
|
return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
|
|
2087
1936
|
}
|
|
2088
1937
|
const resolved = yield* resolveTarget(target, op);
|
|
2089
|
-
const path = yield*
|
|
1938
|
+
const path = yield* callerPath(resolved.node.ino);
|
|
2090
1939
|
if (path === undefined)
|
|
2091
1940
|
return yield* resolved.op.fail("NotFound");
|
|
2092
1941
|
return ownedPath(nameBytes(path));
|
|
2093
1942
|
}));
|
|
2094
1943
|
}),
|
|
2095
|
-
access: Effect.
|
|
1944
|
+
access: Effect.fnUntraced(function* (input, bits = 0) {
|
|
2096
1945
|
const op = OpContext.make("access");
|
|
2097
1946
|
const target = asTarget(input);
|
|
2098
1947
|
if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
|
|
@@ -2112,7 +1961,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2112
1961
|
return granted;
|
|
2113
1962
|
}));
|
|
2114
1963
|
}),
|
|
2115
|
-
readFile: Effect.
|
|
1964
|
+
readFile: Effect.fnUntraced(function* (input) {
|
|
2116
1965
|
const op = OpContext.make("readFile");
|
|
2117
1966
|
const target = asTarget(input);
|
|
2118
1967
|
return yield* accessing(op, Effect.gen(function* () {
|
|
@@ -2127,7 +1976,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2127
1976
|
return { value: data, access: { node, now: yield* timestamp(op) } };
|
|
2128
1977
|
}));
|
|
2129
1978
|
}),
|
|
2130
|
-
writeFile: Effect.
|
|
1979
|
+
writeFile: Effect.fnUntraced(function* (input, bytes, options) {
|
|
2131
1980
|
const op = OpContext.make("writeFile");
|
|
2132
1981
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
2133
1982
|
const optionsOp = preparedOp(prepared, op);
|
|
@@ -2136,66 +1985,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2136
1985
|
const captured = new Uint8Array(bytes);
|
|
2137
1986
|
const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
2138
1987
|
return yield* coordinated(op, Effect.gen(function* () {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
return yield* entryOp.fail("AlreadyExists");
|
|
2151
|
-
if (exists.failure.code !== "NotFound")
|
|
2152
|
-
return yield* exists.failure;
|
|
2153
|
-
}
|
|
2154
|
-
const resolved = yield* lookup(path, prepared.base, op, {
|
|
2155
|
-
followFinalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false,
|
|
2156
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
2157
|
-
});
|
|
2158
|
-
if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
|
|
2159
|
-
return yield* entryOp.fail("IsDirectory");
|
|
2160
|
-
}
|
|
2161
|
-
parent = resolved.parent;
|
|
2162
|
-
name = resolved.name;
|
|
2163
|
-
found = resolved.node;
|
|
2164
|
-
trailingSlash = path.trailingSlash;
|
|
2165
|
-
}
|
|
2166
|
-
else {
|
|
2167
|
-
const directory = yield* entryDirectory(prepared.directory, op);
|
|
2168
|
-
if (isDotComponent(prepared.name))
|
|
2169
|
-
return yield* op.fail("InvalidArgument");
|
|
2170
|
-
yield* authorize(directory.node, identity, EXECUTE, op);
|
|
2171
|
-
parent = directory.node;
|
|
2172
|
-
name = prepared.name;
|
|
2173
|
-
const childIno = parent.entries.get(name);
|
|
2174
|
-
let child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
2175
|
-
if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
|
|
2176
|
-
chosen.followFinalSymlink !== false) {
|
|
2177
|
-
const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
|
|
2178
|
-
followFinalSymlink: true,
|
|
2179
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
2180
|
-
}, parent);
|
|
2181
|
-
if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
|
|
2182
|
-
return yield* op.fail("IsDirectory");
|
|
2183
|
-
}
|
|
2184
|
-
parent = resolved.parent;
|
|
2185
|
-
name = resolved.name;
|
|
2186
|
-
child = resolved.node;
|
|
2187
|
-
}
|
|
2188
|
-
if (child === undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
2189
|
-
return yield* op.fail("NotFound");
|
|
2190
|
-
}
|
|
2191
|
-
if (child !== undefined && chosen.create === "exclusive")
|
|
2192
|
-
return yield* op.fail("AlreadyExists");
|
|
2193
|
-
if (child?.kind === "directory")
|
|
2194
|
-
return yield* op.fail("IsDirectory");
|
|
2195
|
-
found = child;
|
|
2196
|
-
}
|
|
2197
|
-
if (found === undefined && trailingSlash)
|
|
1988
|
+
const resolved = yield* resolver.resolve(prepared, {
|
|
1989
|
+
kind: "OrCreate",
|
|
1990
|
+
create: chosen.create ?? "never",
|
|
1991
|
+
finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
|
|
1992
|
+
? "follow"
|
|
1993
|
+
: "preserve",
|
|
1994
|
+
entryExclusive: "afterSymlink"
|
|
1995
|
+
}, op);
|
|
1996
|
+
const { parent, name, trailingSlash, op: entryOp } = resolved;
|
|
1997
|
+
const found = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1998
|
+
if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
|
|
2198
1999
|
return yield* entryOp.fail("IsDirectory");
|
|
2000
|
+
}
|
|
2199
2001
|
const replaced = found?.kind === "symlink" ? found : undefined;
|
|
2200
2002
|
if (replaced !== undefined && !chosen.replaceFinalSymlink)
|
|
2201
2003
|
return yield* entryOp.fail("SymlinkLoop");
|
|
@@ -2260,18 +2062,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2260
2062
|
}
|
|
2261
2063
|
}));
|
|
2262
2064
|
}),
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
open: Effect.fn("Caller.open")(function* (input, options) {
|
|
2266
|
-
const op = OpContext.make("open");
|
|
2267
|
-
if (isEntry(input))
|
|
2268
|
-
return yield* openEntry(input, options, op);
|
|
2269
|
-
const target = asTarget(input);
|
|
2270
|
-
if (Target.$is("Path")(target))
|
|
2271
|
-
return yield* openPath(target, options, op);
|
|
2272
|
-
return yield* openNode(target, options, op);
|
|
2273
|
-
}),
|
|
2274
|
-
mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
|
|
2065
|
+
open,
|
|
2066
|
+
mkdir: Effect.fnUntraced(function* (input, options = {}) {
|
|
2275
2067
|
const op = OpContext.make("mkdir");
|
|
2276
2068
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
2277
2069
|
const optionsOp = preparedOp(prepared, op);
|
|
@@ -2287,7 +2079,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2287
2079
|
return { reference: referenceFor(child), directory };
|
|
2288
2080
|
}));
|
|
2289
2081
|
}),
|
|
2290
|
-
symlink: Effect.
|
|
2082
|
+
symlink: Effect.fnUntraced(function* (target, input, options = {}) {
|
|
2291
2083
|
const op = OpContext.make("symlink");
|
|
2292
2084
|
const targetOp = op.at(target);
|
|
2293
2085
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
@@ -2306,7 +2098,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2306
2098
|
return { reference: referenceFor(child), directory };
|
|
2307
2099
|
}));
|
|
2308
2100
|
}),
|
|
2309
|
-
link: Effect.
|
|
2101
|
+
link: Effect.fnUntraced(function* (sourceInput, input) {
|
|
2310
2102
|
const op = OpContext.make("link");
|
|
2311
2103
|
const source = asTarget(sourceInput);
|
|
2312
2104
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
@@ -2322,30 +2114,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2322
2114
|
return { reference: referenceFor(node.ino), directory };
|
|
2323
2115
|
}));
|
|
2324
2116
|
}),
|
|
2325
|
-
unlink: Effect.
|
|
2117
|
+
unlink: Effect.fnUntraced(function* (input) {
|
|
2326
2118
|
return yield* entryVerb("unlink", input, unlinkEntry);
|
|
2327
2119
|
}),
|
|
2328
|
-
rmdir: Effect.
|
|
2120
|
+
rmdir: Effect.fnUntraced(function* (input) {
|
|
2329
2121
|
return yield* entryVerb("rmdir", input, rmdirEntry);
|
|
2330
2122
|
}),
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
const op = OpContext.make("remove");
|
|
2334
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
2335
|
-
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
2336
|
-
const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
|
|
2337
|
-
const target = (effect) => chosen.force === true
|
|
2338
|
-
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
2339
|
-
: effect;
|
|
2340
|
-
const removed = yield* Effect.result(target(once));
|
|
2341
|
-
if (Result.isSuccess(removed))
|
|
2342
|
-
return removed.success;
|
|
2343
|
-
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
2344
|
-
return yield* removed.failure;
|
|
2345
|
-
yield* emptyDirectory(prepared, op, chosen.force === true);
|
|
2346
|
-
return yield* target(once);
|
|
2347
|
-
}),
|
|
2348
|
-
rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
|
|
2123
|
+
remove,
|
|
2124
|
+
rename: Effect.fnUntraced(function* (fromInput, toInput) {
|
|
2349
2125
|
const op = OpContext.make("rename");
|
|
2350
2126
|
const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
|
|
2351
2127
|
const to = yield* Effect.fromResult(prepareEntry(toInput, op));
|
|
@@ -2355,34 +2131,34 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2355
2131
|
return yield* renameEntry(source, destination, op);
|
|
2356
2132
|
}));
|
|
2357
2133
|
}),
|
|
2358
|
-
chmod: Effect.
|
|
2134
|
+
chmod: Effect.fnUntraced(function* (input, mode) {
|
|
2359
2135
|
const op = OpContext.make("chmod");
|
|
2360
2136
|
const target = asTarget(input);
|
|
2361
2137
|
if (!isMode(mode))
|
|
2362
2138
|
return yield* op.fail("InvalidArgument");
|
|
2363
2139
|
yield* changeAttributes(() => asResolvedNode(target, op), { mode }, op);
|
|
2364
2140
|
}),
|
|
2365
|
-
chown: Effect.
|
|
2141
|
+
chown: Effect.fnUntraced(function* (input, owner) {
|
|
2366
2142
|
const op = OpContext.make("chown");
|
|
2367
2143
|
const target = asTarget(input);
|
|
2368
2144
|
const decoded = yield* decodeOwnerUpdate(owner).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
2369
2145
|
yield* changeAttributes(() => asResolvedNode(target, op), { owner: decoded }, op);
|
|
2370
2146
|
}),
|
|
2371
|
-
utimes: Effect.
|
|
2147
|
+
utimes: Effect.fnUntraced(function* (input, times) {
|
|
2372
2148
|
const op = OpContext.make("utimes");
|
|
2373
2149
|
const target = asTarget(input);
|
|
2374
2150
|
const decoded = yield* decodeTimes(times).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
2375
2151
|
yield* changeAttributes(() => asResolvedNode(target, op), { times: decoded }, op);
|
|
2376
2152
|
}),
|
|
2377
2153
|
// A negative length fails before the target resolves, as truncate(2) rejects it before the lookup.
|
|
2378
|
-
truncate: Effect.
|
|
2154
|
+
truncate: Effect.fnUntraced(function* (input, length) {
|
|
2379
2155
|
const op = OpContext.make("truncate");
|
|
2380
2156
|
const target = asTarget(input);
|
|
2381
2157
|
if (!isLength(length))
|
|
2382
2158
|
return yield* op.fail("InvalidArgument");
|
|
2383
2159
|
yield* changeAttributes(() => asResolvedNode(target, op), { size: length }, op);
|
|
2384
2160
|
}),
|
|
2385
|
-
setattr: Effect.
|
|
2161
|
+
setattr: Effect.fnUntraced(function* (input, attributes) {
|
|
2386
2162
|
const op = OpContext.make("setattr");
|
|
2387
2163
|
const target = asTarget(input);
|
|
2388
2164
|
// A caller outside TypeScript can pass anything, so a non-object fails typed rather than as a defect;
|
|
@@ -2408,29 +2184,36 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2408
2184
|
: yield* decodeExpected(attributes.expected).pipe(Effect.mapError((cause) => fail(op, cause, "expected")));
|
|
2409
2185
|
yield* changeAttributes(() => asResolvedNode(target, op), { size: attributes.size, mode: attributes.mode, owner, times, expected }, op);
|
|
2410
2186
|
}),
|
|
2411
|
-
|
|
2187
|
+
withRoot: Effect.fnUntraced(function* (input) {
|
|
2188
|
+
const acquired = yield* acquireDirectory(input, OpContext.make("withRoot"));
|
|
2189
|
+
return createCaller(acquired, identity, umask, [...roots, TokenRegistry.inode(acquired)], acquired.boundaries ?? []);
|
|
2190
|
+
}),
|
|
2191
|
+
withDirectory: Effect.fnUntraced(function* (input) {
|
|
2412
2192
|
const acquired = yield* acquireDirectory(input, OpContext.make("withDirectory"));
|
|
2413
|
-
return createCaller(acquired, identity, umask);
|
|
2193
|
+
return createCaller(acquired, identity, umask, acquired.namespaceRoots ?? roots, acquired.boundaries ?? []);
|
|
2414
2194
|
}),
|
|
2415
|
-
openDirectory: Effect.
|
|
2195
|
+
openDirectory: Effect.fnUntraced(function* (input) {
|
|
2416
2196
|
const acquired = yield* acquireDirectory(input, OpContext.make("openDirectory"));
|
|
2417
2197
|
const statOp = OpContext.make("stat");
|
|
2418
2198
|
const handle = Object.freeze({
|
|
2419
2199
|
[DirectoryHandleId]: true,
|
|
2420
2200
|
stat: coordinatedRead(statOp, Effect.gen(function* () {
|
|
2421
|
-
const
|
|
2201
|
+
const ino = TokenRegistry.inode(acquired);
|
|
2202
|
+
const node = ino === undefined ? undefined : yield* view(ino);
|
|
2203
|
+
if (node !== undefined && acquired.authority !== undefined)
|
|
2204
|
+
yield* acquired.authority(node, statOp);
|
|
2422
2205
|
return yield* (node === undefined
|
|
2423
2206
|
? Effect.fail(statOp.fail("InvalidHandle"))
|
|
2424
2207
|
: Effect.succeed(withMetadata(node)));
|
|
2425
|
-
}))
|
|
2208
|
+
})),
|
|
2426
2209
|
close: coordinatedCleanup(Effect.suspend(() => {
|
|
2427
|
-
if (acquired
|
|
2210
|
+
if (TokenRegistry.inode(acquired) === undefined) {
|
|
2428
2211
|
return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
|
|
2429
2212
|
}
|
|
2430
2213
|
return releaseDirectory(acquired);
|
|
2431
|
-
})).pipe(Effect.ensuring(closeReleasedScope(acquired))
|
|
2214
|
+
})).pipe(Effect.ensuring(closeReleasedScope(acquired)))
|
|
2432
2215
|
});
|
|
2433
|
-
|
|
2216
|
+
registry.registerDirectory(handle, acquired);
|
|
2434
2217
|
return handle;
|
|
2435
2218
|
})
|
|
2436
2219
|
});
|
|
@@ -2442,9 +2225,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2442
2225
|
identity,
|
|
2443
2226
|
incarnation,
|
|
2444
2227
|
limits,
|
|
2445
|
-
usage: coordinatedRead(OpContext.make("usage"), Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries })))
|
|
2446
|
-
|
|
2447
|
-
watch: Effect.fn("Volume.watch")(function* (options) {
|
|
2228
|
+
usage: coordinatedRead(OpContext.make("usage"), Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries }))),
|
|
2229
|
+
watch: Effect.fnUntraced(function* (options) {
|
|
2448
2230
|
const op = OpContext.make("watch");
|
|
2449
2231
|
const decoded = yield* Effect.fromResult(decodeConfiguration(WatchOptions, options === undefined ? {} : options, "watch"));
|
|
2450
2232
|
const recursive = decoded.recursive ?? true;
|
|
@@ -2458,38 +2240,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2458
2240
|
const stream = yield* admit(op, watchHub.subscribe(select, seams.afterSubscribe));
|
|
2459
2241
|
return Stream.map(stream, (event) => event.change);
|
|
2460
2242
|
}),
|
|
2461
|
-
snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state)))
|
|
2462
|
-
referenceKey: Effect.
|
|
2243
|
+
snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))),
|
|
2244
|
+
referenceKey: Effect.fnUntraced(function* (reference) {
|
|
2463
2245
|
const op = OpContext.make("referenceKey");
|
|
2464
|
-
return yield* coordinatedRead(op, Effect.
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
if (known.volume !== volumeIdentity)
|
|
2469
|
-
return Effect.fail(op.fail("ForeignReference"));
|
|
2470
|
-
if (!addressable(known.ino))
|
|
2471
|
-
return Effect.fail(op.fail("StaleReference"));
|
|
2472
|
-
const ino = BigInt(known.ino);
|
|
2473
|
-
return Effect.succeed({ identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) });
|
|
2246
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2247
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
|
|
2248
|
+
const ino = BigInt(node.ino);
|
|
2249
|
+
return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
|
|
2474
2250
|
}));
|
|
2475
2251
|
}),
|
|
2476
|
-
resolveReferenceKey: Effect.
|
|
2252
|
+
resolveReferenceKey: Effect.fnUntraced(function* (key) {
|
|
2477
2253
|
const op = OpContext.make("resolveReferenceKey");
|
|
2478
|
-
return yield* coordinatedRead(op, Effect.
|
|
2254
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2479
2255
|
if (!Schema.is(ReferenceKey)(key))
|
|
2480
|
-
return
|
|
2256
|
+
return yield* op.fail("InvalidReference");
|
|
2481
2257
|
// Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
|
|
2482
2258
|
if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
|
|
2483
|
-
return
|
|
2259
|
+
return yield* op.fail("ForeignReference");
|
|
2484
2260
|
}
|
|
2485
2261
|
// Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
|
|
2486
2262
|
if (!sameTag(key.tag, keyTag(key.ino)))
|
|
2487
|
-
return
|
|
2263
|
+
return yield* op.fail("InvalidReference");
|
|
2488
2264
|
const ino = Ino(Number(key.ino));
|
|
2489
|
-
|
|
2265
|
+
yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
|
|
2266
|
+
return referenceFor(ino);
|
|
2490
2267
|
}));
|
|
2491
2268
|
}),
|
|
2492
|
-
caller: Effect.
|
|
2269
|
+
caller: Effect.fnUntraced(function* (options) {
|
|
2493
2270
|
const decoded = yield* Effect.fromResult(decodeConfiguration(RootCallerOptions, options === undefined ? {} : options, "caller"));
|
|
2494
2271
|
const chosen = decoded.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
|
|
2495
2272
|
const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
|
|
@@ -2500,47 +2277,37 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2500
2277
|
changes: coordinatedRead(OpContext.make("changes"), Effect.suspend(() => observeChanges(state))),
|
|
2501
2278
|
capture: coordinatedRead(OpContext.make("capture"), captureState())
|
|
2502
2279
|
});
|
|
2503
|
-
return Object.freeze({ volume, shutdown,
|
|
2280
|
+
return Object.freeze({ volume, shutdown, observe });
|
|
2504
2281
|
});
|
|
2505
2282
|
/** @internal */
|
|
2506
|
-
export const make = Effect.
|
|
2507
|
-
return (yield* makeVolume(VolumeSource.Empty(), options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
|
|
2283
|
+
export const make = Effect.fnUntraced(function* (options) {
|
|
2284
|
+
return (yield* makeVolume(VolumeSource.Empty(), memoryCommitProvider, options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
|
|
2508
2285
|
});
|
|
2509
2286
|
/** @internal */
|
|
2510
2287
|
export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2288
|
+
const decoded = yield* Effect.fromResult(Limits.configuration(options, "prepareEmptyImage"));
|
|
2289
|
+
const limits = decoded.limits;
|
|
2290
|
+
const identity = decoded.identity ?? VolumeIdentity.make(yield* randomHex128);
|
|
2291
|
+
const epoch = VolumeEpoch.make(yield* randomHex128);
|
|
2292
|
+
const keySecret = KeySecret.make(yield* randomHex128);
|
|
2293
|
+
const now = yield* Clock.currentTimeNanos;
|
|
2294
|
+
if (!isTimestamp(now))
|
|
2295
|
+
return yield* argumentFailure("prepareEmptyImage", "clock.currentTimeNanos");
|
|
2296
|
+
return yield* LiveImage.encode(emptyState(now), { identity, epoch, keySecret }, limits).pipe(Effect.mapError((error) => retargetFailure("prepareEmptyImage", error)));
|
|
2515
2297
|
});
|
|
2516
2298
|
/** @internal */
|
|
2517
|
-
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only") {
|
|
2299
|
+
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only", options) {
|
|
2518
2300
|
const restored = yield* LiveImage.decode(image, maxImageBytes);
|
|
2519
|
-
|
|
2520
|
-
const
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
maxPendingOperations: 64,
|
|
2528
|
-
maxWatchEvents: 256
|
|
2301
|
+
const requested = yield* Effect.fromResult(Limits.fromOptions(options, "openImage"));
|
|
2302
|
+
const limits = yield* Effect.fromResult(Limits.fromStored(restored.limits, requested));
|
|
2303
|
+
const provider = {
|
|
2304
|
+
mode: "durable",
|
|
2305
|
+
shutdown: Effect.void,
|
|
2306
|
+
commit: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => makeError({ code: "StorageRejected", operation: "commit", cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
|
|
2307
|
+
? Effect.fail(makeError({ code: "StorageRejected", operation: "commit" }))
|
|
2308
|
+
: commit(bytes)))
|
|
2529
2309
|
};
|
|
2530
|
-
const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }),
|
|
2531
|
-
prepare: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => commitOp.fail("StorageRejected", { cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
|
|
2532
|
-
? commitOp.fail("StorageRejected")
|
|
2533
|
-
: Effect.sync(() => {
|
|
2534
|
-
prepared = bytes;
|
|
2535
|
-
}))),
|
|
2536
|
-
commit: () => Effect.suspend(() => {
|
|
2537
|
-
const bytes = prepared;
|
|
2538
|
-
prepared = undefined;
|
|
2539
|
-
return bytes === undefined ? Effect.succeed("unknown") : commit(bytes);
|
|
2540
|
-
})
|
|
2541
|
-
}, undefined, durability);
|
|
2542
|
-
if (shutdown === undefined)
|
|
2543
|
-
return yield* imageFailure("openImage", "InvalidStructure", { field: "liveImage" });
|
|
2310
|
+
const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), provider, options, durability);
|
|
2544
2311
|
return Object.freeze({ volume, shutdown });
|
|
2545
2312
|
});
|
|
2546
2313
|
/** @internal */
|
|
@@ -2552,22 +2319,22 @@ const changeOptions = (options) => {
|
|
|
2552
2319
|
return Effect.fromResult(decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options, "changes"));
|
|
2553
2320
|
};
|
|
2554
2321
|
/** @internal */
|
|
2555
|
-
export const fromSnapshot = Effect.
|
|
2556
|
-
return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), options)).volume;
|
|
2322
|
+
export const fromSnapshot = Effect.fnUntraced(function* (snapshot, options) {
|
|
2323
|
+
return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), memoryCommitProvider, options)).volume;
|
|
2557
2324
|
}, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
|
|
2558
2325
|
/** @internal */
|
|
2559
|
-
export const makeOverlay = Effect.
|
|
2326
|
+
export const makeOverlay = Effect.fnUntraced(function* (base, options) {
|
|
2560
2327
|
const value = yield* Image.valueOf(base);
|
|
2561
|
-
const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), options), (error) => retargetFailure("makeOverlay", error));
|
|
2328
|
+
const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), memoryCommitProvider, options), (error) => retargetFailure("makeOverlay", error));
|
|
2562
2329
|
const baseObservation = yield* observeChanges(value);
|
|
2563
2330
|
const overlay = Object.freeze({
|
|
2564
2331
|
...made.volume,
|
|
2565
|
-
changes: Effect.
|
|
2332
|
+
changes: Effect.fnUntraced(function* (options) {
|
|
2566
2333
|
const selected = yield* changeOptions(options);
|
|
2567
2334
|
const current = yield* made.observe.changes;
|
|
2568
2335
|
return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
|
|
2569
2336
|
}),
|
|
2570
|
-
capture: Effect.
|
|
2337
|
+
capture: Effect.fnUntraced(function* (options) {
|
|
2571
2338
|
const selected = yield* changeOptions(options);
|
|
2572
2339
|
const current = yield* made.observe.capture;
|
|
2573
2340
|
return Object.freeze({
|