@effect-vfs/core 0.8.0 → 0.10.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 +9 -0
- package/dist/BytePath.d.ts +4 -1
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +6 -4
- package/dist/Caller.d.ts +107 -66
- package/dist/Caller.d.ts.map +1 -1
- package/dist/Caller.js +16 -86
- package/dist/LiveVolume.d.ts +75 -2
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +87 -3
- package/dist/Search.d.ts +32 -8
- package/dist/Search.d.ts.map +1 -1
- package/dist/Search.js +19 -10
- package/dist/SynchronousAdapter.d.ts +58 -0
- package/dist/SynchronousAdapter.d.ts.map +1 -0
- package/dist/SynchronousAdapter.js +116 -0
- package/dist/Target.d.ts +7 -91
- package/dist/Target.d.ts.map +1 -1
- package/dist/Target.js +7 -47
- package/dist/Testing.d.ts +5 -2
- package/dist/Testing.d.ts.map +1 -1
- package/dist/Testing.js +2 -1
- package/dist/VfsError.d.ts +4 -2
- package/dist/VfsError.d.ts.map +1 -1
- package/dist/VfsError.js +3 -1
- package/dist/VirtualFileSystem.d.ts +151 -44
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +31 -21
- package/dist/Volume.d.ts +6 -1
- package/dist/Volume.d.ts.map +1 -1
- package/dist/Volume.js +6 -2
- package/dist/Watch.d.ts +18 -0
- package/dist/Watch.d.ts.map +1 -1
- package/dist/Watch.js +12 -0
- package/dist/internal/adapterBinding.d.ts +13 -0
- package/dist/internal/adapterBinding.d.ts.map +1 -0
- package/dist/internal/adapterBinding.js +13 -0
- package/dist/internal/bytePath.d.ts +6 -1
- package/dist/internal/bytePath.d.ts.map +1 -1
- package/dist/internal/callerWatch.d.ts +36 -0
- package/dist/internal/callerWatch.d.ts.map +1 -0
- package/dist/internal/callerWatch.js +185 -0
- package/dist/internal/engine.d.ts +1305 -0
- package/dist/internal/engine.d.ts.map +1 -0
- package/dist/internal/engine.js +3458 -0
- package/dist/internal/errors.d.ts +1 -33
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +1 -45
- package/dist/internal/fixture.d.ts +2 -1
- package/dist/internal/fixture.d.ts.map +1 -1
- package/dist/internal/fixture.js +8 -4
- package/dist/internal/image.d.ts +4 -1
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +6 -3
- package/dist/internal/lines.d.ts.map +1 -1
- package/dist/internal/lines.js +3 -2
- package/dist/internal/liveImage.d.ts +4 -2
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +3 -1
- package/dist/internal/merkle.d.ts +7 -4
- package/dist/internal/merkle.d.ts.map +1 -1
- package/dist/internal/merkle.js +6 -4
- package/dist/internal/search.d.ts +3 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +4 -3
- package/dist/internal/snapshotDelta.d.ts +11 -7
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +15 -9
- package/dist/internal/snapshotMerge.d.ts +1 -1
- package/dist/internal/snapshotMerge.d.ts.map +1 -1
- package/dist/internal/snapshotMerge.js +5 -2
- package/dist/internal/tree.d.ts +4 -2
- package/dist/internal/tree.d.ts.map +1 -1
- package/dist/internal/tree.js +4 -2
- package/dist/internal/virtualFileSystem.d.ts +20 -49
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +85 -1927
- package/dist/internal/volumeLimits.d.ts +1 -0
- package/dist/internal/volumeLimits.d.ts.map +1 -1
- package/dist/internal/volumeLimits.js +3 -2
- package/dist/internal/volumeState.d.ts +10 -79
- package/dist/internal/volumeState.d.ts.map +1 -1
- package/dist/internal/volumeState.js +9 -79
- package/dist/internal/watchHub.d.ts +9 -5
- package/dist/internal/watchHub.d.ts.map +1 -1
- package/dist/internal/watchHub.js +35 -8
- package/package.json +16 -9
- package/dist/internal/inodeTable.d.ts +0 -20
- package/dist/internal/inodeTable.d.ts.map +0 -1
- package/dist/internal/inodeTable.js +0 -48
- package/dist/internal/path.d.ts +0 -56
- package/dist/internal/path.d.ts.map +0 -1
- package/dist/internal/path.js +0 -141
- package/dist/internal/resolution.d.ts +0 -135
- package/dist/internal/resolution.d.ts.map +0 -1
- package/dist/internal/resolution.js +0 -277
- package/dist/internal/testSeams.d.ts +0 -17
- package/dist/internal/testSeams.d.ts.map +0 -1
- package/dist/internal/testSeams.js +0 -20
- package/dist/internal/tokenRegistry.d.ts +0 -77
- package/dist/internal/tokenRegistry.d.ts.map +0 -1
- package/dist/internal/tokenRegistry.js +0 -128
- package/dist/internal/turnstile.d.ts +0 -8
- package/dist/internal/turnstile.d.ts.map +0 -1
- package/dist/internal/turnstile.js +0 -43
|
@@ -1,228 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Community volume assembly over the shared private filesystem engine.
|
|
3
|
+
* IDs, authenticated keys, snapshots, overlays, storage encoders and watch delivery stay here.
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
2
6
|
import * as ByteSize from "effect/ByteSize";
|
|
3
|
-
import * as Cause from "effect/Cause";
|
|
4
7
|
import * as Clock from "effect/Clock";
|
|
5
|
-
import * as Context from "effect/Context";
|
|
6
8
|
import * as Crypto from "effect/Crypto";
|
|
7
9
|
import * as Data from "effect/Data";
|
|
8
10
|
import * as Effect from "effect/Effect";
|
|
9
11
|
import * as Hex from "effect/encoding/Hex";
|
|
10
|
-
import * as Exit from "effect/Exit";
|
|
11
|
-
import * as Match from "effect/Match";
|
|
12
|
-
import * as MutableRef from "effect/MutableRef";
|
|
13
|
-
import * as Option from "effect/Option";
|
|
14
12
|
import * as Predicate from "effect/Predicate";
|
|
15
13
|
import * as Result from "effect/Result";
|
|
16
14
|
import * as Schema from "effect/Schema";
|
|
17
|
-
import * as Semaphore from "effect/Semaphore";
|
|
18
15
|
import * as Stream from "effect/Stream";
|
|
19
16
|
import { BytePath } from "../BytePath.js";
|
|
20
|
-
import { CallerId
|
|
21
|
-
import {
|
|
22
|
-
import { Mode, OwnerUpdate, Times } from "../Metadata.js";
|
|
23
|
-
import { isEntry, Target } from "../Target.js";
|
|
24
|
-
import { make as makeError } from "../VfsError.js";
|
|
17
|
+
import { CallerId } from "../Caller.js";
|
|
18
|
+
import { make as makeError, VfsError } from "../VfsError.js";
|
|
25
19
|
import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
|
|
26
|
-
import { WatchOptions } from "../Watch.js";
|
|
20
|
+
import { CallerWatchOptions, WatchOptions } from "../Watch.js";
|
|
21
|
+
import * as AdapterBinding from "./adapterBinding.js";
|
|
27
22
|
import { sameBytes } from "./bytes.js";
|
|
28
|
-
import
|
|
23
|
+
import * as CallerWatch from "./callerWatch.js";
|
|
24
|
+
import * as Engine from "./engine.js";
|
|
25
|
+
import { imageFailure, retargetFailure } from "./errors.js";
|
|
29
26
|
import { KeySecret, VolumeEpoch } from "./hex128.js";
|
|
30
27
|
import { hmacSha256, sameTag } from "./hmac.js";
|
|
31
28
|
import * as Image from "./image.js";
|
|
32
|
-
import * as InodeTable from "./inodeTable.js";
|
|
33
29
|
import * as LiveImage from "./liveImage.js";
|
|
34
30
|
import * as MetadataDomain from "./metadata.js";
|
|
35
31
|
import { compareOverlay } from "./overlayDiff.js";
|
|
36
|
-
import {
|
|
37
|
-
import * as Resolution from "./resolution.js";
|
|
38
|
-
import { VolumeTestSeams } from "./testSeams.js";
|
|
39
|
-
import * as TokenRegistry from "./tokenRegistry.js";
|
|
40
|
-
import { makeTurnstile } from "./turnstile.js";
|
|
32
|
+
import { Errors, Paths, State, VolumeTestSeams } from "./engine.js";
|
|
41
33
|
import * as Limits from "./volumeLimits.js";
|
|
42
|
-
import {
|
|
34
|
+
import { reachableValue, storedMetadata } from "./volumeState.js";
|
|
43
35
|
import * as WatchHub from "./watchHub.js";
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const ANY_EXECUTE = 0o111;
|
|
48
|
-
const SET_ID_BITS = 0o6000;
|
|
49
|
-
const STICKY_BIT = 0o1000;
|
|
50
|
-
const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
|
|
36
|
+
const { argumentFailure, decodeConfiguration, OpContext } = Errors;
|
|
37
|
+
const { joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE } = Paths;
|
|
38
|
+
const { emptyState, entryPath, getNode, Ino, pathOf, ROOT_INO, WALK_YIELD_INTERVAL } = State;
|
|
51
39
|
/** @internal */
|
|
52
|
-
export const memoryCommitProvider =
|
|
53
|
-
mode: "memory",
|
|
54
|
-
commit: () => Effect.succeed("committed"),
|
|
55
|
-
shutdown: Effect.void
|
|
56
|
-
});
|
|
57
|
-
// An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
|
|
58
|
-
const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
|
|
59
|
-
// TODO(#186): Preserve the cause of a provider defect or interruption.
|
|
60
|
-
if (Exit.isFailure(committed)) {
|
|
61
|
-
if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
|
|
62
|
-
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
63
|
-
}
|
|
64
|
-
const failure = Cause.findErrorOption(committed.cause);
|
|
65
|
-
return Option.isSome(failure)
|
|
66
|
-
? { available: !cleanup, failure: failure.value }
|
|
67
|
-
: { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
68
|
-
}
|
|
69
|
-
if (committed.value === "rejected") {
|
|
70
|
-
return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
|
|
71
|
-
}
|
|
72
|
-
if (committed.value === "unknown") {
|
|
73
|
-
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
74
|
-
}
|
|
75
|
-
return { available: true, failure: undefined };
|
|
76
|
-
});
|
|
77
|
-
const WALK_CHUNK_ENTRIES = 128;
|
|
78
|
-
const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
|
|
79
|
-
const isMode = Schema.is(Mode);
|
|
80
|
-
const isNatural = Schema.is(Schema.Natural);
|
|
81
|
-
const isLength = Schema.is(SetattrOptions.fields.size.schema);
|
|
82
|
-
const inGroup = Resolution.inGroup;
|
|
40
|
+
export const memoryCommitProvider = Engine.memoryCommitProvider;
|
|
83
41
|
const isTimestamp = Schema.is(MetadataDomain.Timestamp);
|
|
84
|
-
const isSeekMode = Schema.is(SeekMode);
|
|
85
|
-
const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
|
|
86
|
-
const decodeTimes = Schema.decodeEffect(Times, { onExcessProperty: "error" });
|
|
87
|
-
const decodeExpected = Schema.decodeEffect(SetattrOptions.fields.expected.schema, { onExcessProperty: "error" });
|
|
88
|
-
const SETATTR_FIELDS = Object.keys(SetattrOptions.fields);
|
|
89
|
-
const timeAt = (update, value, now) => update === undefined ? value : Match.value(update).pipe(Match.discriminator("kind")("omit", () => value), Match.discriminator("kind")("now", () => now), Match.discriminator("kind")("value", ({ nanoseconds }) => nanoseconds), Match.exhaustive);
|
|
90
|
-
const decodeWriteFileOptions = Schema.decodeEffect(WriteFileOptions, { onExcessProperty: "error" });
|
|
91
|
-
const decodeOpenOptions = Schema.decodeEffect(OpenOptions, { onExcessProperty: "error" });
|
|
92
|
-
const decodeMkdirOptions = Schema.decodeEffect(MkdirOptions, { onExcessProperty: "error" });
|
|
93
|
-
const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessProperty: "error" });
|
|
94
|
-
const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
|
|
95
|
-
const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
|
|
96
|
-
const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
|
|
97
|
-
// Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
|
|
98
|
-
// status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
|
|
99
|
-
// skips it, so reads at one instant (a frozen or coarse clock) store nothing.
|
|
100
|
-
const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
|
|
101
|
-
now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
|
|
102
|
-
const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
|
|
103
|
-
class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
|
|
104
|
-
make: Effect.fnUntraced(function* (base) {
|
|
105
|
-
const revision = base.revision + 1n;
|
|
106
|
-
const cell = MutableRef.make({
|
|
107
|
-
owner: Symbol(),
|
|
108
|
-
pending: new Map(),
|
|
109
|
-
opens: undefined,
|
|
110
|
-
nextInode: base.nextInode,
|
|
111
|
-
changed: false,
|
|
112
|
-
finished: undefined,
|
|
113
|
-
entries: base.entries,
|
|
114
|
-
usedBytes: base.usedBytes,
|
|
115
|
-
events: [],
|
|
116
|
-
after: [],
|
|
117
|
-
removed: []
|
|
118
|
-
});
|
|
119
|
-
const get = (ino) => Effect.sync(() => {
|
|
120
|
-
const draft = MutableRef.get(cell);
|
|
121
|
-
return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
|
|
122
|
-
});
|
|
123
|
-
const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
|
|
124
|
-
const put = (node, changed) => Effect.sync(() => {
|
|
125
|
-
MutableRef.update(cell, (draft) => {
|
|
126
|
-
// This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
|
|
127
|
-
draft.pending.set(node.ino, changed ? { ...node, revision } : node);
|
|
128
|
-
return { ...draft, changed: draft.changed || changed };
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
return {
|
|
132
|
-
revision,
|
|
133
|
-
get,
|
|
134
|
-
put: (node) => put(node, true),
|
|
135
|
-
putQuiet: (node) => put(node, false),
|
|
136
|
-
unchanged: Effect.sync(() => {
|
|
137
|
-
const draft = MutableRef.get(cell);
|
|
138
|
-
return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
|
|
139
|
-
draft.entries === base.entries && draft.usedBytes === base.usedBytes;
|
|
140
|
-
}),
|
|
141
|
-
remove: (ino) => Effect.sync(() => {
|
|
142
|
-
const draft = MutableRef.get(cell);
|
|
143
|
-
draft.pending.set(ino, undefined);
|
|
144
|
-
draft.removed.push(ino);
|
|
145
|
-
}),
|
|
146
|
-
allocate: Effect.sync(() => {
|
|
147
|
-
const next = MutableRef.get(cell).nextInode;
|
|
148
|
-
MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
|
|
149
|
-
return next;
|
|
150
|
-
}),
|
|
151
|
-
canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
|
|
152
|
-
openCount,
|
|
153
|
-
retain: (ino) => Effect.sync(() => {
|
|
154
|
-
const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
|
|
155
|
-
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
|
|
156
|
-
}),
|
|
157
|
-
release: (ino) => Effect.sync(() => {
|
|
158
|
-
const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
|
|
159
|
-
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
|
|
160
|
-
return count;
|
|
161
|
-
}),
|
|
162
|
-
entries: Effect.sync(() => MutableRef.get(cell).entries),
|
|
163
|
-
addEntries: (amount) => Effect.sync(() => {
|
|
164
|
-
MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
|
|
165
|
-
}),
|
|
166
|
-
usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
|
|
167
|
-
addUsedBytes: (amount) => Effect.sync(() => {
|
|
168
|
-
MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
|
|
169
|
-
}),
|
|
170
|
-
publish: (event) => Effect.sync(() => {
|
|
171
|
-
MutableRef.get(cell).events.push(event);
|
|
172
|
-
}),
|
|
173
|
-
afterInstall: (action) => Effect.sync(() => {
|
|
174
|
-
MutableRef.get(cell).after.push(action);
|
|
175
|
-
}),
|
|
176
|
-
finish: Effect.sync(() => {
|
|
177
|
-
const draft = MutableRef.get(cell);
|
|
178
|
-
if (draft.finished !== undefined)
|
|
179
|
-
return draft.finished;
|
|
180
|
-
let inodes = base.inodes;
|
|
181
|
-
for (const [ino, node] of draft.pending)
|
|
182
|
-
inodes = InodeTable.set(inodes, ino, node, draft.owner);
|
|
183
|
-
let open = base.open;
|
|
184
|
-
if (draft.opens !== undefined) {
|
|
185
|
-
const next = new Map(base.open);
|
|
186
|
-
for (const [ino, count] of draft.opens) {
|
|
187
|
-
if (count > 0)
|
|
188
|
-
next.set(ino, count);
|
|
189
|
-
else
|
|
190
|
-
next.delete(ino);
|
|
191
|
-
}
|
|
192
|
-
open = next;
|
|
193
|
-
}
|
|
194
|
-
const finished = {
|
|
195
|
-
inodes,
|
|
196
|
-
open,
|
|
197
|
-
nextInode: draft.nextInode,
|
|
198
|
-
revision: draft.changed ? revision : base.revision,
|
|
199
|
-
entries: draft.entries,
|
|
200
|
-
usedBytes: draft.usedBytes
|
|
201
|
-
};
|
|
202
|
-
MutableRef.update(cell, (current) => ({ ...current, finished }));
|
|
203
|
-
return finished;
|
|
204
|
-
}),
|
|
205
|
-
actions: Effect.sync(() => {
|
|
206
|
-
const { events, after, removed } = MutableRef.get(cell);
|
|
207
|
-
return { events, after, removed };
|
|
208
|
-
})
|
|
209
|
-
};
|
|
210
|
-
})
|
|
211
|
-
}) {
|
|
212
|
-
}
|
|
213
|
-
const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
|
|
214
|
-
const withEntries = (directory, edit) => {
|
|
215
|
-
const entries = new Map(directory.entries);
|
|
216
|
-
edit(entries);
|
|
217
|
-
return { ...directory, entries };
|
|
218
|
-
};
|
|
219
|
-
const withoutLink = (links, parent, name) => {
|
|
220
|
-
const index = links.findIndex((link) => link.parent === parent && link.name === name);
|
|
221
|
-
return index < 0 ? links : [...links.slice(0, index), ...links.slice(index + 1)];
|
|
222
|
-
};
|
|
223
42
|
/** @internal */
|
|
224
43
|
export const VolumeSource = Data.taggedEnum();
|
|
225
|
-
const UpdateChange = Schema.TaggedStruct("Update", { path: BytePath });
|
|
226
44
|
const RescanChange = Schema.TaggedStruct("Rescan", { path: BytePath });
|
|
227
45
|
const RemoveChange = Schema.TaggedStruct("Remove", { path: BytePath });
|
|
228
46
|
const observeChanges = Effect.fnUntraced(function* (captured) {
|
|
@@ -253,16 +71,17 @@ const observeChanges = Effect.fnUntraced(function* (captured) {
|
|
|
253
71
|
return observation;
|
|
254
72
|
});
|
|
255
73
|
const randomHex128 = Effect.gen(function* () {
|
|
74
|
+
// TODO: (#304) Use SecureRandom for construction once its service is released; retain 16-byte IDs.
|
|
256
75
|
const crypto = yield* Crypto.Crypto;
|
|
257
76
|
return Hex.encode(yield* crypto.randomBytes(16));
|
|
258
77
|
}).pipe(Effect.orDie);
|
|
259
78
|
/** @internal */
|
|
260
79
|
export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, options, durability = "memory-only") {
|
|
261
|
-
const services = yield* Effect.context();
|
|
262
|
-
// These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
|
|
263
|
-
const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
|
|
264
80
|
const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
|
|
265
81
|
const decoded = yield* Effect.fromResult(Limits.configuration(options));
|
|
82
|
+
if (decoded.options.execution === "sync" && commitProvider !== memoryCommitProvider) {
|
|
83
|
+
return yield* argumentFailure("make", "execution");
|
|
84
|
+
}
|
|
266
85
|
const requested = decoded.limits;
|
|
267
86
|
const limits = live === undefined ? requested : yield* Effect.fromResult(Limits.fromStored(live.limits, requested));
|
|
268
87
|
const identity = live?.identity ?? (decoded.identity === undefined
|
|
@@ -279,32 +98,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
|
|
|
279
98
|
if (!isTimestamp(initialTime)) {
|
|
280
99
|
return yield* argumentFailure("make", "clock.currentTimeNanos");
|
|
281
100
|
}
|
|
282
|
-
|
|
283
|
-
const now = clock.currentTimeNanosUnsafe();
|
|
284
|
-
return isTimestamp(now)
|
|
285
|
-
? Effect.succeed(now)
|
|
286
|
-
: Effect.fail(op.fail("InvalidArgument"));
|
|
287
|
-
});
|
|
288
|
-
const volumeIdentity = Symbol();
|
|
289
|
-
const directoryHolds = new Map();
|
|
290
|
-
const maxPendingOperations = limits.maxPendingOperations;
|
|
291
|
-
const permits = maxPendingOperations + 1;
|
|
292
|
-
const gate = Semaphore.makeUnsafe(permits);
|
|
293
|
-
// The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
|
|
294
|
-
// overtaken by each later observation. A change holds a turnstile while it gathers the permits, and an
|
|
295
|
-
// observation passes the turnstile before it takes one, so observations that arrive after a change wait
|
|
296
|
-
// behind it. The turnstile goes to its waiters in arrival order, even to one that arrives while a finished
|
|
297
|
-
// change is still releasing. Both waits stay interruptible, and every permit is taken and released by
|
|
298
|
-
// `withPermits`.
|
|
299
|
-
const turnstile = makeTurnstile();
|
|
300
|
-
const observing = (effect) => Effect.andThen(turnstile.withTurn(Effect.void), gate.withPermits(1)(effect));
|
|
301
|
-
const changing = (effect) => turnstile.withTurn(gate.withPermits(permits)(effect));
|
|
302
|
-
const admission = yield* Semaphore.make(permits);
|
|
303
|
-
const admit = (op, effect) => admission.withPermitsIfAvailable(1)(effect).pipe(Effect.flatMap(Option.match({
|
|
304
|
-
onNone: () => op.fail("VolumeBusy"),
|
|
305
|
-
onSome: Effect.succeed
|
|
306
|
-
})));
|
|
307
|
-
let state = emptyState(initialTime);
|
|
101
|
+
let state;
|
|
308
102
|
const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
|
|
309
103
|
if (Predicate.isTagged("Restored")(source)) {
|
|
310
104
|
const restored = yield* reachableValue(source.value);
|
|
@@ -319,142 +113,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
|
|
|
319
113
|
}
|
|
320
114
|
if (live !== undefined)
|
|
321
115
|
state = live.value;
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const install = Effect.fnUntraced(function* (finished) {
|
|
332
|
-
const before = state;
|
|
333
|
-
state = yield* finished.finish;
|
|
334
|
-
const actions = yield* finished.actions;
|
|
335
|
-
for (const ino of actions.removed)
|
|
336
|
-
registry.forget(ino);
|
|
337
|
-
for (const apply of actions.after)
|
|
338
|
-
apply();
|
|
339
|
-
const after = state;
|
|
340
|
-
watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), { before, after });
|
|
341
|
-
});
|
|
342
|
-
const transition = Effect.fnUntraced(function* (change) {
|
|
343
|
-
const current = yield* Draft.make(state);
|
|
344
|
-
const value = yield* Effect.provideService(change, Draft, current);
|
|
345
|
-
return [value, current];
|
|
346
|
-
});
|
|
347
|
-
// Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
|
|
348
|
-
const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
|
|
349
|
-
if ((yield* finished.unchanged) || (cleanup && !available))
|
|
350
|
-
return yield* install(finished);
|
|
351
|
-
const next = yield* finished.finish;
|
|
352
|
-
const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
|
|
353
|
-
if (!answer.available)
|
|
354
|
-
available = false;
|
|
355
|
-
if (answer.failure !== undefined) {
|
|
356
|
-
if (cleanup)
|
|
357
|
-
yield* install(finished);
|
|
358
|
-
return yield* answer.failure;
|
|
359
|
-
}
|
|
360
|
-
yield* install(finished);
|
|
361
|
-
});
|
|
362
|
-
const applyCleanup = Effect.fnUntraced(function* (change) {
|
|
363
|
-
const current = yield* Draft.make(state);
|
|
364
|
-
yield* Effect.provideService(change(current), Draft, current);
|
|
365
|
-
yield* committed(OpContext.make("close"), current, true);
|
|
366
|
-
});
|
|
367
|
-
const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
|
|
368
|
-
// Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
|
|
369
|
-
// interruptible until the candidate is ready. Every provider commit and installation is masked.
|
|
370
|
-
const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
|
|
371
|
-
const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
|
|
372
|
-
return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
|
|
373
|
-
}))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
374
|
-
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));
|
|
375
|
-
const refreshAccess = Effect.fnUntraced(function* (read) {
|
|
376
|
-
if (refreshDue(read)) {
|
|
377
|
-
const { node, now } = read.access;
|
|
378
|
-
const draft = yield* Draft;
|
|
379
|
-
yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
|
|
380
|
-
}
|
|
381
|
-
return read.value;
|
|
382
|
-
});
|
|
383
|
-
// A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
|
|
384
|
-
// only when the access time is due does it run again as a change. The change repeats every check and the rule,
|
|
385
|
-
// since another read may have refreshed the time meanwhile, and its result is the one returned.
|
|
386
|
-
const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
|
|
387
|
-
? coordinated(op, Effect.flatMap(read, refreshAccess))
|
|
388
|
-
: Effect.succeed(observed.value));
|
|
389
|
-
const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
|
|
390
|
-
const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
|
|
391
|
-
available = false;
|
|
392
|
-
}), commitProvider.shutdown)));
|
|
393
|
-
const pathOf = (get, ino) => {
|
|
394
|
-
const names = [];
|
|
395
|
-
let node = get(ino);
|
|
396
|
-
if (node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
397
|
-
return undefined;
|
|
398
|
-
while (node.ino !== ROOT_INO) {
|
|
399
|
-
names.push(node.name);
|
|
400
|
-
const parent = get(node.parent);
|
|
401
|
-
if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
|
|
402
|
-
return undefined;
|
|
403
|
-
node = parent;
|
|
404
|
-
}
|
|
405
|
-
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
406
|
-
};
|
|
407
|
-
const pathOfCurrent = Effect.fnUntraced(function* (ino) {
|
|
408
|
-
const names = [];
|
|
409
|
-
const first = yield* view(ino);
|
|
410
|
-
if (first?.kind !== "directory" || first.metadata.nlink === 0)
|
|
411
|
-
return undefined;
|
|
412
|
-
let node = first;
|
|
413
|
-
while (node.ino !== ROOT_INO) {
|
|
414
|
-
names.push(node.name);
|
|
415
|
-
const parent = yield* view(node.parent);
|
|
416
|
-
if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
|
|
417
|
-
return undefined;
|
|
418
|
-
node = parent;
|
|
419
|
-
}
|
|
420
|
-
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
421
|
-
});
|
|
422
|
-
const entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
|
|
423
|
-
const publishEntry = (_tag, parent, name, ino) => Effect.flatMap(Draft, (draft) => draft.publish((installed) => {
|
|
424
|
-
const prefix = pathOf((at) => getNode(installed, at), parent);
|
|
425
|
-
return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
|
|
426
|
-
}));
|
|
427
|
-
const publishNode = Effect.fnUntraced(function* (ino) {
|
|
428
|
-
const draft = yield* Draft;
|
|
429
|
-
const target = yield* draft.get(ino);
|
|
430
|
-
if (target === undefined)
|
|
431
|
-
return;
|
|
432
|
-
if (target.kind === "directory") {
|
|
433
|
-
yield* draft.publish((installed) => {
|
|
434
|
-
const directory = getNode(installed, ino);
|
|
435
|
-
const path = pathOf((at) => getNode(installed, at), ino);
|
|
436
|
-
return path === undefined || directory?.kind !== "directory"
|
|
437
|
-
? []
|
|
438
|
-
: [{ change: UpdateChange.make({ path: ownedPath(nameBytes(path)) }), parent: directory.parent, ino }];
|
|
439
|
-
});
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const links = target.links;
|
|
443
|
-
yield* draft.publish((installed) => {
|
|
444
|
-
const events = [];
|
|
445
|
-
for (const link of links) {
|
|
446
|
-
const prefix = pathOf((at) => getNode(installed, at), link.parent);
|
|
447
|
-
if (prefix !== undefined) {
|
|
448
|
-
events.push({
|
|
449
|
-
change: UpdateChange.make({ path: entryPath(prefix, link.name) }),
|
|
450
|
-
parent: link.parent,
|
|
451
|
-
ino
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return events;
|
|
456
|
-
});
|
|
116
|
+
let watchHub;
|
|
117
|
+
const { maxWatchEvents, ...basicLimits } = limits;
|
|
118
|
+
const engine = yield* Engine.makeEngine({
|
|
119
|
+
execution: decoded.options.execution,
|
|
120
|
+
limits: basicLimits,
|
|
121
|
+
initialTime,
|
|
122
|
+
initialState: state,
|
|
123
|
+
commitProvider,
|
|
124
|
+
installed: (events, installation) => watchHub.publishUnsafe(events, installation)
|
|
457
125
|
});
|
|
126
|
+
watchHub = yield* WatchHub.make(engine.coordinate, maxWatchEvents, engine.execution === "sync");
|
|
458
127
|
const pathsOf = (installed, ino) => {
|
|
459
128
|
const node = getNode(installed, ino);
|
|
460
129
|
if (node === undefined)
|
|
@@ -488,10 +157,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
|
|
|
488
157
|
includes: () => true,
|
|
489
158
|
rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
|
|
490
159
|
};
|
|
491
|
-
const scopeRoot =
|
|
492
|
-
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(scope, "reference"), "reference", op);
|
|
493
|
-
return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
|
|
494
|
-
});
|
|
160
|
+
const scopeRoot = engine.watchRoot;
|
|
495
161
|
// A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
|
|
496
162
|
// each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
|
|
497
163
|
// gone the watch reports its removal from the names it had and ends; that Remove is held back for the end of
|
|
@@ -517,12 +183,15 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
|
|
|
517
183
|
: undefined
|
|
518
184
|
};
|
|
519
185
|
};
|
|
520
|
-
|
|
521
|
-
|
|
186
|
+
// Cooperative capture retains its established ordering; sync summaries run after immutable capture.
|
|
187
|
+
const capture = (operation, read) => engine.cooperativeCapture === undefined
|
|
188
|
+
? Effect.flatMap(engine.capture(operation), read)
|
|
189
|
+
: engine.cooperativeCapture(operation, read);
|
|
190
|
+
const captureState = capture("capture", Effect.fnUntraced(function* (captured) {
|
|
522
191
|
const snapshot = Image.make(captured);
|
|
523
192
|
yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
|
|
524
193
|
return { snapshot, observation: yield* observeChanges(captured) };
|
|
525
|
-
});
|
|
194
|
+
}));
|
|
526
195
|
const keyTag = (ino) => {
|
|
527
196
|
const message = new Uint8Array(40);
|
|
528
197
|
message.set(identityBytes);
|
|
@@ -530,1538 +199,32 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
|
|
|
530
199
|
new DataView(message.buffer).setBigUint64(32, ino);
|
|
531
200
|
return hmacSha256(keySecretBytes, message).subarray(0, 16);
|
|
532
201
|
};
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
directoryHolds.delete(ino);
|
|
543
|
-
const node = getNode(state, ino);
|
|
544
|
-
if (node?.kind === "directory" && node.metadata.nlink === 0) {
|
|
545
|
-
yield* applyCleanup((d) => d.remove(ino));
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
|
|
549
|
-
const finalizeDirectory = (reference) => {
|
|
550
|
-
const close = coordinatedCleanup(releaseDirectory(reference));
|
|
551
|
-
return TokenRegistry.finalize(reference, close, Effect.ignore(close));
|
|
552
|
-
};
|
|
553
|
-
const closeReleasedScope = TokenRegistry.closeReleasedScope;
|
|
554
|
-
const permitted = Resolution.permitted;
|
|
555
|
-
const authorize = Resolution.authorize;
|
|
556
|
-
const reclaim = Effect.fnUntraced(function* (d, ino) {
|
|
557
|
-
const node = yield* d.get(ino);
|
|
558
|
-
if (node === undefined)
|
|
559
|
-
return;
|
|
560
|
-
if (node.kind === "file") {
|
|
561
|
-
if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
|
|
562
|
-
return;
|
|
563
|
-
yield* d.addUsedBytes(-BigInt(node.data.length));
|
|
564
|
-
}
|
|
565
|
-
else if (node.kind === "symlink") {
|
|
566
|
-
if (node.links.length > 0)
|
|
567
|
-
return;
|
|
568
|
-
yield* d.addUsedBytes(-BigInt(node.target.length));
|
|
569
|
-
}
|
|
570
|
-
else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
|
|
571
|
-
return;
|
|
572
|
-
yield* d.remove(ino);
|
|
573
|
-
});
|
|
574
|
-
const atEntryLimit = Effect.gen(function* () {
|
|
575
|
-
const draft = yield* Draft;
|
|
576
|
-
return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
|
|
577
|
-
});
|
|
578
|
-
const reserveEntry = Effect.fnUntraced(function* (op) {
|
|
579
|
-
const draft = yield* Draft;
|
|
580
|
-
if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
|
|
581
|
-
return yield* op.fail("NoSpace");
|
|
582
|
-
});
|
|
583
|
-
const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
|
|
584
|
-
const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
|
|
585
|
-
if (limits.maxBytes === undefined)
|
|
586
|
-
return;
|
|
587
|
-
const draft = yield* Draft;
|
|
588
|
-
if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
|
|
589
|
-
return yield* op.fail("NoSpace");
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
const attach = Effect.fnUntraced(function* (parent, name, child, now) {
|
|
593
|
-
const d = yield* Draft;
|
|
594
|
-
yield* d.put(withEntries({
|
|
595
|
-
...parent,
|
|
596
|
-
metadata: {
|
|
597
|
-
...parent.metadata,
|
|
598
|
-
nlink: parent.metadata.nlink + (child.kind === "directory" ? 1 : 0),
|
|
599
|
-
mtimeNs: now,
|
|
600
|
-
ctimeNs: now
|
|
601
|
-
}
|
|
602
|
-
}, (entries) => entries.set(name, child.ino)));
|
|
603
|
-
if (child.kind === "directory")
|
|
604
|
-
yield* d.put({ ...child, parent: parent.ino, name });
|
|
605
|
-
else {
|
|
606
|
-
yield* d.put({
|
|
607
|
-
...child,
|
|
608
|
-
links: [...child.links, { parent: parent.ino, name }],
|
|
609
|
-
metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
const detach = Effect.fnUntraced(function* (child, parent, name, now) {
|
|
614
|
-
const d = yield* Draft;
|
|
615
|
-
if (child.kind === "directory") {
|
|
616
|
-
yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
|
|
617
|
-
}
|
|
618
|
-
else {
|
|
619
|
-
const links = withoutLink(child.links, parent, name);
|
|
620
|
-
yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
|
|
621
|
-
}
|
|
622
|
-
yield* reclaim(d, child.ino);
|
|
623
|
-
});
|
|
624
|
-
const releaseFileInode = Effect.fnUntraced(function* (ino) {
|
|
625
|
-
const d = yield* Draft;
|
|
626
|
-
yield* d.release(ino);
|
|
627
|
-
yield* reclaim(d, ino);
|
|
628
|
-
});
|
|
629
|
-
const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
|
|
630
|
-
const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
|
|
631
|
-
// Admission refusal leaves the handle open. Every admitted close shares the registry release,
|
|
632
|
-
// and a storage failure still completes cleanup while only explicit close reports the failure.
|
|
633
|
-
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)));
|
|
634
|
-
const finalizeFile = (ref) => TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref));
|
|
635
|
-
const acquireHandle = TokenRegistry.acquire;
|
|
636
|
-
const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
|
|
637
|
-
const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
|
|
638
|
-
const d = yield* Draft;
|
|
639
|
-
yield* d.addUsedBytes(BigInt(data.length - file.data.length));
|
|
640
|
-
yield* d.put({
|
|
641
|
-
...file,
|
|
642
|
-
data,
|
|
643
|
-
metadata: {
|
|
644
|
-
...file.metadata,
|
|
645
|
-
size: BigInt(data.length),
|
|
646
|
-
mode: file.metadata.mode & ~SET_ID_BITS,
|
|
647
|
-
mtimeNs: now,
|
|
648
|
-
ctimeNs: now
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
if (publish)
|
|
652
|
-
yield* publishNode(file.ino);
|
|
653
|
-
});
|
|
654
|
-
const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
|
|
655
|
-
if (length > BigInt(maxFileBytes))
|
|
656
|
-
return yield* op.fail("FileTooLarge");
|
|
657
|
-
const size = Number(length);
|
|
658
|
-
yield* reserveBytes(op, BigInt(size - file.data.length));
|
|
659
|
-
const data = new Uint8Array(size);
|
|
660
|
-
data.set(file.data.subarray(0, size));
|
|
661
|
-
yield* replaceContent(file, data, now, false);
|
|
662
|
-
});
|
|
663
|
-
const resize = Effect.fnUntraced(function* (file, length, op) {
|
|
664
|
-
if (!isLength(length))
|
|
665
|
-
return yield* op.fail("InvalidArgument");
|
|
666
|
-
yield* resizeAt(file, length, yield* timestamp(op), op);
|
|
667
|
-
yield* publishNode(file.ino);
|
|
668
|
-
});
|
|
669
|
-
const fileHandle = (ref) => {
|
|
670
|
-
const get = Effect.fnUntraced(function* (op, access) {
|
|
671
|
-
const ino = TokenRegistry.inode(ref);
|
|
672
|
-
const node = ino === undefined ? undefined : yield* view(ino);
|
|
673
|
-
if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
|
|
674
|
-
(access === "write" && ref.access === "read"))
|
|
675
|
-
return yield* op.fail("InvalidHandle");
|
|
676
|
-
return node;
|
|
677
|
-
});
|
|
678
|
-
const read = (maximum, position) => {
|
|
679
|
-
const op = OpContext.make(position === undefined ? "read" : "pread");
|
|
680
|
-
const readOp = OpContext.make("read");
|
|
681
|
-
const body = Effect.gen(function* () {
|
|
682
|
-
const file = yield* get(op, "read");
|
|
683
|
-
if (!isNatural(maximum))
|
|
684
|
-
return yield* readOp.fail("InvalidArgument");
|
|
685
|
-
const offset = position ?? ref.offset;
|
|
686
|
-
if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
|
|
687
|
-
return yield* readOp.fail("InvalidArgument");
|
|
688
|
-
}
|
|
689
|
-
const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
|
|
690
|
-
const data = file.data.slice(start, start + Math.min(maximum, file.data.length - start));
|
|
691
|
-
const eof = start + data.length >= file.data.length;
|
|
692
|
-
const access = maximum > 0 ? { node: file, now: yield* timestamp(readOp) } : undefined;
|
|
693
|
-
return { value: { bytes: data, eof, next: offset + BigInt(data.length) }, access };
|
|
694
|
-
});
|
|
695
|
-
// A positioned read runs beside other reads. A cursor read stays a change, since two reads beside each
|
|
696
|
-
// other would start at the same offset.
|
|
697
|
-
if (position !== undefined)
|
|
698
|
-
return Effect.map(accessing(op, body), ({ bytes, eof }) => ({ bytes, eof }));
|
|
699
|
-
return coordinated(op, Effect.gen(function* () {
|
|
700
|
-
const { bytes, eof, next } = yield* refreshAccess(yield* body);
|
|
701
|
-
const draft = yield* Draft;
|
|
702
|
-
yield* draft.afterInstall(() => {
|
|
703
|
-
ref.offset = next;
|
|
704
|
-
});
|
|
705
|
-
return { bytes, eof };
|
|
706
|
-
}));
|
|
707
|
-
};
|
|
708
|
-
const write = Effect.fnUntraced(function* (input, position) {
|
|
709
|
-
const op = OpContext.make(position === undefined ? "write" : "pwrite");
|
|
710
|
-
const writeOp = OpContext.make("write");
|
|
711
|
-
if (!isAttachedBytes(input))
|
|
712
|
-
return yield* writeOp.fail("InvalidArgument");
|
|
713
|
-
const bytes = new Uint8Array(input);
|
|
714
|
-
return yield* coordinated(op, Effect.gen(function* () {
|
|
715
|
-
const file = yield* get(op, "write");
|
|
716
|
-
const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
|
|
717
|
-
if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
|
|
718
|
-
return yield* writeOp.fail("InvalidArgument");
|
|
719
|
-
}
|
|
720
|
-
if (bytes.length === 0) {
|
|
721
|
-
return 0;
|
|
722
|
-
}
|
|
723
|
-
if (offset >= BigInt(maxFileBytes))
|
|
724
|
-
return yield* writeOp.fail("FileTooLarge");
|
|
725
|
-
const start = Number(offset);
|
|
726
|
-
const draft = yield* Draft;
|
|
727
|
-
const free = limits.maxBytes === undefined
|
|
728
|
-
? BigInt(maxFileBytes)
|
|
729
|
-
: ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
|
|
730
|
-
const maximumEnd = BigInt(file.data.length) + free;
|
|
731
|
-
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
732
|
-
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
733
|
-
if (count === 0)
|
|
734
|
-
return yield* writeOp.fail("NoSpace");
|
|
735
|
-
const size = Math.max(file.data.length, start + count);
|
|
736
|
-
// Always detach before mutation. A same-sized write is the critical
|
|
737
|
-
// case: the current payload may belong to the base or a prior capture.
|
|
738
|
-
const data = new Uint8Array(size);
|
|
739
|
-
data.set(file.data);
|
|
740
|
-
const now = yield* timestamp(writeOp);
|
|
741
|
-
data.set(bytes.subarray(0, count), start);
|
|
742
|
-
yield* replaceContent(file, data, now);
|
|
743
|
-
if (position === undefined) {
|
|
744
|
-
const next = offset + BigInt(count);
|
|
745
|
-
yield* draft.afterInstall(() => {
|
|
746
|
-
ref.offset = next;
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
return count;
|
|
750
|
-
}));
|
|
751
|
-
});
|
|
752
|
-
const statOp = OpContext.make("stat");
|
|
753
|
-
const syncOp = OpContext.make("sync");
|
|
754
|
-
const closeOp = OpContext.make("close");
|
|
755
|
-
const handle = Object.freeze({
|
|
756
|
-
[FileHandleId]: true,
|
|
757
|
-
read: Effect.fn("FileHandle.read")(function* (maximum) {
|
|
758
|
-
return (yield* read(maximum)).bytes;
|
|
759
|
-
}),
|
|
760
|
-
pread: Effect.fn("FileHandle.pread")(function* (maximum, offset) {
|
|
761
|
-
return yield* read(maximum, offset);
|
|
762
|
-
}),
|
|
763
|
-
write: Effect.fn("FileHandle.write")(function* (bytes) {
|
|
764
|
-
return yield* write(bytes);
|
|
765
|
-
}),
|
|
766
|
-
pwrite: Effect.fn("FileHandle.pwrite")(function* (bytes, offset) {
|
|
767
|
-
return yield* write(bytes, offset);
|
|
768
|
-
}),
|
|
769
|
-
seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
|
|
770
|
-
const op = OpContext.make("seek");
|
|
771
|
-
return yield* coordinatedRead(op, Effect.uninterruptible(Effect.gen(function* () {
|
|
772
|
-
const file = yield* get(op);
|
|
773
|
-
if (!Predicate.isBigInt(offset) || !isSeekMode(mode)) {
|
|
774
|
-
return yield* op.fail("InvalidArgument");
|
|
775
|
-
}
|
|
776
|
-
let next = mode === "current"
|
|
777
|
-
? ref.offset + offset
|
|
778
|
-
: mode === "end"
|
|
779
|
-
? file.metadata.size + offset
|
|
780
|
-
: offset;
|
|
781
|
-
if (next < 0n || next > MAX_FILE_OFFSET) {
|
|
782
|
-
return yield* op.fail("InvalidArgument");
|
|
783
|
-
}
|
|
784
|
-
if (mode === "data" || mode === "hole") {
|
|
785
|
-
if (offset >= file.metadata.size)
|
|
786
|
-
return yield* op.fail("NoData");
|
|
787
|
-
if (mode === "hole")
|
|
788
|
-
next = file.metadata.size;
|
|
789
|
-
}
|
|
790
|
-
ref.offset = next;
|
|
791
|
-
return next;
|
|
792
|
-
})));
|
|
793
|
-
}),
|
|
794
|
-
truncate: Effect.fn("FileHandle.truncate")(function* (length) {
|
|
795
|
-
const op = OpContext.make("truncate");
|
|
796
|
-
return yield* coordinated(op, Effect.gen(function* () {
|
|
797
|
-
yield* resize(yield* get(op, "write"), length, op);
|
|
798
|
-
}));
|
|
799
|
-
}),
|
|
800
|
-
stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata)).pipe(Effect.withSpan("FileHandle.stat")),
|
|
801
|
-
sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))).pipe(Effect.withSpan("FileHandle.sync")),
|
|
802
|
-
close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref)), Effect.withSpan("FileHandle.close"))
|
|
803
|
-
});
|
|
804
|
-
registry.registerFile(handle, ref);
|
|
805
|
-
return handle;
|
|
806
|
-
};
|
|
807
|
-
const createCaller = (reference, identity, umask) => {
|
|
808
|
-
const resolver = Resolution.make({
|
|
809
|
-
caller: reference,
|
|
810
|
-
get: view,
|
|
811
|
-
identity,
|
|
812
|
-
maxPathBytes: limits.maxPathBytes,
|
|
813
|
-
registry
|
|
814
|
-
});
|
|
815
|
-
const prepare = resolver.prepare;
|
|
816
|
-
const prepareEntry = resolver.prepareEntry;
|
|
817
|
-
const entryName = Resolution.entryName;
|
|
818
|
-
const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
|
|
819
|
-
const resolveDirectory = resolver.directory;
|
|
820
|
-
const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
|
|
821
|
-
const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
|
|
822
|
-
const directoryNow = Effect.fnUntraced(function* (ino) {
|
|
823
|
-
const node = yield* view(ino);
|
|
824
|
-
if (node?.kind !== "directory") {
|
|
825
|
-
return dieInvariant("Directory left the inode table during a transition");
|
|
826
|
-
}
|
|
827
|
-
return node;
|
|
828
|
-
});
|
|
829
|
-
const nodeNow = Effect.fnUntraced(function* (ino) {
|
|
830
|
-
const node = yield* view(ino);
|
|
831
|
-
if (node === undefined)
|
|
832
|
-
return dieInvariant("Inode left the table during a transition");
|
|
833
|
-
return node;
|
|
834
|
-
});
|
|
835
|
-
const creationTimes = (times, now) => ({
|
|
836
|
-
atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
|
|
837
|
-
mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
|
|
838
|
-
});
|
|
839
|
-
const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
|
|
840
|
-
const draft = yield* Draft;
|
|
841
|
-
const ino = yield* draft.allocate;
|
|
842
|
-
return {
|
|
843
|
-
kind: "directory",
|
|
844
|
-
ino,
|
|
845
|
-
parent: parent.ino,
|
|
846
|
-
name: "",
|
|
847
|
-
entries: new Map(),
|
|
848
|
-
metadata: {
|
|
849
|
-
...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, mode, now),
|
|
850
|
-
...creationTimes(times, now)
|
|
851
|
-
},
|
|
852
|
-
revision: 0n
|
|
853
|
-
};
|
|
854
|
-
});
|
|
855
|
-
const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
|
|
856
|
-
const draft = yield* Draft;
|
|
857
|
-
const ino = yield* draft.allocate;
|
|
858
|
-
return {
|
|
859
|
-
kind: "file",
|
|
860
|
-
ino,
|
|
861
|
-
data,
|
|
862
|
-
links: [],
|
|
863
|
-
metadata: {
|
|
864
|
-
...directoryMetadata(BigInt(ino), owner?.uid ?? identity.uid, owner?.gid ?? parent.metadata.gid, mode, now),
|
|
865
|
-
...creationTimes(times, now),
|
|
866
|
-
kind: "file",
|
|
867
|
-
size: BigInt(data.length),
|
|
868
|
-
nlink: 0
|
|
869
|
-
},
|
|
870
|
-
revision: 0n
|
|
871
|
-
};
|
|
872
|
-
});
|
|
873
|
-
const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
|
|
874
|
-
const draft = yield* Draft;
|
|
875
|
-
const ino = yield* draft.allocate;
|
|
876
|
-
return {
|
|
877
|
-
kind: "symlink",
|
|
878
|
-
ino,
|
|
879
|
-
target,
|
|
880
|
-
links: [],
|
|
881
|
-
metadata: {
|
|
882
|
-
...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, 0o777, now),
|
|
883
|
-
...creationTimes(times, now),
|
|
884
|
-
kind: "symlink",
|
|
885
|
-
nlink: 0,
|
|
886
|
-
size: BigInt(target.length)
|
|
887
|
-
},
|
|
888
|
-
revision: 0n
|
|
889
|
-
};
|
|
890
|
-
});
|
|
891
|
-
const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
|
|
892
|
-
const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
|
|
893
|
-
? Effect.fail(op.fail("NotPermitted"))
|
|
894
|
-
: Effect.succeed(grantedMode(metadata, mode));
|
|
895
|
-
// Changes the attributes one change at a time would, as one change: one draft, one revision, one event.
|
|
896
|
-
// Every check runs against the node before any attribute applies, ownership (NotPermitted) before
|
|
897
|
-
// permission (AccessDenied), so the first failure leaves everything unchanged. The attributes apply as
|
|
898
|
-
// size, owner, mode, then times, the POSIX composition of chown then chmod, so a requested mode wins over
|
|
899
|
-
// the set-ID clearing that a resize or an owner change triggers; an adapter owns any protocol-specific
|
|
900
|
-
// sanitising of the mode, as NFS SETATTR does after knfsd. The attributes arrive validated; an owner with
|
|
901
|
-
// neither id still checks ownership, and times that omit both are no change and check nothing. An expected
|
|
902
|
-
// revision is checked first, against the target as the change finds it, so a caller that decided the
|
|
903
|
-
// attributes from an earlier observation learns the target moved instead of applying them to a newer state.
|
|
904
|
-
const changeAttributes = (resolve, attributes, op) => {
|
|
905
|
-
const { size, mode } = attributes;
|
|
906
|
-
const expected = attributes.expected?.revision;
|
|
907
|
-
const owner = attributes.owner === undefined ? undefined : { ...attributes.owner };
|
|
908
|
-
const times = attributes.times === undefined
|
|
909
|
-
? undefined
|
|
910
|
-
: { access: { ...attributes.times.access }, modification: { ...attributes.times.modification } };
|
|
911
|
-
const timed = times !== undefined && (times.access.kind !== "omit" || times.modification.kind !== "omit");
|
|
912
|
-
const bothNow = times?.access.kind === "now" && times.modification.kind === "now";
|
|
913
|
-
const resolving = resolve();
|
|
914
|
-
return coordinated(op, Effect.gen(function* () {
|
|
915
|
-
const resolved = yield* resolving;
|
|
916
|
-
const node = yield* nodeNow(resolved.ino);
|
|
917
|
-
if (expected !== undefined && node.revision !== expected) {
|
|
918
|
-
return yield* resolved.op.fail("StaleReference", { field: "expected" });
|
|
919
|
-
}
|
|
920
|
-
if (size !== undefined && node.kind === "symlink")
|
|
921
|
-
return yield* resolved.op.fail("SymlinkLoop");
|
|
922
|
-
if (size !== undefined && node.kind !== "file")
|
|
923
|
-
return yield* resolved.op.fail("IsDirectory");
|
|
924
|
-
const owns = identity.privileged || identity.uid === node.metadata.uid;
|
|
925
|
-
if ((mode !== undefined && !owns) ||
|
|
926
|
-
(owner !== undefined && !identity.privileged && (!owns ||
|
|
927
|
-
(owner.uid !== undefined && owner.uid !== node.metadata.uid) ||
|
|
928
|
-
(owner.gid !== undefined && !inGroup(identity, owner.gid)))) ||
|
|
929
|
-
// POSIX grants write access only when both times are UTIME_NOW; every other combination that
|
|
930
|
-
// changes a time, mixed ones included, needs ownership.
|
|
931
|
-
(timed && !owns && !bothNow)) {
|
|
932
|
-
return yield* resolved.op.fail("NotPermitted");
|
|
933
|
-
}
|
|
934
|
-
if (size !== undefined || (timed && !owns))
|
|
935
|
-
yield* authorize(node, identity, WRITE, resolved.op);
|
|
936
|
-
const chowned = owner?.uid !== undefined || owner?.gid !== undefined;
|
|
937
|
-
if (size === undefined && !chowned && mode === undefined && !timed)
|
|
938
|
-
return;
|
|
939
|
-
const now = yield* timestamp(op);
|
|
940
|
-
if (size !== undefined && node.kind === "file")
|
|
941
|
-
yield* resizeAt(node, size, now, op);
|
|
942
|
-
const sized = yield* nodeNow(resolved.ino);
|
|
943
|
-
const gid = owner?.gid ?? sized.metadata.gid;
|
|
944
|
-
const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
|
|
945
|
-
if (chowned || mode !== undefined || timed) {
|
|
946
|
-
yield* (yield* Draft).put({
|
|
947
|
-
...sized,
|
|
948
|
-
metadata: {
|
|
949
|
-
...sized.metadata,
|
|
950
|
-
uid: owner?.uid ?? sized.metadata.uid,
|
|
951
|
-
gid,
|
|
952
|
-
mode: mode === undefined ? cleared : grantedMode({ kind: sized.kind, gid }, mode),
|
|
953
|
-
atimeNs: timeAt(times?.access, sized.metadata.atimeNs, now),
|
|
954
|
-
mtimeNs: timeAt(times?.modification, sized.metadata.mtimeNs, now),
|
|
955
|
-
ctimeNs: now
|
|
956
|
-
}
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
yield* publishNode(node.ino);
|
|
960
|
-
}));
|
|
961
|
-
};
|
|
962
|
-
const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
|
|
963
|
-
identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
|
|
964
|
-
? Effect.fail(op.fail("NotPermitted"))
|
|
965
|
-
: Effect.void;
|
|
966
|
-
const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
|
|
967
|
-
const parent = yield* directoryNow(entry.parent);
|
|
968
|
-
if (isDotComponent(entry.name)) {
|
|
969
|
-
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
|
|
970
|
-
}
|
|
971
|
-
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
972
|
-
if (parent.entries.has(entry.name))
|
|
973
|
-
return yield* entry.op.fail("AlreadyExists");
|
|
974
|
-
if (trailingSlash === "rejected" && entry.trailingSlash)
|
|
975
|
-
return yield* entry.op.fail("NotFound");
|
|
976
|
-
yield* authorize(parent, identity, WRITE, entry.op);
|
|
977
|
-
return entry.name;
|
|
978
|
-
});
|
|
979
|
-
const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
|
|
980
|
-
const name = yield* claimName(entry);
|
|
981
|
-
yield* reserveEntry(entry.op);
|
|
982
|
-
const parent = yield* directoryNow(entry.parent);
|
|
983
|
-
const before = parent.revision;
|
|
984
|
-
const now = yield* timestamp(op);
|
|
985
|
-
const mode = request.exactMode
|
|
986
|
-
? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
|
|
987
|
-
: (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
|
|
988
|
-
const child = yield* newDirectory(parent, mode, now, request.times);
|
|
989
|
-
yield* attach(parent, name, child, now);
|
|
990
|
-
const draft = yield* Draft;
|
|
991
|
-
yield* draft.addEntries(1);
|
|
992
|
-
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
993
|
-
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
994
|
-
});
|
|
995
|
-
// Creates every missing directory a path names in the running transition, so a failure anywhere discards
|
|
996
|
-
// them all. The mode applies to each directory created and the times to the one in the final position. An
|
|
997
|
-
// entry names one child, created unless it is already a directory. The result names the directory the path
|
|
998
|
-
// ends on and its parent's revision before and after the call, so a final directory that already exists is
|
|
999
|
-
// no change, while a path that leaves a directory it created, such as `new/..`, reports the creation.
|
|
1000
|
-
const makeDirectories = Effect.fnUntraced(function* (prepared, request, op) {
|
|
1001
|
-
let node;
|
|
1002
|
-
let entryOp = op;
|
|
1003
|
-
const revisionsBefore = new Map();
|
|
1004
|
-
if (prepared.kind === "entry") {
|
|
1005
|
-
const entry = yield* resolveEntry(prepared, op);
|
|
1006
|
-
const made = yield* Effect.result(makeDirectory(entry, request, op));
|
|
1007
|
-
if (Result.isSuccess(made))
|
|
1008
|
-
return made.success;
|
|
1009
|
-
const existingIno = entry.name === undefined
|
|
1010
|
-
? undefined
|
|
1011
|
-
: (yield* directoryNow(entry.parent)).entries.get(entry.name);
|
|
1012
|
-
node = existingIno === undefined ? undefined : (yield* view(existingIno));
|
|
1013
|
-
if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
|
|
1014
|
-
return yield* made.failure;
|
|
1015
|
-
}
|
|
1016
|
-
else {
|
|
1017
|
-
entryOp = op.at(prepared.path.input);
|
|
1018
|
-
const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
|
|
1019
|
-
? { path: prepared.path.input }
|
|
1020
|
-
: { path: prepared.path.input, relativeTo: prepared.base }), {
|
|
1021
|
-
kind: "Node",
|
|
1022
|
-
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) => {
|
|
1023
|
-
if (!revisionsBefore.has(parent.ino))
|
|
1024
|
-
revisionsBefore.set(parent.ino, made.directory.before);
|
|
1025
|
-
return directoryNow(made.child);
|
|
1026
|
-
})
|
|
1027
|
-
}, op);
|
|
1028
|
-
node = resolved.node;
|
|
1029
|
-
}
|
|
1030
|
-
if (node?.kind !== "directory")
|
|
1031
|
-
return yield* entryOp.fail("AlreadyExists");
|
|
1032
|
-
const after = (yield* directoryNow(node.parent)).revision;
|
|
1033
|
-
return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
|
|
1034
|
-
});
|
|
1035
|
-
const linkNode = Effect.fnUntraced(function* (node, entry, op) {
|
|
1036
|
-
const name = yield* claimName(entry, "rejected");
|
|
1037
|
-
yield* reserveEntry(entry.op);
|
|
1038
|
-
const parent = yield* directoryNow(entry.parent);
|
|
1039
|
-
const before = parent.revision;
|
|
1040
|
-
const now = yield* timestamp(op);
|
|
1041
|
-
yield* attach(parent, name, node, now);
|
|
1042
|
-
const draft = yield* Draft;
|
|
1043
|
-
yield* draft.addEntries(1);
|
|
1044
|
-
yield* publishEntry("Create", parent.ino, name, node.ino);
|
|
1045
|
-
return { before, after: draft.revision };
|
|
1046
|
-
});
|
|
1047
|
-
const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
|
|
1048
|
-
const name = yield* claimName(entry, "rejected");
|
|
1049
|
-
yield* reserveEntry(entry.op);
|
|
1050
|
-
yield* reserveBytes(entry.op, BigInt(target.length));
|
|
1051
|
-
const parent = yield* directoryNow(entry.parent);
|
|
1052
|
-
const before = parent.revision;
|
|
1053
|
-
const now = yield* timestamp(op);
|
|
1054
|
-
const child = yield* newSymlink(parent, target, now, times);
|
|
1055
|
-
yield* attach(parent, name, child, now);
|
|
1056
|
-
const draft = yield* Draft;
|
|
1057
|
-
yield* draft.addEntries(1);
|
|
1058
|
-
yield* draft.addUsedBytes(BigInt(target.length));
|
|
1059
|
-
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
1060
|
-
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
1061
|
-
});
|
|
1062
|
-
const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
|
|
1063
|
-
const before = parent.revision;
|
|
1064
|
-
const now = yield* timestamp(op);
|
|
1065
|
-
const d = yield* Draft;
|
|
1066
|
-
yield* d.put(withEntries({
|
|
1067
|
-
...parent,
|
|
1068
|
-
metadata: {
|
|
1069
|
-
...parent.metadata,
|
|
1070
|
-
nlink: parent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
|
|
1071
|
-
mtimeNs: now,
|
|
1072
|
-
ctimeNs: now
|
|
1073
|
-
}
|
|
1074
|
-
}, (entries) => entries.delete(name)));
|
|
1075
|
-
yield* publishEntry("Remove", parent.ino, name, child.ino);
|
|
1076
|
-
yield* detach(child, parent.ino, name, now);
|
|
1077
|
-
yield* d.addEntries(-1);
|
|
1078
|
-
return { before, after: d.revision };
|
|
1079
|
-
});
|
|
1080
|
-
// Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
|
|
1081
|
-
// entry, and each verb reports it with its own code. Search permission comes before the lookup and write
|
|
1082
|
-
// permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
|
|
1083
|
-
const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
|
|
1084
|
-
const parent = yield* directoryNow(entry.parent);
|
|
1085
|
-
if (isDotComponent(entry.name)) {
|
|
1086
|
-
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
|
|
1087
|
-
}
|
|
1088
|
-
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
1089
|
-
const childIno = parent.entries.get(entry.name);
|
|
1090
|
-
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1091
|
-
if (child === undefined)
|
|
1092
|
-
return yield* entry.op.fail("NotFound");
|
|
1093
|
-
const rejected = beforeWrite?.(child);
|
|
1094
|
-
if (rejected !== undefined)
|
|
1095
|
-
return yield* rejected;
|
|
1096
|
-
yield* authorize(parent, identity, WRITE, entry.op);
|
|
1097
|
-
return { parent, name: entry.name, child };
|
|
1098
|
-
});
|
|
1099
|
-
const removeEntry = Effect.fnUntraced(function* (entry, op) {
|
|
1100
|
-
const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument", (found) => entry.trailingSlash && found.kind !== "directory" ? entry.op.fail("NotDirectory") : undefined);
|
|
1101
|
-
yield* authorizeRemoval(parent, child, entry.op);
|
|
1102
|
-
if (child.kind === "directory" && child.entries.size > 0)
|
|
1103
|
-
return yield* entry.op.fail("NotEmpty");
|
|
1104
|
-
return yield* removeChild(parent, name, child, op);
|
|
1105
|
-
});
|
|
1106
|
-
const unlinkEntry = Effect.fnUntraced(function* (entry, op) {
|
|
1107
|
-
const { child, name, parent } = yield* removalTarget(entry, "IsDirectory", (found) => !entry.trailingSlash
|
|
1108
|
-
? undefined
|
|
1109
|
-
: entry.op.fail(found.kind === "directory" ? "IsDirectory" : "NotDirectory"));
|
|
1110
|
-
if (child.kind === "directory")
|
|
1111
|
-
return yield* entry.op.fail("IsDirectory");
|
|
1112
|
-
yield* authorizeRemoval(parent, child, entry.op);
|
|
1113
|
-
return yield* removeChild(parent, name, child, op);
|
|
1114
|
-
});
|
|
1115
|
-
const rmdirEntry = Effect.fnUntraced(function* (entry, op) {
|
|
1116
|
-
const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument");
|
|
1117
|
-
yield* authorizeRemoval(parent, child, entry.op);
|
|
1118
|
-
if (child.kind !== "directory")
|
|
1119
|
-
return yield* entry.op.fail("NotDirectory");
|
|
1120
|
-
if (child.entries.size > 0)
|
|
1121
|
-
return yield* entry.op.fail("NotEmpty");
|
|
1122
|
-
return yield* removeChild(parent, name, child, op);
|
|
1123
|
-
});
|
|
1124
|
-
const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
|
|
1125
|
-
const sourceDirectory = yield* directoryNow(source.parent);
|
|
1126
|
-
const destinationDirectory = yield* directoryNow(destination.parent);
|
|
1127
|
-
const sameDirectory = source.parent === destination.parent;
|
|
1128
|
-
const sourceName = source.name;
|
|
1129
|
-
const destinationName = destination.name;
|
|
1130
|
-
if (isDotComponent(sourceName) || isDotComponent(destinationName)) {
|
|
1131
|
-
return yield* source.op.fail("InvalidArgument");
|
|
1132
|
-
}
|
|
1133
|
-
// Linux's order: search permission on both directories before either name is looked up; the names,
|
|
1134
|
-
// trailing slashes, the subtree rule and the same-object no-op; then write permission and the rest.
|
|
1135
|
-
yield* authorize(sourceDirectory, identity, EXECUTE, source.op);
|
|
1136
|
-
yield* authorize(destinationDirectory, identity, EXECUTE, destination.op);
|
|
1137
|
-
const sourceBefore = sourceDirectory.revision;
|
|
1138
|
-
const destinationBefore = destinationDirectory.revision;
|
|
1139
|
-
const childIno = sourceDirectory.entries.get(sourceName);
|
|
1140
|
-
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1141
|
-
if (child === undefined)
|
|
1142
|
-
return yield* source.op.fail("NotFound");
|
|
1143
|
-
const replacedIno = destinationDirectory.entries.get(destinationName);
|
|
1144
|
-
const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
|
|
1145
|
-
// A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
|
|
1146
|
-
// source is one, as Linux allows.
|
|
1147
|
-
if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
|
|
1148
|
-
return yield* (source.trailingSlash ? source.op : destination.op).fail("NotDirectory");
|
|
1149
|
-
}
|
|
1150
|
-
if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
|
|
1151
|
-
return yield* destination.op.fail("NotDirectory");
|
|
1152
|
-
}
|
|
1153
|
-
const result = Effect.fnUntraced(function* () {
|
|
1154
|
-
return sameDirectory
|
|
1155
|
-
? {
|
|
1156
|
-
_tag: "SameDirectory",
|
|
1157
|
-
directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
|
|
1158
|
-
}
|
|
1159
|
-
: {
|
|
1160
|
-
_tag: "DifferentDirectories",
|
|
1161
|
-
sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
|
|
1162
|
-
destinationDirectory: {
|
|
1163
|
-
before: destinationBefore,
|
|
1164
|
-
after: (yield* directoryNow(destination.parent)).revision
|
|
1165
|
-
}
|
|
1166
|
-
};
|
|
1167
|
-
});
|
|
1168
|
-
for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
|
|
1169
|
-
if (ancestor.ino === child.ino)
|
|
1170
|
-
return yield* destination.op.fail("InvalidArgument");
|
|
1171
|
-
if (ancestor.ino === ROOT_INO)
|
|
1172
|
-
break;
|
|
1173
|
-
}
|
|
1174
|
-
if (child.ino === replaced?.ino)
|
|
1175
|
-
return yield* result();
|
|
1176
|
-
yield* authorize(sourceDirectory, identity, WRITE, source.op);
|
|
1177
|
-
yield* authorize(destinationDirectory, identity, WRITE, destination.op);
|
|
1178
|
-
yield* authorizeRemoval(sourceDirectory, child, source.op);
|
|
1179
|
-
if (replaced !== undefined) {
|
|
1180
|
-
yield* authorizeRemoval(destinationDirectory, replaced, destination.op);
|
|
1181
|
-
if (child.kind === "directory" && replaced.kind !== "directory") {
|
|
1182
|
-
return yield* destination.op.fail("NotDirectory");
|
|
1183
|
-
}
|
|
1184
|
-
if (child.kind !== "directory" && replaced.kind === "directory") {
|
|
1185
|
-
return yield* destination.op.fail("IsDirectory");
|
|
1186
|
-
}
|
|
1187
|
-
if (replaced.kind === "directory" && replaced.entries.size > 0) {
|
|
1188
|
-
return yield* destination.op.fail("NotEmpty");
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
const now = yield* timestamp(op);
|
|
1192
|
-
const d = yield* Draft;
|
|
1193
|
-
yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
|
|
1194
|
-
yield* d.put(withEntries({
|
|
1195
|
-
...sourceDirectory,
|
|
1196
|
-
metadata: {
|
|
1197
|
-
...sourceDirectory.metadata,
|
|
1198
|
-
nlink: sourceDirectory.metadata.nlink - (child.kind === "directory" ? 1 : 0),
|
|
1199
|
-
mtimeNs: now,
|
|
1200
|
-
ctimeNs: now
|
|
1201
|
-
}
|
|
1202
|
-
}, (entries) => entries.delete(sourceName)));
|
|
1203
|
-
const destinationNow = yield* directoryNow(destination.parent);
|
|
1204
|
-
yield* d.put(withEntries({
|
|
1205
|
-
...destinationNow,
|
|
1206
|
-
metadata: {
|
|
1207
|
-
...destinationNow.metadata,
|
|
1208
|
-
nlink: destinationNow.metadata.nlink + (child.kind === "directory" && replaced === undefined ? 1 : 0),
|
|
1209
|
-
mtimeNs: now,
|
|
1210
|
-
ctimeNs: now
|
|
1211
|
-
}
|
|
1212
|
-
}, (entries) => entries.set(destinationName, child.ino)));
|
|
1213
|
-
const moved = yield* nodeNow(child.ino);
|
|
1214
|
-
if (moved.kind === "directory") {
|
|
1215
|
-
yield* d.put({
|
|
1216
|
-
...moved,
|
|
1217
|
-
parent: destination.parent,
|
|
1218
|
-
name: destinationName,
|
|
1219
|
-
metadata: { ...moved.metadata, ctimeNs: now }
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
yield* d.put({
|
|
1224
|
-
...moved,
|
|
1225
|
-
links: [
|
|
1226
|
-
...withoutLink(moved.links, source.parent, sourceName),
|
|
1227
|
-
{ parent: destination.parent, name: destinationName }
|
|
1228
|
-
],
|
|
1229
|
-
metadata: { ...moved.metadata, ctimeNs: now }
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
if (replaced !== undefined) {
|
|
1233
|
-
yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
|
|
1234
|
-
yield* d.addEntries(-1);
|
|
1235
|
-
}
|
|
1236
|
-
yield* publishEntry("Create", destination.parent, destinationName, child.ino);
|
|
1237
|
-
return yield* result();
|
|
1238
|
-
});
|
|
1239
|
-
const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
|
|
1240
|
-
yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
|
|
1241
|
-
if (request.truncate)
|
|
1242
|
-
yield* resize(file, 0n, op);
|
|
1243
|
-
});
|
|
1244
|
-
const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
|
|
1245
|
-
const { name } = entry;
|
|
1246
|
-
const parent = yield* directoryNow(entry.parent);
|
|
1247
|
-
const before = parent.revision;
|
|
1248
|
-
let file = found;
|
|
1249
|
-
let created = false;
|
|
1250
|
-
if (file === undefined) {
|
|
1251
|
-
if (request.create === undefined || request.create === "never")
|
|
1252
|
-
return yield* entry.op.fail("NotFound");
|
|
1253
|
-
yield* authorize(parent, identity, WRITE | EXECUTE, entry.op);
|
|
1254
|
-
yield* reserveEntry(entry.op);
|
|
1255
|
-
const size = request.initialSize ?? 0n;
|
|
1256
|
-
if (request.initialSize !== undefined) {
|
|
1257
|
-
if (size < 0n)
|
|
1258
|
-
return yield* entry.op.fail("InvalidArgument");
|
|
1259
|
-
if (size > BigInt(maxFileBytes))
|
|
1260
|
-
return yield* entry.op.fail("FileTooLarge");
|
|
1261
|
-
yield* reserveBytes(entry.op, size);
|
|
1262
|
-
}
|
|
1263
|
-
const owner = request.owner;
|
|
1264
|
-
if (!identity.privileged &&
|
|
1265
|
-
((owner?.uid !== undefined && owner.uid !== identity.uid) ||
|
|
1266
|
-
(owner?.gid !== undefined && !inGroup(identity, owner.gid)))) {
|
|
1267
|
-
return yield* entry.op.fail("NotPermitted");
|
|
1268
|
-
}
|
|
1269
|
-
const now = yield* timestamp(op);
|
|
1270
|
-
const mode = request.exactMode
|
|
1271
|
-
? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
|
|
1272
|
-
: (request.mode ?? 0o666) & 0o777 & ~umask;
|
|
1273
|
-
file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
|
|
1274
|
-
yield* attach(parent, name, file, now);
|
|
1275
|
-
const draft = yield* Draft;
|
|
1276
|
-
yield* draft.addEntries(1);
|
|
1277
|
-
yield* draft.addUsedBytes(size);
|
|
1278
|
-
created = true;
|
|
1279
|
-
yield* publishEntry("Create", parent.ino, name, file.ino);
|
|
1280
|
-
}
|
|
1281
|
-
else {
|
|
1282
|
-
if (file.kind === "symlink")
|
|
1283
|
-
return yield* entry.op.fail("SymlinkLoop");
|
|
1284
|
-
if (file.kind !== "file")
|
|
1285
|
-
return yield* entry.op.fail("IsDirectory");
|
|
1286
|
-
yield* openExisting(file, request, entry.op, op);
|
|
1287
|
-
}
|
|
1288
|
-
yield* (yield* Draft).retain(file.ino);
|
|
1289
|
-
yield* bindFile(acquired, file.ino);
|
|
1290
|
-
return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
|
|
1291
|
-
});
|
|
1292
|
-
const releasePending = (opened) => () => Effect.gen(function* () {
|
|
1293
|
-
const ino = opened();
|
|
1294
|
-
if (ino === undefined)
|
|
1295
|
-
return;
|
|
1296
|
-
yield* releaseFileInode(ino);
|
|
1297
|
-
});
|
|
1298
|
-
const acquireOpenedFile = (ref, op, acquire) => {
|
|
1299
|
-
let opened;
|
|
1300
|
-
return acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
|
|
1301
|
-
opened = ino;
|
|
1302
|
-
}), releasePending(() => opened), finalizeFile(ref));
|
|
1303
|
-
};
|
|
1304
|
-
const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
|
|
1305
|
-
const asTarget = (input) => Target.of(input);
|
|
1306
|
-
const acquireDirectory = Effect.fnUntraced(function* (input, op) {
|
|
1307
|
-
const target = asTarget(input);
|
|
1308
|
-
const acquired = makeDirectoryReference();
|
|
1309
|
-
return yield* acquireHandle(acquired, (acquire) => coordinatedRead(op, Effect.uninterruptible(acquire)), Effect.gen(function* () {
|
|
1310
|
-
const directory = yield* resolveDirectory(target, op);
|
|
1311
|
-
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
1312
|
-
holdDirectory(directory.node.ino);
|
|
1313
|
-
TokenRegistry.publish(acquired, directory.node.ino);
|
|
1314
|
-
return acquired;
|
|
1315
|
-
}),
|
|
1316
|
-
// Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
|
|
1317
|
-
// every permit, where a detached directory may leave the table.
|
|
1318
|
-
() => Effect.void, finalizeDirectory(acquired));
|
|
1319
|
-
});
|
|
1320
|
-
const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
|
|
1321
|
-
const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
|
|
1322
|
-
const openPath = Effect.fnUntraced(function* (target, options, op) {
|
|
1323
|
-
const pathOp = op.at(target.path);
|
|
1324
|
-
const prepared = prepare(target.path, op);
|
|
1325
|
-
const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(pathOp, cause)));
|
|
1326
|
-
if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
|
|
1327
|
-
return yield* pathOp.fail("InvalidArgument");
|
|
1328
|
-
}
|
|
1329
|
-
if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
1330
|
-
return yield* pathOp.fail("InvalidArgument");
|
|
1331
|
-
}
|
|
1332
|
-
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
1333
|
-
const follow = target.followFinalSymlink ?? true;
|
|
1334
|
-
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1335
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1336
|
-
const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
|
|
1337
|
-
kind: "OrCreate",
|
|
1338
|
-
create: chosen.create ?? "never",
|
|
1339
|
-
finalSymlink: follow ? "follow" : "preserve"
|
|
1340
|
-
}, op);
|
|
1341
|
-
const { parent, name } = resolved;
|
|
1342
|
-
if (resolved.kind === "missing" && path.trailingSlash)
|
|
1343
|
-
return yield* pathOp.fail("IsDirectory");
|
|
1344
|
-
yield* authorize(parent, identity, EXECUTE, pathOp);
|
|
1345
|
-
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1346
|
-
const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
|
|
1347
|
-
opened(result.ino);
|
|
1348
|
-
return fileHandle(acquired);
|
|
1349
|
-
}));
|
|
1350
|
-
});
|
|
1351
|
-
const openNode = Effect.fnUntraced(function* (target, options, op) {
|
|
1352
|
-
const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
1353
|
-
if (chosen.access === "read" && (chosen.append || chosen.truncate))
|
|
1354
|
-
return yield* op.fail("InvalidArgument");
|
|
1355
|
-
if ((chosen.create !== undefined && chosen.create !== "never") || chosen.mode !== undefined) {
|
|
1356
|
-
return yield* op.fail("InvalidArgument");
|
|
1357
|
-
}
|
|
1358
|
-
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
1359
|
-
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1360
|
-
const resolved = yield* resolveTarget(target, op);
|
|
1361
|
-
const node = resolved.node;
|
|
1362
|
-
if (node.kind !== "file")
|
|
1363
|
-
return yield* resolved.op.fail("IsDirectory");
|
|
1364
|
-
if (node.metadata.nlink === 0)
|
|
1365
|
-
return yield* resolved.op.fail("StaleReference");
|
|
1366
|
-
yield* openExisting(node, chosen, resolved.op, op);
|
|
1367
|
-
const draft = yield* Draft;
|
|
1368
|
-
yield* draft.retain(node.ino);
|
|
1369
|
-
yield* bindFile(acquired, node.ino);
|
|
1370
|
-
opened(node.ino);
|
|
1371
|
-
return fileHandle(acquired);
|
|
1372
|
-
}));
|
|
1373
|
-
});
|
|
1374
|
-
const openEntry = Effect.fnUntraced(function* (entry, options, op) {
|
|
1375
|
-
const name = yield* Effect.fromResult(entryName(entry.name, op));
|
|
1376
|
-
const decoded = yield* decodeOpenEntryOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
1377
|
-
const chosen = { ...decoded };
|
|
1378
|
-
if (chosen.access === "read" && (chosen.append || chosen.truncate))
|
|
1379
|
-
return yield* op.fail("InvalidArgument");
|
|
1380
|
-
if ((chosen.mode !== undefined || chosen.times !== undefined || chosen.initialSize !== undefined ||
|
|
1381
|
-
chosen.exactMode !== undefined || chosen.owner !== undefined) &&
|
|
1382
|
-
(chosen.create === undefined || chosen.create === "never")) {
|
|
1383
|
-
return yield* op.fail("InvalidArgument");
|
|
1384
|
-
}
|
|
1385
|
-
if (chosen.exactMode && chosen.mode === undefined)
|
|
1386
|
-
return yield* op.fail("InvalidArgument");
|
|
1387
|
-
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
1388
|
-
const expected = chosen.expected;
|
|
1389
|
-
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1390
|
-
const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
|
|
1391
|
-
kind: "OrCreate",
|
|
1392
|
-
create: chosen.create ?? "never",
|
|
1393
|
-
finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
|
|
1394
|
-
beforeFollow: Effect.fnUntraced(function* (_, direct) {
|
|
1395
|
-
if (expected !== undefined) {
|
|
1396
|
-
let expectedIno;
|
|
1397
|
-
if (expected !== null) {
|
|
1398
|
-
const observed = yield* Effect.result(referencedNode(expected, op));
|
|
1399
|
-
if (Result.isFailure(observed)) {
|
|
1400
|
-
return yield* op.fail("VolumeBusy", { cause: observed.failure });
|
|
1401
|
-
}
|
|
1402
|
-
expectedIno = observed.success.ino;
|
|
1403
|
-
}
|
|
1404
|
-
if (direct?.ino !== expectedIno)
|
|
1405
|
-
return yield* op.fail("VolumeBusy");
|
|
1406
|
-
}
|
|
1407
|
-
if (chosen.expectedChild === null) {
|
|
1408
|
-
if (direct !== undefined)
|
|
1409
|
-
return yield* op.fail("StaleReference");
|
|
1410
|
-
}
|
|
1411
|
-
else if (chosen.expectedChild !== undefined) {
|
|
1412
|
-
const expectedChild = chosen.expectedChild;
|
|
1413
|
-
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
1414
|
-
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
1415
|
-
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
1416
|
-
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
1417
|
-
return yield* op.fail("StaleReference");
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
})
|
|
1421
|
-
}, op);
|
|
1422
|
-
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1423
|
-
const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
|
|
1424
|
-
const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
|
|
1425
|
-
const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
|
|
1426
|
-
opened(result.ino);
|
|
1427
|
-
return {
|
|
1428
|
-
handle: fileHandle(acquired),
|
|
1429
|
-
reference: referenceFor(result.ino),
|
|
1430
|
-
created: result.created,
|
|
1431
|
-
directory: result.directory
|
|
1432
|
-
};
|
|
1433
|
-
}));
|
|
1434
|
-
});
|
|
1435
|
-
const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
|
|
1436
|
-
const listed = referenceFor(directory.ino);
|
|
1437
|
-
const frames = [];
|
|
1438
|
-
for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
|
|
1439
|
-
const child = yield* view(childIno);
|
|
1440
|
-
if (child === undefined)
|
|
1441
|
-
continue;
|
|
1442
|
-
const bytes = nameBytes(name);
|
|
1443
|
-
frames.push({
|
|
1444
|
-
ino: child.ino,
|
|
1445
|
-
kind: child.kind,
|
|
1446
|
-
name: bytes,
|
|
1447
|
-
key: name,
|
|
1448
|
-
path: joinPath(path, bytes),
|
|
1449
|
-
parent: directory.ino,
|
|
1450
|
-
up,
|
|
1451
|
-
depth: depth + 1,
|
|
1452
|
-
bytes: child.kind === "directory" ? 0n : child.metadata.size,
|
|
1453
|
-
reference: referenceFor(child.ino),
|
|
1454
|
-
directory: listed,
|
|
1455
|
-
listed: false
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
return frames;
|
|
1459
|
-
});
|
|
1460
|
-
const anchorFrame = (parent, key, root) => ({
|
|
1461
|
-
ino: root.ino,
|
|
1462
|
-
kind: root.kind,
|
|
1463
|
-
name: nameBytes(key),
|
|
1464
|
-
key,
|
|
1465
|
-
path: new Uint8Array(0),
|
|
1466
|
-
parent: parent.ino,
|
|
1467
|
-
up: undefined,
|
|
1468
|
-
depth: 0,
|
|
1469
|
-
bytes: 0n,
|
|
1470
|
-
reference: referenceFor(root.ino),
|
|
1471
|
-
directory: referenceFor(parent.ino),
|
|
1472
|
-
listed: false
|
|
1473
|
-
});
|
|
1474
|
-
const pathAnchor = (target, root, op) => {
|
|
1475
|
-
if (!Target.$is("Path")(target))
|
|
1476
|
-
return Effect.undefined;
|
|
1477
|
-
const prepared = prepare(target.path, op);
|
|
1478
|
-
if (Result.isFailure(prepared))
|
|
1479
|
-
return Effect.undefined;
|
|
1480
|
-
return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
|
|
1481
|
-
return entry.name !== undefined && parent?.kind === "directory" &&
|
|
1482
|
-
parent.entries.get(entry.name) === root.ino
|
|
1483
|
-
? anchorFrame(parent, entry.name, root)
|
|
1484
|
-
: undefined;
|
|
1485
|
-
})), Effect.catch(() => Effect.undefined));
|
|
1486
|
-
};
|
|
1487
|
-
const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
|
|
1488
|
-
// The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
|
|
1489
|
-
// the way must be searchable and must still hold the name the walk listed for the object it listed. A frame
|
|
1490
|
-
// whose chain no longer holds it, such as a directory renamed out of the tree, reaches nothing. A root the
|
|
1491
|
-
// walk reached by name is anchored by that name, so a root moved away reaches nothing too; a root reached
|
|
1492
|
-
// through a reference or a handle is held by the object it resolved to, as a descriptor holds it.
|
|
1493
|
-
// It steps down the chain in a loop, so a tree of any depth reaches its frames without deepening the stack.
|
|
1494
|
-
const reachFrame = (frame, at) => Effect.gen(function* () {
|
|
1495
|
-
const chain = [];
|
|
1496
|
-
for (let link = frame; link !== undefined; link = link.up)
|
|
1497
|
-
chain.push(link);
|
|
1498
|
-
let node = yield* view((chain.at(-1) ?? frame).parent);
|
|
1499
|
-
for (let index = chain.length - 1; index >= 0; index--) {
|
|
1500
|
-
const link = chain[index];
|
|
1501
|
-
if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
1502
|
-
return undefined;
|
|
1503
|
-
yield* authorize(node, identity, EXECUTE, at);
|
|
1504
|
-
node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
|
|
1505
|
-
}
|
|
1506
|
-
return node;
|
|
1507
|
-
});
|
|
1508
|
-
// Walks the tree below the directory `first` lists, depth first. Every later directory is read in its own
|
|
1509
|
-
// observation, so a walk holds one permit at a time and never a handle; it writes nothing, so it refreshes no
|
|
1510
|
-
// access time. Each directory is reached by name, so it needs search permission on the directories above it,
|
|
1511
|
-
// and one that left the tree after it was listed has nothing to walk. A directory past `maxDepth` is never
|
|
1512
|
-
// read. `locate` names an entry's path in a failure, and `listable` authorizes reading a directory. Entries
|
|
1513
|
-
// gathered before a failure are handed on before the failure is.
|
|
1514
|
-
const walkFrames = (op, first, plan, locate, listable) => Stream.suspend(() => {
|
|
1515
|
-
let pending;
|
|
1516
|
-
let entries = 0;
|
|
1517
|
-
let bytes = 0n;
|
|
1518
|
-
let failure;
|
|
1519
|
-
const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
|
|
1520
|
-
if (TokenRegistry.inode(reference) === undefined)
|
|
1521
|
-
return Effect.fail(op.fail("ClosedCaller"));
|
|
1522
|
-
const at = op.at(locate(frame.path));
|
|
1523
|
-
return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
|
|
1524
|
-
? Effect.succeed([])
|
|
1525
|
-
: Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
|
|
1526
|
-
}));
|
|
1527
|
-
const admit = (frame) => {
|
|
1528
|
-
entries++;
|
|
1529
|
-
bytes += frame.bytes;
|
|
1530
|
-
return plan.limit(frame, { entries, bytes }, "entry");
|
|
1531
|
-
};
|
|
1532
|
-
const step = Effect.gen(function* () {
|
|
1533
|
-
// Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
|
|
1534
|
-
// oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
|
|
1535
|
-
if (failure !== undefined)
|
|
1536
|
-
return yield* Effect.fail(failure);
|
|
1537
|
-
if (pending === undefined)
|
|
1538
|
-
pending = (yield* first).reverse();
|
|
1539
|
-
const out = [];
|
|
1540
|
-
const stop = (error) => {
|
|
1541
|
-
if (out.length === 0)
|
|
1542
|
-
return Effect.fail(error);
|
|
1543
|
-
failure = error;
|
|
1544
|
-
return Effect.succeed([out, Option.some(undefined)]);
|
|
1545
|
-
};
|
|
1546
|
-
while (out.length < WALK_CHUNK_ENTRIES) {
|
|
1547
|
-
const frame = pending.pop();
|
|
1548
|
-
if (frame === undefined)
|
|
1549
|
-
return [out, Option.none()];
|
|
1550
|
-
const unlisted = frame.kind === "directory" && !frame.listed;
|
|
1551
|
-
if (!unlisted || plan.order === "pre") {
|
|
1552
|
-
const rejected = admit(frame);
|
|
1553
|
-
if (rejected !== undefined)
|
|
1554
|
-
return yield* stop(rejected);
|
|
1555
|
-
out.push(frame);
|
|
1556
|
-
}
|
|
1557
|
-
if (unlisted) {
|
|
1558
|
-
const rejected = plan.limit(frame, { entries, bytes }, "directory");
|
|
1559
|
-
if (rejected !== undefined)
|
|
1560
|
-
return yield* stop(rejected);
|
|
1561
|
-
const listed = yield* Effect.result(list(frame));
|
|
1562
|
-
if (Result.isFailure(listed))
|
|
1563
|
-
return yield* stop(listed.failure);
|
|
1564
|
-
if (plan.order === "post")
|
|
1565
|
-
pending.push({ ...frame, listed: true });
|
|
1566
|
-
for (let index = listed.success.length - 1; index >= 0; index--) {
|
|
1567
|
-
const child = listed.success[index];
|
|
1568
|
-
if (child !== undefined)
|
|
1569
|
-
pending.push(child);
|
|
1570
|
-
}
|
|
1571
|
-
return [out, Option.some(undefined)];
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
return [out, Option.some(undefined)];
|
|
1575
|
-
});
|
|
1576
|
-
return Stream.paginate(undefined, () => step);
|
|
1577
|
-
});
|
|
1578
|
-
// Empties the directory an entry names, entries before their directories, each removal its own change. Each
|
|
1579
|
-
// entry is removed by its name in the directory the walk reached by name from the target's name, and only
|
|
1580
|
-
// while that name still holds the object the walk listed, so a subtree renamed out of the target, the target
|
|
1581
|
-
// renamed away, and a replacement created under a listed name are all left alone. It stops at the first
|
|
1582
|
-
// 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
|
|
1583
|
-
// no permission is changed to make one so. With `force`, the entry itself going missing leaves nothing to
|
|
1584
|
-
// empty.
|
|
1585
|
-
const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force) {
|
|
1586
|
-
const prefix = prepared.kind === "path" ? prepared.path.bytes : nameBytes(prepared.name);
|
|
1587
|
-
const locate = (path) => ownedPath(joinPath(prefix, path));
|
|
1588
|
-
const listable = (directory, at) => directory.entries.size === 0 ? Effect.void : authorize(directory, identity, READ, at);
|
|
1589
|
-
let anchor;
|
|
1590
|
-
let targetAt = preparedOp(prepared, op);
|
|
1591
|
-
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
1592
|
-
const entry = yield* resolveEntry(prepared, op);
|
|
1593
|
-
const parent = yield* directoryNow(entry.parent);
|
|
1594
|
-
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
1595
|
-
const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
|
|
1596
|
-
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1597
|
-
if (entry.name === undefined || child === undefined)
|
|
1598
|
-
return yield* entry.op.fail("NotFound");
|
|
1599
|
-
if (child.kind !== "directory")
|
|
1600
|
-
return [];
|
|
1601
|
-
yield* listable(child, entry.op);
|
|
1602
|
-
anchor = anchorFrame(parent, entry.name, child);
|
|
1603
|
-
targetAt = entry.op;
|
|
1604
|
-
return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
|
|
1605
|
-
})).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
|
|
202
|
+
const wrapContext = (context) => Object.freeze({
|
|
203
|
+
...context.operations,
|
|
204
|
+
[CallerId]: true,
|
|
205
|
+
limits,
|
|
206
|
+
watch: Effect.fnUntraced(function* (input, options) {
|
|
207
|
+
const op = OpContext.make("watch");
|
|
208
|
+
const decoded = yield* Effect.fromResult(decodeConfiguration(CallerWatchOptions, options ?? {}, "watch"));
|
|
1606
209
|
const seams = yield* VolumeTestSeams;
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
name: frame.key,
|
|
1618
|
-
trailingSlash: false,
|
|
1619
|
-
addressing: "entry",
|
|
1620
|
-
op: at
|
|
1621
|
-
}, op));
|
|
1622
|
-
})));
|
|
1623
|
-
};
|
|
1624
|
-
const plan = { order: "post", limit: () => undefined };
|
|
1625
|
-
return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
|
|
1626
|
-
});
|
|
1627
|
-
const rootOp = OpContext.make("root");
|
|
1628
|
-
const entryVerb = Effect.fnUntraced(function* (operation, input, body) {
|
|
1629
|
-
const op = OpContext.make(operation);
|
|
1630
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1631
|
-
return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
|
|
1632
|
-
});
|
|
1633
|
-
const openDispatch = Effect.fn("Caller.open")(function* (input, options) {
|
|
1634
|
-
const op = OpContext.make("open");
|
|
1635
|
-
if (isEntry(input))
|
|
1636
|
-
return yield* openEntry(input, options, op);
|
|
1637
|
-
const target = asTarget(input);
|
|
1638
|
-
if (Target.$is("Path")(target))
|
|
1639
|
-
return yield* openPath(target, options, op);
|
|
1640
|
-
return yield* openNode(target, options, op);
|
|
1641
|
-
});
|
|
1642
|
-
function open(input, options) {
|
|
1643
|
-
return openDispatch(input, options);
|
|
1644
|
-
}
|
|
1645
|
-
const removeDispatch = Effect.fn("Caller.remove")(function* (input, options) {
|
|
1646
|
-
const op = OpContext.make("remove");
|
|
1647
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1648
|
-
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
1649
|
-
const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
|
|
1650
|
-
const target = (effect) => chosen.force === true
|
|
1651
|
-
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
1652
|
-
: effect;
|
|
1653
|
-
const removed = yield* Effect.result(target(once));
|
|
1654
|
-
if (Result.isSuccess(removed))
|
|
1655
|
-
return removed.success;
|
|
1656
|
-
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
1657
|
-
return yield* removed.failure;
|
|
1658
|
-
yield* emptyDirectory(prepared, op, chosen.force === true);
|
|
1659
|
-
return yield* target(once);
|
|
1660
|
-
});
|
|
1661
|
-
function remove(input, options) {
|
|
1662
|
-
return removeDispatch(input, options);
|
|
1663
|
-
}
|
|
1664
|
-
const caller = Object.freeze({
|
|
1665
|
-
[CallerId]: true,
|
|
1666
|
-
root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
|
|
1667
|
-
? Effect.fail(rootOp.fail("ClosedCaller"))
|
|
1668
|
-
: Effect.succeed(referenceFor(ROOT_INO)))).pipe(Effect.withSpan("Caller.root")),
|
|
1669
|
-
lookup: Effect.fn("Caller.lookup")(function* (input) {
|
|
1670
|
-
const op = OpContext.make("lookup");
|
|
1671
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1672
|
-
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
1673
|
-
const entry = yield* resolveEntry(prepared, op);
|
|
1674
|
-
const directory = yield* directoryNow(entry.parent);
|
|
1675
|
-
if (isDotComponent(entry.name)) {
|
|
1676
|
-
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
|
|
1677
|
-
}
|
|
1678
|
-
yield* authorize(directory, identity, EXECUTE, entry.op);
|
|
1679
|
-
const child = directory.entries.get(entry.name);
|
|
1680
|
-
if (child === undefined)
|
|
1681
|
-
return yield* entry.op.fail("NotFound");
|
|
1682
|
-
return referenceFor(child);
|
|
1683
|
-
}));
|
|
1684
|
-
}),
|
|
1685
|
-
parent: Effect.fn("Caller.parent")(function* (input) {
|
|
1686
|
-
const op = OpContext.make("parent");
|
|
1687
|
-
const target = asTarget(input);
|
|
1688
|
-
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
1689
|
-
const directory = yield* resolveDirectory(target, op);
|
|
1690
|
-
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
1691
|
-
return referenceFor(directory.node.parent);
|
|
1692
|
-
}));
|
|
1693
|
-
}),
|
|
1694
|
-
stat: Effect.fn("Caller.stat")(function* (input) {
|
|
1695
|
-
const op = OpContext.make("stat");
|
|
1696
|
-
const target = asTarget(input);
|
|
1697
|
-
return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => withMetadata(resolved.node)));
|
|
1698
|
-
}),
|
|
1699
|
-
readDirectory: Effect.fn("Caller.readDirectory")(function* (input) {
|
|
1700
|
-
const op = OpContext.make("readDirectory");
|
|
1701
|
-
const target = asTarget(input);
|
|
1702
|
-
return yield* accessing(op, Effect.gen(function* () {
|
|
1703
|
-
const directory = yield* resolveDirectory(target, op);
|
|
1704
|
-
yield* authorize(directory.node, identity, READ, directory.op);
|
|
1705
|
-
const value = Object.freeze([...directory.node.entries].map(([name, child]) => Object.freeze({ name: nameBytes(name), reference: referenceFor(child) })));
|
|
1706
|
-
const access = { node: directory.node, now: yield* timestamp(op) };
|
|
1707
|
-
return { value: Object.freeze({ value, revision: directory.node.revision }), access };
|
|
1708
|
-
}));
|
|
1709
|
-
}),
|
|
1710
|
-
walk: (input, options) => {
|
|
1711
|
-
const op = OpContext.make("walk");
|
|
1712
|
-
const target = asTarget(input);
|
|
1713
|
-
const prefix = Target.$is("Path")(target) ? Result.getOrUndefined(inputBytes(target.path)) : undefined;
|
|
1714
|
-
const locate = (path) => ownedPath(prefix === undefined ? path : joinPath(prefix, path));
|
|
1715
|
-
const readable = (directory, at) => authorize(directory, identity, READ, at);
|
|
1716
|
-
return Stream.unwrap(Effect.gen(function* () {
|
|
1717
|
-
const chosen = yield* decodeWalkOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(Target.$is("Path")(target) ? op.at(target.path) : op, cause)));
|
|
1718
|
-
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
1719
|
-
const directory = yield* resolveDirectory(target, op);
|
|
1720
|
-
yield* authorize(directory.node, identity, READ, directory.op);
|
|
1721
|
-
return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
1722
|
-
}));
|
|
1723
|
-
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1724
|
-
const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
|
|
1725
|
-
const plan = {
|
|
1726
|
-
order: chosen.order ?? "pre",
|
|
1727
|
-
limit: (frame, progress, phase) => {
|
|
1728
|
-
if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
|
|
1729
|
-
return exceeded(frame, "maxDepth");
|
|
1730
|
-
if (phase === "directory")
|
|
1731
|
-
return undefined;
|
|
1732
|
-
if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
|
|
1733
|
-
return exceeded(frame, "maxEntries");
|
|
1734
|
-
}
|
|
1735
|
-
return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
|
|
1736
|
-
}
|
|
1737
|
-
};
|
|
1738
|
-
return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
|
|
1739
|
-
path: ownedPath(frame.path),
|
|
1740
|
-
name: frame.name,
|
|
1741
|
-
reference: frame.reference,
|
|
1742
|
-
directory: frame.directory,
|
|
1743
|
-
kind: frame.kind,
|
|
1744
|
-
depth: frame.depth
|
|
1745
|
-
}));
|
|
1746
|
-
})).pipe(Stream.withSpan("Caller.walk"));
|
|
1747
|
-
},
|
|
1748
|
-
readLink: Effect.fn("Caller.readLink")(function* (input) {
|
|
1749
|
-
const op = OpContext.make("readLink");
|
|
1750
|
-
const target = asTarget(input);
|
|
1751
|
-
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
1752
|
-
const resolved = yield* resolveTarget(target, op, { followFinalSymlink: false, final: true });
|
|
1753
|
-
if (resolved.node.kind !== "symlink")
|
|
1754
|
-
return yield* resolved.op.fail("InvalidArgument");
|
|
1755
|
-
return new Uint8Array(resolved.node.target);
|
|
1756
|
-
}));
|
|
1757
|
-
}),
|
|
1758
|
-
realPath: Effect.fn("Caller.realPath")(function* (input) {
|
|
1759
|
-
const op = OpContext.make("realPath");
|
|
1760
|
-
const target = asTarget(input);
|
|
1761
|
-
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
1762
|
-
if (Target.$is("Path")(target)) {
|
|
1763
|
-
const pathOp = op.at(target.path);
|
|
1764
|
-
const result = yield* resolveTarget(target, op, { final: true });
|
|
1765
|
-
const directory = result.node.kind === "directory"
|
|
1766
|
-
? result.node
|
|
1767
|
-
: result.kind === "entry"
|
|
1768
|
-
? result.parent
|
|
1769
|
-
: undefined;
|
|
1770
|
-
const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
|
|
1771
|
-
if (prefix === undefined)
|
|
1772
|
-
return yield* pathOp.fail("NotFound");
|
|
1773
|
-
if (result.node.kind === "directory" || result.kind === "node")
|
|
1774
|
-
return ownedPath(nameBytes(prefix));
|
|
1775
|
-
return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
|
|
1776
|
-
}
|
|
1777
|
-
const resolved = yield* resolveTarget(target, op);
|
|
1778
|
-
const path = yield* pathOfCurrent(resolved.node.ino);
|
|
1779
|
-
if (path === undefined)
|
|
1780
|
-
return yield* resolved.op.fail("NotFound");
|
|
1781
|
-
return ownedPath(nameBytes(path));
|
|
1782
|
-
}));
|
|
1783
|
-
}),
|
|
1784
|
-
access: Effect.fn("Caller.access")(function* (input, bits = 0) {
|
|
1785
|
-
const op = OpContext.make("access");
|
|
1786
|
-
const target = asTarget(input);
|
|
1787
|
-
if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
|
|
1788
|
-
return yield* (Target.$is("Path")(target) ? op.at(target.path) : op).fail("InvalidArgument");
|
|
1789
|
-
}
|
|
1790
|
-
return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => {
|
|
1791
|
-
const node = resolved.node;
|
|
1792
|
-
let granted = 0;
|
|
1793
|
-
for (const bit of [READ, WRITE, EXECUTE]) {
|
|
1794
|
-
if ((bits & bit) === 0)
|
|
1795
|
-
continue;
|
|
1796
|
-
if (bit === EXECUTE && node.kind === "file" && (node.metadata.mode & ANY_EXECUTE) === 0)
|
|
1797
|
-
continue;
|
|
1798
|
-
if (identity.privileged || (permitted(node, identity) & bit) !== 0)
|
|
1799
|
-
granted |= bit;
|
|
1800
|
-
}
|
|
1801
|
-
return granted;
|
|
1802
|
-
}));
|
|
1803
|
-
}),
|
|
1804
|
-
readFile: Effect.fn("Caller.readFile")(function* (input) {
|
|
1805
|
-
const op = OpContext.make("readFile");
|
|
1806
|
-
const target = asTarget(input);
|
|
1807
|
-
return yield* accessing(op, Effect.gen(function* () {
|
|
1808
|
-
const resolved = yield* resolveTarget(target, op);
|
|
1809
|
-
const node = resolved.node;
|
|
1810
|
-
if (node.kind === "symlink")
|
|
1811
|
-
return yield* resolved.op.fail("SymlinkLoop");
|
|
1812
|
-
if (node.kind !== "file")
|
|
1813
|
-
return yield* resolved.op.fail("IsDirectory");
|
|
1814
|
-
yield* authorize(node, identity, READ, resolved.op);
|
|
1815
|
-
const data = new Uint8Array(node.data);
|
|
1816
|
-
return { value: data, access: { node, now: yield* timestamp(op) } };
|
|
1817
|
-
}));
|
|
1818
|
-
}),
|
|
1819
|
-
writeFile: Effect.fn("Caller.writeFile")(function* (input, bytes, options) {
|
|
1820
|
-
const op = OpContext.make("writeFile");
|
|
1821
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1822
|
-
const optionsOp = preparedOp(prepared, op);
|
|
1823
|
-
if (!isAttachedBytes(bytes))
|
|
1824
|
-
return yield* optionsOp.fail("InvalidArgument");
|
|
1825
|
-
const captured = new Uint8Array(bytes);
|
|
1826
|
-
const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
1827
|
-
return yield* coordinated(op, Effect.gen(function* () {
|
|
1828
|
-
const resolved = yield* resolver.resolve(prepared, {
|
|
1829
|
-
kind: "OrCreate",
|
|
1830
|
-
create: chosen.create ?? "never",
|
|
1831
|
-
finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
|
|
1832
|
-
? "follow"
|
|
1833
|
-
: "preserve",
|
|
1834
|
-
entryExclusive: "afterSymlink"
|
|
1835
|
-
}, op);
|
|
1836
|
-
const { parent, name, trailingSlash, op: entryOp } = resolved;
|
|
1837
|
-
const found = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1838
|
-
if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
|
|
1839
|
-
return yield* entryOp.fail("IsDirectory");
|
|
1840
|
-
}
|
|
1841
|
-
const replaced = found?.kind === "symlink" ? found : undefined;
|
|
1842
|
-
if (replaced !== undefined && !chosen.replaceFinalSymlink)
|
|
1843
|
-
return yield* entryOp.fail("SymlinkLoop");
|
|
1844
|
-
if (chosen.access === "read")
|
|
1845
|
-
return yield* entryOp.fail("InvalidHandle");
|
|
1846
|
-
const file = found?.kind === "file" ? found : undefined;
|
|
1847
|
-
if (file === undefined) {
|
|
1848
|
-
yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
|
|
1849
|
-
if (replaced !== undefined)
|
|
1850
|
-
yield* authorizeRemoval(parent, replaced, entryOp);
|
|
1851
|
-
yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
|
|
1852
|
-
}
|
|
1853
|
-
else {
|
|
1854
|
-
yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
|
|
1855
|
-
}
|
|
1856
|
-
const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, entryOp);
|
|
1857
|
-
const previous = file?.data.length ?? 0;
|
|
1858
|
-
const initial = chosen.truncate ? 0 : previous;
|
|
1859
|
-
const position = chosen.append ? initial : 0;
|
|
1860
|
-
const size = Math.max(initial, position + captured.length);
|
|
1861
|
-
if (size > maxFileBytes)
|
|
1862
|
-
return yield* entryOp.fail("FileTooLarge");
|
|
1863
|
-
const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
|
|
1864
|
-
yield* reserveBytes(entryOp, BigInt(size - previous) - BigInt(reclaimed));
|
|
1865
|
-
if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
|
|
1866
|
-
return;
|
|
1867
|
-
}
|
|
1868
|
-
let data = captured;
|
|
1869
|
-
if (position !== 0 || size !== captured.length) {
|
|
1870
|
-
data = new Uint8Array(size);
|
|
1871
|
-
if (file !== undefined && !chosen.truncate)
|
|
1872
|
-
data.set(file.data);
|
|
1873
|
-
data.set(captured, position);
|
|
1874
|
-
}
|
|
1875
|
-
const now = yield* timestamp(op);
|
|
1876
|
-
const d = yield* Draft;
|
|
1877
|
-
const node = file ??
|
|
1878
|
-
(yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
|
|
1879
|
-
const written = {
|
|
1880
|
-
...node,
|
|
1881
|
-
data,
|
|
1882
|
-
metadata: {
|
|
1883
|
-
...node.metadata,
|
|
1884
|
-
mode: finalMode ?? node.metadata.mode & ~SET_ID_BITS,
|
|
1885
|
-
size: BigInt(size),
|
|
1886
|
-
mtimeNs: now,
|
|
1887
|
-
ctimeNs: now
|
|
1888
|
-
}
|
|
1889
|
-
};
|
|
1890
|
-
yield* d.addUsedBytes(BigInt(size - previous));
|
|
1891
|
-
if (file === undefined) {
|
|
1892
|
-
if (replaced !== undefined)
|
|
1893
|
-
yield* detach(replaced, parent.ino, name, now);
|
|
1894
|
-
yield* attach(yield* directoryNow(parent.ino), name, written, now);
|
|
1895
|
-
if (replaced === undefined)
|
|
1896
|
-
yield* d.addEntries(1);
|
|
1897
|
-
yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
|
|
1898
|
-
}
|
|
1899
|
-
else {
|
|
1900
|
-
yield* d.put(written);
|
|
1901
|
-
yield* publishNode(written.ino);
|
|
1902
|
-
}
|
|
1903
|
-
}));
|
|
1904
|
-
}),
|
|
1905
|
-
open,
|
|
1906
|
-
mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
|
|
1907
|
-
const op = OpContext.make("mkdir");
|
|
1908
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1909
|
-
const optionsOp = preparedOp(prepared, op);
|
|
1910
|
-
const decoded = yield* decodeMkdirOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
1911
|
-
const chosen = { ...decoded };
|
|
1912
|
-
if (chosen.exactMode && chosen.mode === undefined)
|
|
1913
|
-
return yield* optionsOp.fail("InvalidArgument");
|
|
1914
|
-
return yield* coordinated(op, Effect.gen(function* () {
|
|
1915
|
-
const request = { mode: chosen.mode ?? 0o777, exactMode: chosen.exactMode, times: chosen.times };
|
|
1916
|
-
const { child, directory } = chosen.recursive === true
|
|
1917
|
-
? yield* makeDirectories(prepared, request, op)
|
|
1918
|
-
: yield* makeDirectory(yield* resolveEntry(prepared, op), request, op);
|
|
1919
|
-
return { reference: referenceFor(child), directory };
|
|
1920
|
-
}));
|
|
1921
|
-
}),
|
|
1922
|
-
symlink: Effect.fn("Caller.symlink")(function* (target, input, options = {}) {
|
|
1923
|
-
const op = OpContext.make("symlink");
|
|
1924
|
-
const targetOp = op.at(target);
|
|
1925
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1926
|
-
const optionsOp = preparedOp(prepared, op);
|
|
1927
|
-
const decoded = yield* decodeSymlinkOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
1928
|
-
const rawTarget = inputBytes(target);
|
|
1929
|
-
if (Result.isFailure(rawTarget))
|
|
1930
|
-
return yield* targetOp.fail(rawTarget.failure);
|
|
1931
|
-
if (rawTarget.success.includes(0))
|
|
1932
|
-
return yield* targetOp.fail("InvalidArgument");
|
|
1933
|
-
const targetBytes = new Uint8Array(rawTarget.success);
|
|
1934
|
-
const chosen = { ...decoded };
|
|
1935
|
-
return yield* coordinated(op, Effect.gen(function* () {
|
|
1936
|
-
const entry = yield* resolveEntry(prepared, op);
|
|
1937
|
-
const { child, directory } = yield* makeSymlink(entry, targetBytes, chosen.times, op);
|
|
1938
|
-
return { reference: referenceFor(child), directory };
|
|
1939
|
-
}));
|
|
1940
|
-
}),
|
|
1941
|
-
link: Effect.fn("Caller.link")(function* (sourceInput, input) {
|
|
1942
|
-
const op = OpContext.make("link");
|
|
1943
|
-
const source = asTarget(sourceInput);
|
|
1944
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1945
|
-
return yield* coordinated(op, Effect.gen(function* () {
|
|
1946
|
-
const resolved = yield* resolveTarget(source, op, { followFinalSymlink: false });
|
|
1947
|
-
const node = resolved.node;
|
|
1948
|
-
if (node.kind === "directory")
|
|
1949
|
-
return yield* resolved.op.fail("IsDirectory");
|
|
1950
|
-
if (node.metadata.nlink === 0)
|
|
1951
|
-
return yield* resolved.op.fail("StaleReference");
|
|
1952
|
-
const entry = yield* resolveEntry(prepared, op);
|
|
1953
|
-
const directory = yield* linkNode(node, entry, op);
|
|
1954
|
-
return { reference: referenceFor(node.ino), directory };
|
|
1955
|
-
}));
|
|
1956
|
-
}),
|
|
1957
|
-
unlink: Effect.fn("Caller.unlink")(function* (input) {
|
|
1958
|
-
return yield* entryVerb("unlink", input, unlinkEntry);
|
|
1959
|
-
}),
|
|
1960
|
-
rmdir: Effect.fn("Caller.rmdir")(function* (input) {
|
|
1961
|
-
return yield* entryVerb("rmdir", input, rmdirEntry);
|
|
1962
|
-
}),
|
|
1963
|
-
remove,
|
|
1964
|
-
rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
|
|
1965
|
-
const op = OpContext.make("rename");
|
|
1966
|
-
const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
|
|
1967
|
-
const to = yield* Effect.fromResult(prepareEntry(toInput, op));
|
|
1968
|
-
return yield* coordinated(op, Effect.gen(function* () {
|
|
1969
|
-
const source = yield* resolveEntry(from, op);
|
|
1970
|
-
const destination = yield* resolveEntry(to, op);
|
|
1971
|
-
return yield* renameEntry(source, destination, op);
|
|
1972
|
-
}));
|
|
1973
|
-
}),
|
|
1974
|
-
chmod: Effect.fn("Caller.chmod")(function* (input, mode) {
|
|
1975
|
-
const op = OpContext.make("chmod");
|
|
1976
|
-
const target = asTarget(input);
|
|
1977
|
-
if (!isMode(mode))
|
|
1978
|
-
return yield* op.fail("InvalidArgument");
|
|
1979
|
-
yield* changeAttributes(() => asResolvedNode(target, op), { mode }, op);
|
|
1980
|
-
}),
|
|
1981
|
-
chown: Effect.fn("Caller.chown")(function* (input, owner) {
|
|
1982
|
-
const op = OpContext.make("chown");
|
|
1983
|
-
const target = asTarget(input);
|
|
1984
|
-
const decoded = yield* decodeOwnerUpdate(owner).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
1985
|
-
yield* changeAttributes(() => asResolvedNode(target, op), { owner: decoded }, op);
|
|
1986
|
-
}),
|
|
1987
|
-
utimes: Effect.fn("Caller.utimes")(function* (input, times) {
|
|
1988
|
-
const op = OpContext.make("utimes");
|
|
1989
|
-
const target = asTarget(input);
|
|
1990
|
-
const decoded = yield* decodeTimes(times).pipe(Effect.mapError((cause) => fail(op, cause)));
|
|
1991
|
-
yield* changeAttributes(() => asResolvedNode(target, op), { times: decoded }, op);
|
|
1992
|
-
}),
|
|
1993
|
-
// A negative length fails before the target resolves, as truncate(2) rejects it before the lookup.
|
|
1994
|
-
truncate: Effect.fn("Caller.truncate")(function* (input, length) {
|
|
1995
|
-
const op = OpContext.make("truncate");
|
|
1996
|
-
const target = asTarget(input);
|
|
1997
|
-
if (!isLength(length))
|
|
1998
|
-
return yield* op.fail("InvalidArgument");
|
|
1999
|
-
yield* changeAttributes(() => asResolvedNode(target, op), { size: length }, op);
|
|
2000
|
-
}),
|
|
2001
|
-
setattr: Effect.fn("Caller.setattr")(function* (input, attributes) {
|
|
2002
|
-
const op = OpContext.make("setattr");
|
|
2003
|
-
const target = asTarget(input);
|
|
2004
|
-
// A caller outside TypeScript can pass anything, so a non-object fails typed rather than as a defect;
|
|
2005
|
-
// the check reads a widened copy so the attributes keep their type below.
|
|
2006
|
-
const raw = attributes;
|
|
2007
|
-
if (!Predicate.isObject(raw))
|
|
2008
|
-
return yield* fail(op);
|
|
2009
|
-
const unknown = Object.keys(attributes).find((key) => !SETATTR_FIELDS.includes(key));
|
|
2010
|
-
if (unknown !== undefined)
|
|
2011
|
-
return yield* fail(op, undefined, unknown);
|
|
2012
|
-
if (attributes.size !== undefined && !isLength(attributes.size))
|
|
2013
|
-
return yield* fail(op, undefined, "size");
|
|
2014
|
-
if (attributes.mode !== undefined && !isMode(attributes.mode))
|
|
2015
|
-
return yield* fail(op, undefined, "mode");
|
|
2016
|
-
const owner = attributes.owner === undefined
|
|
2017
|
-
? undefined
|
|
2018
|
-
: yield* decodeOwnerUpdate(attributes.owner).pipe(Effect.mapError((cause) => fail(op, cause, "owner")));
|
|
2019
|
-
const times = attributes.times === undefined
|
|
2020
|
-
? undefined
|
|
2021
|
-
: yield* decodeTimes(attributes.times).pipe(Effect.mapError((cause) => fail(op, cause, "times")));
|
|
2022
|
-
const expected = attributes.expected === undefined
|
|
2023
|
-
? undefined
|
|
2024
|
-
: yield* decodeExpected(attributes.expected).pipe(Effect.mapError((cause) => fail(op, cause, "expected")));
|
|
2025
|
-
yield* changeAttributes(() => asResolvedNode(target, op), { size: attributes.size, mode: attributes.mode, owner, times, expected }, op);
|
|
2026
|
-
}),
|
|
2027
|
-
withDirectory: Effect.fn("Caller.withDirectory")(function* (input) {
|
|
2028
|
-
const acquired = yield* acquireDirectory(input, OpContext.make("withDirectory"));
|
|
2029
|
-
return createCaller(acquired, identity, umask);
|
|
2030
|
-
}),
|
|
2031
|
-
openDirectory: Effect.fn("Caller.openDirectory")(function* (input) {
|
|
2032
|
-
const acquired = yield* acquireDirectory(input, OpContext.make("openDirectory"));
|
|
2033
|
-
const statOp = OpContext.make("stat");
|
|
2034
|
-
const handle = Object.freeze({
|
|
2035
|
-
[DirectoryHandleId]: true,
|
|
2036
|
-
stat: coordinatedRead(statOp, Effect.gen(function* () {
|
|
2037
|
-
const ino = TokenRegistry.inode(acquired);
|
|
2038
|
-
const node = ino === undefined ? undefined : yield* view(ino);
|
|
2039
|
-
return yield* (node === undefined
|
|
2040
|
-
? Effect.fail(statOp.fail("InvalidHandle"))
|
|
2041
|
-
: Effect.succeed(withMetadata(node)));
|
|
2042
|
-
})).pipe(Effect.withSpan("DirectoryHandle.stat")),
|
|
2043
|
-
close: coordinatedCleanup(Effect.suspend(() => {
|
|
2044
|
-
if (TokenRegistry.inode(acquired) === undefined) {
|
|
2045
|
-
return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
|
|
2046
|
-
}
|
|
2047
|
-
return releaseDirectory(acquired);
|
|
2048
|
-
})).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
|
|
2049
|
-
});
|
|
2050
|
-
registry.registerDirectory(handle, acquired);
|
|
2051
|
-
return handle;
|
|
2052
|
-
})
|
|
2053
|
-
});
|
|
2054
|
-
return caller;
|
|
2055
|
-
};
|
|
210
|
+
const select = Effect.map(context.selectWatch(input, decoded), CallerWatch.make);
|
|
211
|
+
return Stream.map(yield* engine.admit(op, watchHub.subscribe(select, seams.afterSubscribe)), (event) => event.change);
|
|
212
|
+
}),
|
|
213
|
+
withRoot: Effect.fnUntraced(function* (input) {
|
|
214
|
+
return wrapContext(yield* context.withRoot(input));
|
|
215
|
+
}),
|
|
216
|
+
withDirectory: Effect.fnUntraced(function* (input) {
|
|
217
|
+
return wrapContext(yield* context.withDirectory(input));
|
|
218
|
+
})
|
|
219
|
+
});
|
|
2056
220
|
const volume = Object.freeze({
|
|
2057
221
|
[VolumeId]: true,
|
|
2058
222
|
durability,
|
|
2059
223
|
identity,
|
|
2060
224
|
incarnation,
|
|
2061
225
|
limits,
|
|
2062
|
-
usage:
|
|
2063
|
-
|
|
2064
|
-
watch: Effect.fn("Volume.watch")(function* (options) {
|
|
226
|
+
usage: engine.usage,
|
|
227
|
+
watch: Effect.fnUntraced(function* (options) {
|
|
2065
228
|
const op = OpContext.make("watch");
|
|
2066
229
|
const decoded = yield* Effect.fromResult(decodeConfiguration(WatchOptions, options === undefined ? {} : options, "watch"));
|
|
2067
230
|
const recursive = decoded.recursive ?? true;
|
|
@@ -2069,53 +232,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
|
|
|
2069
232
|
const seams = yield* VolumeTestSeams;
|
|
2070
233
|
// The scope is resolved while the registration holds the volume, so no change lands between the check and
|
|
2071
234
|
// the subscription.
|
|
2072
|
-
const select = Effect.andThen(checkAvailable("watch"), scope === undefined
|
|
235
|
+
const select = Effect.andThen(engine.checkAvailable("watch"), scope === undefined
|
|
2073
236
|
? Effect.succeed(recursive ? volumeSelection : scopedSelection(ROOT_INO, false))
|
|
2074
237
|
: Effect.map(scopeRoot(scope, op), (root) => scopedSelection(root, recursive)));
|
|
2075
|
-
const stream = yield* admit(op, watchHub.subscribe(select, seams.afterSubscribe));
|
|
238
|
+
const stream = yield* engine.admit(op, watchHub.subscribe(select, seams.afterSubscribe));
|
|
2076
239
|
return Stream.map(stream, (event) => event.change);
|
|
2077
240
|
}),
|
|
2078
|
-
snapshot:
|
|
2079
|
-
referenceKey: Effect.
|
|
241
|
+
snapshot: capture("snapshot", (state) => Effect.succeed(Image.make(state))),
|
|
242
|
+
referenceKey: Effect.fnUntraced(function* (reference) {
|
|
2080
243
|
const op = OpContext.make("referenceKey");
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
const ino = BigInt(node.ino);
|
|
2084
|
-
return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
|
|
2085
|
-
}));
|
|
244
|
+
const ino = BigInt(yield* engine.referenceInode(reference, op));
|
|
245
|
+
return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
|
|
2086
246
|
}),
|
|
2087
|
-
resolveReferenceKey:
|
|
247
|
+
resolveReferenceKey: (key) => {
|
|
2088
248
|
const op = OpContext.make("resolveReferenceKey");
|
|
2089
|
-
return
|
|
249
|
+
return engine.objectFor(Effect.gen(function* () {
|
|
2090
250
|
if (!Schema.is(ReferenceKey)(key))
|
|
2091
251
|
return yield* op.fail("InvalidReference");
|
|
2092
|
-
// Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
|
|
2093
252
|
if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
|
|
2094
253
|
return yield* op.fail("ForeignReference");
|
|
2095
254
|
}
|
|
2096
|
-
// Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
|
|
2097
255
|
if (!sameTag(key.tag, keyTag(key.ino)))
|
|
2098
256
|
return yield* op.fail("InvalidReference");
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
caller: Effect.fn("Volume.caller")(function* (options) {
|
|
2105
|
-
const decoded = yield* Effect.fromResult(decodeConfiguration(RootCallerOptions, options === undefined ? {} : options, "caller"));
|
|
2106
|
-
const chosen = decoded.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
|
|
2107
|
-
const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
|
|
2108
|
-
return yield* coordinatedRead(OpContext.make("caller"), Effect.sync(() => createCaller(makeDirectoryReference(ROOT_INO), identity, decoded.umask ?? 0o022)));
|
|
257
|
+
return Ino(Number(key.ino));
|
|
258
|
+
}), op);
|
|
259
|
+
},
|
|
260
|
+
caller: Effect.fnUntraced(function* (options) {
|
|
261
|
+
return wrapContext(yield* engine.context(options));
|
|
2109
262
|
})
|
|
2110
263
|
});
|
|
2111
264
|
const observe = Object.freeze({
|
|
2112
|
-
changes:
|
|
2113
|
-
capture:
|
|
265
|
+
changes: capture("changes", observeChanges),
|
|
266
|
+
capture: captureState
|
|
2114
267
|
});
|
|
2115
|
-
|
|
268
|
+
AdapterBinding.register(volume, engine);
|
|
269
|
+
return Object.freeze({ volume, shutdown: engine.shutdown, observe });
|
|
2116
270
|
});
|
|
2117
271
|
/** @internal */
|
|
2118
|
-
export const make = Effect.
|
|
272
|
+
export const make = Effect.fnUntraced(function* (options) {
|
|
2119
273
|
return (yield* makeVolume(VolumeSource.Empty(), memoryCommitProvider, options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
|
|
2120
274
|
});
|
|
2121
275
|
/** @internal */
|
|
@@ -2132,6 +286,9 @@ export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
|
|
|
2132
286
|
});
|
|
2133
287
|
/** @internal */
|
|
2134
288
|
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only", options) {
|
|
289
|
+
const decoded = yield* Effect.fromResult(Limits.configuration(options, "openImage"));
|
|
290
|
+
if (decoded.options.execution === "sync")
|
|
291
|
+
return yield* argumentFailure("openImage", "execution");
|
|
2135
292
|
const restored = yield* LiveImage.decode(image, maxImageBytes);
|
|
2136
293
|
const requested = yield* Effect.fromResult(Limits.fromOptions(options, "openImage"));
|
|
2137
294
|
const limits = yield* Effect.fromResult(Limits.fromStored(restored.limits, requested));
|
|
@@ -2154,22 +311,22 @@ const changeOptions = (options) => {
|
|
|
2154
311
|
return Effect.fromResult(decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options, "changes"));
|
|
2155
312
|
};
|
|
2156
313
|
/** @internal */
|
|
2157
|
-
export const fromSnapshot = Effect.
|
|
314
|
+
export const fromSnapshot = Effect.fnUntraced(function* (snapshot, options) {
|
|
2158
315
|
return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), memoryCommitProvider, options)).volume;
|
|
2159
316
|
}, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
|
|
2160
317
|
/** @internal */
|
|
2161
|
-
export const makeOverlay = Effect.
|
|
318
|
+
export const makeOverlay = Effect.fnUntraced(function* (base, options) {
|
|
2162
319
|
const value = yield* Image.valueOf(base);
|
|
2163
320
|
const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), memoryCommitProvider, options), (error) => retargetFailure("makeOverlay", error));
|
|
2164
321
|
const baseObservation = yield* observeChanges(value);
|
|
2165
322
|
const overlay = Object.freeze({
|
|
2166
323
|
...made.volume,
|
|
2167
|
-
changes: Effect.
|
|
324
|
+
changes: Effect.fnUntraced(function* (options) {
|
|
2168
325
|
const selected = yield* changeOptions(options);
|
|
2169
326
|
const current = yield* made.observe.changes;
|
|
2170
327
|
return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
|
|
2171
328
|
}),
|
|
2172
|
-
capture: Effect.
|
|
329
|
+
capture: Effect.fnUntraced(function* (options) {
|
|
2173
330
|
const selected = yield* changeOptions(options);
|
|
2174
331
|
const current = yield* made.observe.capture;
|
|
2175
332
|
return Object.freeze({
|
|
@@ -2178,5 +335,6 @@ export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function*
|
|
|
2178
335
|
});
|
|
2179
336
|
})
|
|
2180
337
|
});
|
|
338
|
+
AdapterBinding.alias(overlay, made.volume);
|
|
2181
339
|
return overlay;
|
|
2182
340
|
});
|