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