@effect-vfs/core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -238
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +9 -9
- package/dist/Search.d.ts +730 -0
- package/dist/Search.d.ts.map +1 -0
- package/dist/Search.js +568 -0
- package/dist/SnapshotDelta.d.ts +234 -0
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +107 -0
- package/dist/VirtualFileSystem.d.ts +53 -1
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +59 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/internal/canonicalBase64.js +3 -3
- package/dist/internal/fixture.js +3 -3
- package/dist/internal/glob.d.ts +66 -0
- package/dist/internal/glob.d.ts.map +1 -0
- package/dist/internal/glob.js +248 -0
- package/dist/internal/image.d.ts +7 -1
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +9 -7
- package/dist/internal/liveImage.d.ts +1 -1
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +2 -10
- package/dist/internal/overlayDiff.js +2 -2
- package/dist/internal/path.d.ts.map +1 -1
- package/dist/internal/path.js +3 -3
- package/dist/internal/resolution.d.ts +135 -0
- package/dist/internal/resolution.d.ts.map +1 -0
- package/dist/internal/resolution.js +277 -0
- package/dist/internal/search.d.ts +24 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +78 -0
- package/dist/internal/searchContent.d.ts +77 -0
- package/dist/internal/searchContent.d.ts.map +1 -0
- package/dist/internal/searchContent.js +223 -0
- package/dist/internal/searchModel.d.ts +83 -0
- package/dist/internal/searchModel.d.ts.map +1 -0
- package/dist/internal/searchModel.js +23 -0
- package/dist/internal/snapshotDelta.d.ts +470 -1
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +60 -26
- package/dist/internal/snapshotMerge.d.ts +54 -0
- package/dist/internal/snapshotMerge.d.ts.map +1 -0
- package/dist/internal/snapshotMerge.js +959 -0
- package/dist/internal/tokenRegistry.d.ts +77 -0
- package/dist/internal/tokenRegistry.d.ts.map +1 -0
- package/dist/internal/tokenRegistry.js +128 -0
- package/dist/internal/tree.js +2 -2
- package/dist/internal/virtualFileSystem.d.ts +32 -17
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +640 -993
- package/dist/internal/volumeLimits.d.ts +53 -0
- package/dist/internal/volumeLimits.d.ts.map +1 -0
- package/dist/internal/volumeLimits.js +63 -0
- package/dist/internal/volumeState.d.ts +2 -0
- package/dist/internal/volumeState.d.ts.map +1 -1
- package/dist/internal/volumeState.js +9 -0
- package/package.json +10 -6
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import * as Arr from "effect/Array";
|
|
2
2
|
import * as ByteSize from "effect/ByteSize";
|
|
3
|
+
import * as Cause from "effect/Cause";
|
|
3
4
|
import * as Clock from "effect/Clock";
|
|
5
|
+
import * as Context from "effect/Context";
|
|
4
6
|
import * as Crypto from "effect/Crypto";
|
|
5
7
|
import * as Data from "effect/Data";
|
|
6
8
|
import * as Effect from "effect/Effect";
|
|
7
|
-
import * as
|
|
9
|
+
import * as Hex from "effect/encoding/Hex";
|
|
8
10
|
import * as Exit from "effect/Exit";
|
|
9
11
|
import * as Match from "effect/Match";
|
|
12
|
+
import * as MutableRef from "effect/MutableRef";
|
|
10
13
|
import * as Option from "effect/Option";
|
|
11
14
|
import * as Predicate from "effect/Predicate";
|
|
12
15
|
import * as Result from "effect/Result";
|
|
13
16
|
import * as Schema from "effect/Schema";
|
|
14
|
-
import * as Scope from "effect/Scope";
|
|
15
17
|
import * as Semaphore from "effect/Semaphore";
|
|
16
18
|
import * as Stream from "effect/Stream";
|
|
17
19
|
import { BytePath } from "../BytePath.js";
|
|
18
|
-
import { CallerId, MkdirOptions,
|
|
20
|
+
import { CallerId, MkdirOptions, OpenEntryOptions, OpenOptions, RemoveOptions, RootCallerOptions, SetattrOptions, SymlinkOptions, WalkOptions, WriteFileOptions } from "../Caller.js";
|
|
19
21
|
import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
|
|
20
22
|
import { Mode, OwnerUpdate, Times } from "../Metadata.js";
|
|
21
|
-
import { isEntry,
|
|
23
|
+
import { isEntry, Target } from "../Target.js";
|
|
22
24
|
import { make as makeError } from "../VfsError.js";
|
|
23
|
-
import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation
|
|
25
|
+
import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
|
|
24
26
|
import { WatchOptions } from "../Watch.js";
|
|
25
27
|
import { sameBytes } from "./bytes.js";
|
|
26
28
|
import { argumentFailure, decodeConfiguration, errorPath, fsFailure, imageFailure, OpContext, retargetFailure, VfsError } from "./errors.js";
|
|
@@ -31,10 +33,13 @@ import * as InodeTable from "./inodeTable.js";
|
|
|
31
33
|
import * as LiveImage from "./liveImage.js";
|
|
32
34
|
import * as MetadataDomain from "./metadata.js";
|
|
33
35
|
import { compareOverlay } from "./overlayDiff.js";
|
|
34
|
-
import {
|
|
36
|
+
import { inputBytes, isAttachedBytes, isDotComponent, joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
|
|
37
|
+
import * as Resolution from "./resolution.js";
|
|
35
38
|
import { VolumeTestSeams } from "./testSeams.js";
|
|
39
|
+
import * as TokenRegistry from "./tokenRegistry.js";
|
|
36
40
|
import { makeTurnstile } from "./turnstile.js";
|
|
37
|
-
import
|
|
41
|
+
import * as Limits from "./volumeLimits.js";
|
|
42
|
+
import { byEntryName, directoryMetadata, emptyState, getNode, Ino, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
38
43
|
import * as WatchHub from "./watchHub.js";
|
|
39
44
|
const EXECUTE = 0o1;
|
|
40
45
|
const WRITE = 0o2;
|
|
@@ -43,11 +48,23 @@ const ANY_EXECUTE = 0o111;
|
|
|
43
48
|
const SET_ID_BITS = 0o6000;
|
|
44
49
|
const STICKY_BIT = 0o1000;
|
|
45
50
|
const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
|
|
51
|
+
/** @internal */
|
|
52
|
+
export const memoryCommitProvider = Object.freeze({
|
|
53
|
+
mode: "memory",
|
|
54
|
+
commit: () => Effect.succeed("committed"),
|
|
55
|
+
shutdown: Effect.void
|
|
56
|
+
});
|
|
46
57
|
// An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
|
|
47
58
|
const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
|
|
48
59
|
// TODO(#186): Preserve the cause of a provider defect or interruption.
|
|
49
60
|
if (Exit.isFailure(committed)) {
|
|
50
|
-
|
|
61
|
+
if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
|
|
62
|
+
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
63
|
+
}
|
|
64
|
+
const failure = Cause.findErrorOption(committed.cause);
|
|
65
|
+
return Option.isSome(failure)
|
|
66
|
+
? { available: !cleanup, failure: failure.value }
|
|
67
|
+
: { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
51
68
|
}
|
|
52
69
|
if (committed.value === "rejected") {
|
|
53
70
|
return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
|
|
@@ -62,7 +79,7 @@ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
|
|
|
62
79
|
const isMode = Schema.is(Mode);
|
|
63
80
|
const isNatural = Schema.is(Schema.Natural);
|
|
64
81
|
const isLength = Schema.is(SetattrOptions.fields.size.schema);
|
|
65
|
-
const inGroup =
|
|
82
|
+
const inGroup = Resolution.inGroup;
|
|
66
83
|
const isTimestamp = Schema.is(MetadataDomain.Timestamp);
|
|
67
84
|
const isSeekMode = Schema.is(SeekMode);
|
|
68
85
|
const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
|
|
@@ -77,112 +94,122 @@ const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessPrope
|
|
|
77
94
|
const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
|
|
78
95
|
const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
|
|
79
96
|
const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
|
|
80
|
-
const encoder = new TextEncoder();
|
|
81
97
|
// Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
|
|
82
98
|
// status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
|
|
83
99
|
// skips it, so reads at one instant (a frozen or coarse clock) store nothing.
|
|
84
100
|
const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
|
|
85
101
|
now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
|
|
86
102
|
const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
103
|
+
class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
|
|
104
|
+
make: Effect.fnUntraced(function* (base) {
|
|
105
|
+
const revision = base.revision + 1n;
|
|
106
|
+
const cell = MutableRef.make({
|
|
107
|
+
owner: Symbol(),
|
|
108
|
+
pending: new Map(),
|
|
109
|
+
opens: undefined,
|
|
110
|
+
nextInode: base.nextInode,
|
|
111
|
+
changed: false,
|
|
112
|
+
finished: undefined,
|
|
113
|
+
entries: base.entries,
|
|
114
|
+
usedBytes: base.usedBytes,
|
|
115
|
+
events: [],
|
|
116
|
+
after: [],
|
|
117
|
+
removed: []
|
|
118
|
+
});
|
|
119
|
+
const get = (ino) => Effect.sync(() => {
|
|
120
|
+
const draft = MutableRef.get(cell);
|
|
121
|
+
return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
|
|
122
|
+
});
|
|
123
|
+
const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
|
|
124
|
+
const put = (node, changed) => Effect.sync(() => {
|
|
125
|
+
MutableRef.update(cell, (draft) => {
|
|
126
|
+
// This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
|
|
127
|
+
draft.pending.set(node.ino, changed ? { ...node, revision } : node);
|
|
128
|
+
return { ...draft, changed: draft.changed || changed };
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
revision,
|
|
133
|
+
get,
|
|
134
|
+
put: (node) => put(node, true),
|
|
135
|
+
putQuiet: (node) => put(node, false),
|
|
136
|
+
unchanged: Effect.sync(() => {
|
|
137
|
+
const draft = MutableRef.get(cell);
|
|
138
|
+
return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
|
|
139
|
+
draft.entries === base.entries && draft.usedBytes === base.usedBytes;
|
|
140
|
+
}),
|
|
141
|
+
remove: (ino) => Effect.sync(() => {
|
|
142
|
+
const draft = MutableRef.get(cell);
|
|
143
|
+
draft.pending.set(ino, undefined);
|
|
144
|
+
draft.removed.push(ino);
|
|
145
|
+
}),
|
|
146
|
+
allocate: Effect.sync(() => {
|
|
147
|
+
const next = MutableRef.get(cell).nextInode;
|
|
148
|
+
MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
|
|
149
|
+
return next;
|
|
150
|
+
}),
|
|
151
|
+
canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
|
|
152
|
+
openCount,
|
|
153
|
+
retain: (ino) => Effect.sync(() => {
|
|
154
|
+
const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
|
|
155
|
+
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
|
|
156
|
+
}),
|
|
157
|
+
release: (ino) => Effect.sync(() => {
|
|
158
|
+
const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
|
|
159
|
+
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
|
|
160
|
+
return count;
|
|
161
|
+
}),
|
|
162
|
+
entries: Effect.sync(() => MutableRef.get(cell).entries),
|
|
163
|
+
addEntries: (amount) => Effect.sync(() => {
|
|
164
|
+
MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
|
|
165
|
+
}),
|
|
166
|
+
usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
|
|
167
|
+
addUsedBytes: (amount) => Effect.sync(() => {
|
|
168
|
+
MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
|
|
169
|
+
}),
|
|
170
|
+
publish: (event) => Effect.sync(() => {
|
|
171
|
+
MutableRef.get(cell).events.push(event);
|
|
172
|
+
}),
|
|
173
|
+
afterInstall: (action) => Effect.sync(() => {
|
|
174
|
+
MutableRef.get(cell).after.push(action);
|
|
175
|
+
}),
|
|
176
|
+
finish: Effect.sync(() => {
|
|
177
|
+
const draft = MutableRef.get(cell);
|
|
178
|
+
if (draft.finished !== undefined)
|
|
179
|
+
return draft.finished;
|
|
180
|
+
let inodes = base.inodes;
|
|
181
|
+
for (const [ino, node] of draft.pending)
|
|
182
|
+
inodes = InodeTable.set(inodes, ino, node, draft.owner);
|
|
183
|
+
let open = base.open;
|
|
184
|
+
if (draft.opens !== undefined) {
|
|
185
|
+
const next = new Map(base.open);
|
|
186
|
+
for (const [ino, count] of draft.opens) {
|
|
187
|
+
if (count > 0)
|
|
188
|
+
next.set(ino, count);
|
|
189
|
+
else
|
|
190
|
+
next.delete(ino);
|
|
191
|
+
}
|
|
192
|
+
open = next;
|
|
193
|
+
}
|
|
194
|
+
const finished = {
|
|
195
|
+
inodes,
|
|
196
|
+
open,
|
|
197
|
+
nextInode: draft.nextInode,
|
|
198
|
+
revision: draft.changed ? revision : base.revision,
|
|
199
|
+
entries: draft.entries,
|
|
200
|
+
usedBytes: draft.usedBytes
|
|
201
|
+
};
|
|
202
|
+
MutableRef.update(cell, (current) => ({ ...current, finished }));
|
|
203
|
+
return finished;
|
|
204
|
+
}),
|
|
205
|
+
actions: Effect.sync(() => {
|
|
206
|
+
const { events, after, removed } = MutableRef.get(cell);
|
|
207
|
+
return { events, after, removed };
|
|
208
|
+
})
|
|
178
209
|
};
|
|
179
|
-
|
|
180
|
-
|
|
210
|
+
})
|
|
211
|
+
}) {
|
|
181
212
|
}
|
|
182
|
-
const objectReferences = new WeakMap();
|
|
183
|
-
const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
|
|
184
|
-
const files = new WeakMap();
|
|
185
|
-
const handles = new WeakMap();
|
|
186
213
|
const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
|
|
187
214
|
const withEntries = (directory, edit) => {
|
|
188
215
|
const entries = new Map(directory.entries);
|
|
@@ -227,38 +254,26 @@ const observeChanges = Effect.fnUntraced(function* (captured) {
|
|
|
227
254
|
});
|
|
228
255
|
const randomHex128 = Effect.gen(function* () {
|
|
229
256
|
const crypto = yield* Crypto.Crypto;
|
|
230
|
-
return
|
|
257
|
+
return Hex.encode(yield* crypto.randomBytes(16));
|
|
231
258
|
}).pipe(Effect.orDie);
|
|
232
259
|
/** @internal */
|
|
233
|
-
export const makeVolume = Effect.fnUntraced(function* (source,
|
|
260
|
+
export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, options, durability = "memory-only") {
|
|
234
261
|
const services = yield* Effect.context();
|
|
235
262
|
// These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
|
|
236
263
|
const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
|
|
237
264
|
const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
recovered.maxEntries = live.limits.maxEntries;
|
|
243
|
-
if (live.limits.maxBytes !== undefined)
|
|
244
|
-
recovered.maxBytes = ByteSize.bytes(live.limits.maxBytes);
|
|
245
|
-
if (live.limits.maxFileBytes !== undefined)
|
|
246
|
-
recovered.maxFileBytes = ByteSize.bytes(live.limits.maxFileBytes);
|
|
247
|
-
if (live.limits.maxPathBytes !== undefined)
|
|
248
|
-
recovered.maxPathBytes = ByteSize.bytes(live.limits.maxPathBytes);
|
|
249
|
-
restoredOptions = recovered;
|
|
250
|
-
}
|
|
251
|
-
const decoded = yield* Effect.fromResult(decodeConfiguration(VolumeOptions, restoredOptions === undefined ? {} : restoredOptions, "make"));
|
|
252
|
-
const settings = { ...decoded };
|
|
253
|
-
const identity = settings.identity === undefined
|
|
265
|
+
const decoded = yield* Effect.fromResult(Limits.configuration(options));
|
|
266
|
+
const requested = decoded.limits;
|
|
267
|
+
const limits = live === undefined ? requested : yield* Effect.fromResult(Limits.fromStored(live.limits, requested));
|
|
268
|
+
const identity = live?.identity ?? (decoded.identity === undefined
|
|
254
269
|
? VolumeIdentity.make(yield* randomHex128)
|
|
255
|
-
:
|
|
270
|
+
: decoded.identity);
|
|
256
271
|
const incarnation = VolumeIncarnation.make(yield* randomHex128);
|
|
257
272
|
const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
|
|
258
273
|
const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
|
|
259
|
-
const identityBytes = Result.getOrThrow(
|
|
260
|
-
const epochBytes = Result.getOrThrow(
|
|
261
|
-
const keySecretBytes = Result.getOrThrow(
|
|
274
|
+
const identityBytes = Result.getOrThrow(Hex.decode(identity));
|
|
275
|
+
const epochBytes = Result.getOrThrow(Hex.decode(epoch));
|
|
276
|
+
const keySecretBytes = Result.getOrThrow(Hex.decode(keySecret));
|
|
262
277
|
const clock = yield* Clock.clockWith(Effect.succeed);
|
|
263
278
|
const initialTime = clock.currentTimeNanosUnsafe();
|
|
264
279
|
if (!isTimestamp(initialTime)) {
|
|
@@ -271,24 +286,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
271
286
|
: Effect.fail(op.fail("InvalidArgument"));
|
|
272
287
|
});
|
|
273
288
|
const volumeIdentity = Symbol();
|
|
274
|
-
const tokens = new Map();
|
|
275
289
|
const directoryHolds = new Map();
|
|
276
|
-
const
|
|
277
|
-
volume: volumeIdentity,
|
|
278
|
-
scope: undefined,
|
|
279
|
-
closed: false,
|
|
280
|
-
ino: undefined,
|
|
281
|
-
offset: 0n,
|
|
282
|
-
access,
|
|
283
|
-
append
|
|
284
|
-
});
|
|
285
|
-
const makeDirectoryReference = (ino) => ({
|
|
286
|
-
volume: volumeIdentity,
|
|
287
|
-
scope: undefined,
|
|
288
|
-
closed: false,
|
|
289
|
-
ino
|
|
290
|
-
});
|
|
291
|
-
const maxPendingOperations = settings.maxPendingOperations ?? 64;
|
|
290
|
+
const maxPendingOperations = limits.maxPendingOperations;
|
|
292
291
|
const permits = maxPendingOperations + 1;
|
|
293
292
|
const gate = Semaphore.makeUnsafe(permits);
|
|
294
293
|
// The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
|
|
@@ -305,125 +304,92 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
305
304
|
onNone: () => op.fail("VolumeBusy"),
|
|
306
305
|
onSome: Effect.succeed
|
|
307
306
|
})));
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
inodes: InodeTable.set(InodeTable.empty(), ROOT_INO, root),
|
|
311
|
-
open: new Map(),
|
|
312
|
-
nextInode: Ino(2),
|
|
313
|
-
revision: 1n,
|
|
314
|
-
entries: 0,
|
|
315
|
-
usedBytes: 0n
|
|
316
|
-
};
|
|
317
|
-
const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(MAX_FILE_BYTES)));
|
|
318
|
-
const limits = Object.freeze({
|
|
319
|
-
maxBytes: settings.maxBytes,
|
|
320
|
-
maxFileBytes: ByteSize.bytes(maxFileBytes),
|
|
321
|
-
maxEntries: settings.maxEntries,
|
|
322
|
-
maxPathBytes: settings.maxPathBytes,
|
|
323
|
-
maxPendingOperations: settings.maxPendingOperations ?? 64,
|
|
324
|
-
maxWatchEvents: settings.maxWatchEvents ?? 256
|
|
325
|
-
});
|
|
307
|
+
let state = emptyState(initialTime);
|
|
308
|
+
const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
|
|
326
309
|
if (Predicate.isTagged("Restored")(source)) {
|
|
327
310
|
const restored = yield* reachableValue(source.value);
|
|
328
311
|
if (restored.largestFile > maxFileBytes) {
|
|
329
312
|
return yield* imageFailure("snapshot", "LimitExceeded", { field: "maxFileBytes" });
|
|
330
313
|
}
|
|
331
|
-
if ((
|
|
332
|
-
(
|
|
314
|
+
if ((limits.maxEntries !== undefined && restored.state.entries > limits.maxEntries) ||
|
|
315
|
+
(limits.maxBytes !== undefined && restored.state.usedBytes > ByteSize.toBigInt(limits.maxBytes))) {
|
|
333
316
|
return yield* imageFailure("snapshot", "LimitExceeded", { field: "volume" });
|
|
334
317
|
}
|
|
335
318
|
state = restored.state;
|
|
336
319
|
}
|
|
337
320
|
if (live !== undefined)
|
|
338
321
|
state = live.value;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
322
|
+
const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
|
|
323
|
+
const registry = TokenRegistry.make(volumeIdentity, view);
|
|
324
|
+
const referenceFor = registry.referenceFor;
|
|
325
|
+
const makeFileReference = registry.file;
|
|
326
|
+
const makeDirectoryReference = registry.directory;
|
|
344
327
|
let available = true;
|
|
345
328
|
const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
|
|
346
329
|
const watchCoordinate = (effect) => changing(effect);
|
|
347
|
-
const watchHub = yield* WatchHub.make(watchCoordinate,
|
|
348
|
-
const install = (finished)
|
|
330
|
+
const watchHub = yield* WatchHub.make(watchCoordinate, limits.maxWatchEvents);
|
|
331
|
+
const install = Effect.fnUntraced(function* (finished) {
|
|
349
332
|
const before = state;
|
|
350
|
-
state = finished.finish
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
333
|
+
state = yield* finished.finish;
|
|
334
|
+
const actions = yield* finished.actions;
|
|
335
|
+
for (const ino of actions.removed)
|
|
336
|
+
registry.forget(ino);
|
|
337
|
+
for (const apply of actions.after)
|
|
354
338
|
apply();
|
|
355
339
|
const after = state;
|
|
356
|
-
watchHub.publishUnsafe(() =>
|
|
357
|
-
};
|
|
358
|
-
const transition = (change) => Effect.suspend(() => {
|
|
359
|
-
const current = new Draft(state);
|
|
360
|
-
draft = current;
|
|
361
|
-
return Effect.onExit(change, () => Effect.sync(() => {
|
|
362
|
-
draft = undefined;
|
|
363
|
-
})).pipe(Effect.map((value) => [value, current]));
|
|
340
|
+
watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), { before, after });
|
|
364
341
|
});
|
|
365
|
-
const
|
|
366
|
-
const current =
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Offers the finished draft's value to the store before installing it. A rejected candidate is discarded and
|
|
377
|
-
// the volume stays available; an uncertain answer stops the volume. A change that is a cleanup runs its
|
|
378
|
-
// release even when the store refuses, since the handle must not stay open.
|
|
379
|
-
const committed = Effect.fnUntraced(function* (op, provider, finished, onStorageFailure) {
|
|
380
|
-
if (finished.unchanged)
|
|
381
|
-
return install(finished);
|
|
382
|
-
const next = finished.finish();
|
|
383
|
-
if (provider.prepare !== undefined)
|
|
384
|
-
yield* provider.prepare(next);
|
|
385
|
-
const answer = yield* offerCommit(provider, op.operation, next, onStorageFailure !== undefined);
|
|
342
|
+
const transition = Effect.fnUntraced(function* (change) {
|
|
343
|
+
const current = yield* Draft.make(state);
|
|
344
|
+
const value = yield* Effect.provideService(change, Draft, current);
|
|
345
|
+
return [value, current];
|
|
346
|
+
});
|
|
347
|
+
// Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
|
|
348
|
+
const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
|
|
349
|
+
if ((yield* finished.unchanged) || (cleanup && !available))
|
|
350
|
+
return yield* install(finished);
|
|
351
|
+
const next = yield* finished.finish;
|
|
352
|
+
const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
|
|
386
353
|
if (!answer.available)
|
|
387
354
|
available = false;
|
|
388
355
|
if (answer.failure !== undefined) {
|
|
389
|
-
if (
|
|
390
|
-
|
|
356
|
+
if (cleanup)
|
|
357
|
+
yield* install(finished);
|
|
391
358
|
return yield* answer.failure;
|
|
392
359
|
}
|
|
393
|
-
install(finished);
|
|
360
|
+
yield* install(finished);
|
|
361
|
+
});
|
|
362
|
+
const applyCleanup = Effect.fnUntraced(function* (change) {
|
|
363
|
+
const current = yield* Draft.make(state);
|
|
364
|
+
yield* Effect.provideService(change(current), Draft, current);
|
|
365
|
+
yield* committed(OpContext.make("close"), current, true);
|
|
394
366
|
});
|
|
395
367
|
const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
|
|
396
|
-
// Permit waits stay interruptible.
|
|
397
|
-
//
|
|
398
|
-
const coordinated = (op, effect,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}))
|
|
403
|
-
: Effect.uninterruptibleMask((restore) => Effect.flatMap(restore(transition(Effect.andThen(checkAvailable(op.operation), effect))), ([value, finished]) => Effect.as(committed(op, commitProvider, finished, onStorageFailure), value))))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
368
|
+
// Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
|
|
369
|
+
// interruptible until the candidate is ready. Every provider commit and installation is masked.
|
|
370
|
+
const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
|
|
371
|
+
const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
|
|
372
|
+
return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
|
|
373
|
+
}))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
404
374
|
const coordinatedRead = (op, effect) => admit(op, observing(Effect.andThen(checkAvailable(op.operation), effect))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
405
|
-
const refreshAccess = (read)
|
|
375
|
+
const refreshAccess = Effect.fnUntraced(function* (read) {
|
|
406
376
|
if (refreshDue(read)) {
|
|
407
377
|
const { node, now } = read.access;
|
|
408
|
-
|
|
378
|
+
const draft = yield* Draft;
|
|
379
|
+
yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
|
|
409
380
|
}
|
|
410
381
|
return read.value;
|
|
411
|
-
};
|
|
382
|
+
});
|
|
412
383
|
// A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
|
|
413
384
|
// only when the access time is due does it run again as a change. The change repeats every check and the rule,
|
|
414
385
|
// since another read may have refreshed the time meanwhile, and its result is the one returned.
|
|
415
386
|
const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
|
|
416
|
-
? coordinated(op, Effect.
|
|
387
|
+
? coordinated(op, Effect.flatMap(read, refreshAccess))
|
|
417
388
|
: Effect.succeed(observed.value));
|
|
418
389
|
const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
|
|
419
|
-
const shutdown = commitProvider ===
|
|
390
|
+
const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
|
|
420
391
|
available = false;
|
|
421
|
-
}));
|
|
422
|
-
const current = () => {
|
|
423
|
-
if (draft === undefined)
|
|
424
|
-
return dieInvariant("Volume mutation outside a transition");
|
|
425
|
-
return draft;
|
|
426
|
-
};
|
|
392
|
+
}), commitProvider.shutdown)));
|
|
427
393
|
const pathOf = (get, ino) => {
|
|
428
394
|
const names = [];
|
|
429
395
|
let node = get(ino);
|
|
@@ -438,19 +404,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
438
404
|
}
|
|
439
405
|
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
440
406
|
};
|
|
407
|
+
const pathOfCurrent = Effect.fnUntraced(function* (ino) {
|
|
408
|
+
const names = [];
|
|
409
|
+
const first = yield* view(ino);
|
|
410
|
+
if (first?.kind !== "directory" || first.metadata.nlink === 0)
|
|
411
|
+
return undefined;
|
|
412
|
+
let node = first;
|
|
413
|
+
while (node.ino !== ROOT_INO) {
|
|
414
|
+
names.push(node.name);
|
|
415
|
+
const parent = yield* view(node.parent);
|
|
416
|
+
if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
|
|
417
|
+
return undefined;
|
|
418
|
+
node = parent;
|
|
419
|
+
}
|
|
420
|
+
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
421
|
+
});
|
|
441
422
|
const entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
|
|
442
|
-
const publishEntry = (_tag, parent, name, ino) => {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const target = current().get(ino);
|
|
423
|
+
const publishEntry = (_tag, parent, name, ino) => Effect.flatMap(Draft, (draft) => draft.publish((installed) => {
|
|
424
|
+
const prefix = pathOf((at) => getNode(installed, at), parent);
|
|
425
|
+
return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
|
|
426
|
+
}));
|
|
427
|
+
const publishNode = Effect.fnUntraced(function* (ino) {
|
|
428
|
+
const draft = yield* Draft;
|
|
429
|
+
const target = yield* draft.get(ino);
|
|
450
430
|
if (target === undefined)
|
|
451
431
|
return;
|
|
452
432
|
if (target.kind === "directory") {
|
|
453
|
-
|
|
433
|
+
yield* draft.publish((installed) => {
|
|
454
434
|
const directory = getNode(installed, ino);
|
|
455
435
|
const path = pathOf((at) => getNode(installed, at), ino);
|
|
456
436
|
return path === undefined || directory?.kind !== "directory"
|
|
@@ -460,7 +440,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
460
440
|
return;
|
|
461
441
|
}
|
|
462
442
|
const links = target.links;
|
|
463
|
-
|
|
443
|
+
yield* draft.publish((installed) => {
|
|
464
444
|
const events = [];
|
|
465
445
|
for (const link of links) {
|
|
466
446
|
const prefix = pathOf((at) => getNode(installed, at), link.parent);
|
|
@@ -474,7 +454,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
474
454
|
}
|
|
475
455
|
return events;
|
|
476
456
|
});
|
|
477
|
-
};
|
|
457
|
+
});
|
|
478
458
|
const pathsOf = (installed, ino) => {
|
|
479
459
|
const node = getNode(installed, ino);
|
|
480
460
|
if (node === undefined)
|
|
@@ -508,16 +488,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
508
488
|
includes: () => true,
|
|
509
489
|
rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
|
|
510
490
|
};
|
|
511
|
-
const scopeRoot = (scope, op)
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
return Effect.fail(op.fail("InvalidReference"));
|
|
515
|
-
if (known.volume !== volumeIdentity)
|
|
516
|
-
return Effect.fail(op.fail("ForeignReference"));
|
|
517
|
-
const node = getNode(state, known.ino);
|
|
518
|
-
return node === undefined || node.metadata.nlink === 0
|
|
519
|
-
? Effect.fail(op.fail("StaleReference"))
|
|
520
|
-
: Effect.succeed(known.ino);
|
|
491
|
+
const scopeRoot = Effect.fnUntraced(function* (scope, op) {
|
|
492
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(scope, "reference"), "reference", op);
|
|
493
|
+
return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
|
|
521
494
|
});
|
|
522
495
|
// A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
|
|
523
496
|
// each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
|
|
@@ -550,10 +523,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
550
523
|
yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
|
|
551
524
|
return { snapshot, observation: yield* observeChanges(captured) };
|
|
552
525
|
});
|
|
553
|
-
const addressable = (ino) => {
|
|
554
|
-
const node = getNode(state, ino);
|
|
555
|
-
return node !== undefined && !(node.kind === "directory" && node.metadata.nlink === 0);
|
|
556
|
-
};
|
|
557
526
|
const keyTag = (ino) => {
|
|
558
527
|
const message = new Uint8Array(40);
|
|
559
528
|
message.set(identityBytes);
|
|
@@ -561,19 +530,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
561
530
|
new DataView(message.buffer).setBigUint64(32, ino);
|
|
562
531
|
return hmacSha256(keySecretBytes, message).subarray(0, 16);
|
|
563
532
|
};
|
|
564
|
-
const referenceFor = (ino) => {
|
|
565
|
-
const existing = tokens.get(ino);
|
|
566
|
-
if (existing !== undefined)
|
|
567
|
-
return existing;
|
|
568
|
-
const reference = Object.freeze({ [ObjectReferenceId]: true });
|
|
569
|
-
objectReferences.set(reference, { volume: volumeIdentity, ino });
|
|
570
|
-
tokens.set(ino, reference);
|
|
571
|
-
return reference;
|
|
572
|
-
};
|
|
573
533
|
const holdDirectory = (ino) => {
|
|
574
534
|
directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
|
|
575
535
|
};
|
|
576
|
-
const unholdDirectory = (ino)
|
|
536
|
+
const unholdDirectory = Effect.fnUntraced(function* (ino) {
|
|
577
537
|
const count = (directoryHolds.get(ino) ?? 1) - 1;
|
|
578
538
|
if (count > 0) {
|
|
579
539
|
directoryHolds.set(ino, count);
|
|
@@ -581,59 +541,57 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
581
541
|
}
|
|
582
542
|
directoryHolds.delete(ino);
|
|
583
543
|
const node = getNode(state, ino);
|
|
584
|
-
if (node?.kind === "directory" && node.metadata.nlink === 0)
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
unholdDirectory(ino);
|
|
593
|
-
};
|
|
594
|
-
const finalizeDirectory = (reference) => Effect.uninterruptible(Effect.suspend(() => reference.closed
|
|
595
|
-
? Effect.void
|
|
596
|
-
: coordinatedCleanup(Effect.sync(() => releaseDirectory(reference)))));
|
|
597
|
-
const closeReleasedScope = (reference) => Effect.suspend(() => !reference.closed || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
|
|
598
|
-
const permitted = (node, identity) => {
|
|
599
|
-
const metadata = node.metadata;
|
|
600
|
-
const shift = metadata.uid === identity.uid ?
|
|
601
|
-
6
|
|
602
|
-
: inGroup(identity, metadata.gid)
|
|
603
|
-
? 3
|
|
604
|
-
: 0;
|
|
605
|
-
return (metadata.mode >> shift) & (READ | WRITE | EXECUTE);
|
|
544
|
+
if (node?.kind === "directory" && node.metadata.nlink === 0) {
|
|
545
|
+
yield* applyCleanup((d) => d.remove(ino));
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
|
|
549
|
+
const finalizeDirectory = (reference) => {
|
|
550
|
+
const close = coordinatedCleanup(releaseDirectory(reference));
|
|
551
|
+
return TokenRegistry.finalize(reference, close, Effect.ignore(close));
|
|
606
552
|
};
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const reclaim = (d, ino)
|
|
611
|
-
const node = d.get(ino);
|
|
553
|
+
const closeReleasedScope = TokenRegistry.closeReleasedScope;
|
|
554
|
+
const permitted = Resolution.permitted;
|
|
555
|
+
const authorize = Resolution.authorize;
|
|
556
|
+
const reclaim = Effect.fnUntraced(function* (d, ino) {
|
|
557
|
+
const node = yield* d.get(ino);
|
|
612
558
|
if (node === undefined)
|
|
613
559
|
return;
|
|
614
560
|
if (node.kind === "file") {
|
|
615
|
-
if (node.links.length > 0 || d.openCount(ino) > 0)
|
|
561
|
+
if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
|
|
616
562
|
return;
|
|
617
|
-
d.
|
|
563
|
+
yield* d.addUsedBytes(-BigInt(node.data.length));
|
|
618
564
|
}
|
|
619
565
|
else if (node.kind === "symlink") {
|
|
620
566
|
if (node.links.length > 0)
|
|
621
567
|
return;
|
|
622
|
-
d.
|
|
568
|
+
yield* d.addUsedBytes(-BigInt(node.target.length));
|
|
623
569
|
}
|
|
624
570
|
else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
|
|
625
571
|
return;
|
|
626
|
-
d.remove(ino);
|
|
627
|
-
};
|
|
628
|
-
const atEntryLimit = (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
572
|
+
yield* d.remove(ino);
|
|
573
|
+
});
|
|
574
|
+
const atEntryLimit = Effect.gen(function* () {
|
|
575
|
+
const draft = yield* Draft;
|
|
576
|
+
return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
|
|
577
|
+
});
|
|
578
|
+
const reserveEntry = Effect.fnUntraced(function* (op) {
|
|
579
|
+
const draft = yield* Draft;
|
|
580
|
+
if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
|
|
581
|
+
return yield* op.fail("NoSpace");
|
|
582
|
+
});
|
|
583
|
+
const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
|
|
584
|
+
const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
|
|
585
|
+
if (limits.maxBytes === undefined)
|
|
586
|
+
return;
|
|
587
|
+
const draft = yield* Draft;
|
|
588
|
+
if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
|
|
589
|
+
return yield* op.fail("NoSpace");
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
const attach = Effect.fnUntraced(function* (parent, name, child, now) {
|
|
593
|
+
const d = yield* Draft;
|
|
594
|
+
yield* d.put(withEntries({
|
|
637
595
|
...parent,
|
|
638
596
|
metadata: {
|
|
639
597
|
...parent.metadata,
|
|
@@ -643,89 +601,43 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
643
601
|
}
|
|
644
602
|
}, (entries) => entries.set(name, child.ino)));
|
|
645
603
|
if (child.kind === "directory")
|
|
646
|
-
d.put({ ...child, parent: parent.ino, name });
|
|
604
|
+
yield* d.put({ ...child, parent: parent.ino, name });
|
|
647
605
|
else {
|
|
648
|
-
d.put({
|
|
606
|
+
yield* d.put({
|
|
649
607
|
...child,
|
|
650
608
|
links: [...child.links, { parent: parent.ino, name }],
|
|
651
609
|
metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
|
|
652
610
|
});
|
|
653
611
|
}
|
|
654
|
-
};
|
|
655
|
-
const detach = (child, parent, name, now)
|
|
656
|
-
const d =
|
|
612
|
+
});
|
|
613
|
+
const detach = Effect.fnUntraced(function* (child, parent, name, now) {
|
|
614
|
+
const d = yield* Draft;
|
|
657
615
|
if (child.kind === "directory") {
|
|
658
|
-
d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
|
|
616
|
+
yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
|
|
659
617
|
}
|
|
660
618
|
else {
|
|
661
619
|
const links = withoutLink(child.links, parent, name);
|
|
662
|
-
d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
|
|
663
|
-
}
|
|
664
|
-
reclaim(d, child.ino);
|
|
665
|
-
};
|
|
666
|
-
const releaseFile = (ref) => {
|
|
667
|
-
const ino = ref.ino;
|
|
668
|
-
if (ino !== undefined) {
|
|
669
|
-
const d = current();
|
|
670
|
-
d.release(ino);
|
|
671
|
-
reclaim(d, ino);
|
|
620
|
+
yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
|
|
672
621
|
}
|
|
673
|
-
|
|
674
|
-
ref.closed = true;
|
|
675
|
-
};
|
|
676
|
-
const releaseOpenFile = (ref) => coordinatedCleanup(Effect.sync(() => {
|
|
677
|
-
if (!ref.closed)
|
|
678
|
-
applyDirect(() => releaseFile(ref));
|
|
679
|
-
}));
|
|
680
|
-
// Explicit close and scope cleanup share one release. A release whose commit fails still completes as
|
|
681
|
-
// cleanup, so the handle never stays open; only an explicit close reports the failure. A close refused
|
|
682
|
-
// admission never entered the volume, so it leaves the handle open for a retry.
|
|
683
|
-
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, Effect.sync(() => {
|
|
684
|
-
const ino = ref.ino;
|
|
685
|
-
if (ino !== undefined) {
|
|
686
|
-
const d = current();
|
|
687
|
-
d.release(ino);
|
|
688
|
-
reclaim(d, ino);
|
|
689
|
-
}
|
|
690
|
-
current().after.push(() => {
|
|
691
|
-
ref.ino = undefined;
|
|
692
|
-
ref.closed = true;
|
|
693
|
-
});
|
|
694
|
-
})), () => applyDirect(() => releaseFile(ref))).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
|
|
695
|
-
// Keyed on the file rather than the closed flag, so a rerun releases an open that published after a first run.
|
|
696
|
-
// Cleanup is uninterruptible and does not need admission, so an interrupted or busy scope close still releases.
|
|
697
|
-
const finalizeFile = (ref) => Effect.uninterruptible(Effect.suspend(() => ref.ino === undefined
|
|
698
|
-
? Effect.sync(() => {
|
|
699
|
-
ref.closed = true;
|
|
700
|
-
})
|
|
701
|
-
: Effect.ignore(closeFile(ref, OpContext.make("close"), Effect.void)).pipe(Effect.andThen(Effect.suspend(() => ref.closed ? Effect.void : releaseOpenFile(ref))))));
|
|
702
|
-
// Acquires a handle into its own scope, forked from the caller's. The finalizer is registered before waiting,
|
|
703
|
-
// since a closed scope runs a new finalizer at once and the permit is not reentrant. A scope that closes
|
|
704
|
-
// before or during acquisition interrupts it, and releasing here, under the permit, keeps the acquisition
|
|
705
|
-
// from outliving a finalizer that already ran. A scope that closes while the acquisition commits interrupts it
|
|
706
|
-
// too. Every exit without a handle reruns the finalizer, which may have run before the commit published and
|
|
707
|
-
// which closing an already closed scope would not run again.
|
|
708
|
-
const acquireHandle = Effect.fnUntraced(function* (reference, coordinate, acquire, releaseAcquired, finalize) {
|
|
709
|
-
const scope = yield* Scope.fork(yield* Effect.scope);
|
|
710
|
-
reference.scope = scope;
|
|
711
|
-
yield* Scope.addFinalizer(scope, finalize);
|
|
712
|
-
const closed = () => Predicate.isTagged(scope.state, "Closed");
|
|
713
|
-
return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquire).pipe(Effect.tap(() => Effect.suspend(() => {
|
|
714
|
-
if (!closed())
|
|
715
|
-
return Effect.void;
|
|
716
|
-
releaseAcquired();
|
|
717
|
-
return Effect.interrupt;
|
|
718
|
-
})))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalize, Scope.close(scope, Exit.void))));
|
|
622
|
+
yield* reclaim(d, child.ino);
|
|
719
623
|
});
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
};
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
624
|
+
const releaseFileInode = Effect.fnUntraced(function* (ino) {
|
|
625
|
+
const d = yield* Draft;
|
|
626
|
+
yield* d.release(ino);
|
|
627
|
+
yield* reclaim(d, ino);
|
|
628
|
+
});
|
|
629
|
+
const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
|
|
630
|
+
const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
|
|
631
|
+
// Admission refusal leaves the handle open. Every admitted close shares the registry release,
|
|
632
|
+
// and a storage failure still completes cleanup while only explicit close reports the failure.
|
|
633
|
+
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, TokenRegistry.release(ref, releaseFileInode, (action) => Effect.flatMap(Draft, (draft) => draft.afterInstall(action)))), true).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
|
|
634
|
+
const finalizeFile = (ref) => TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref));
|
|
635
|
+
const acquireHandle = TokenRegistry.acquire;
|
|
636
|
+
const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
|
|
637
|
+
const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
|
|
638
|
+
const d = yield* Draft;
|
|
639
|
+
yield* d.addUsedBytes(BigInt(data.length - file.data.length));
|
|
640
|
+
yield* d.put({
|
|
729
641
|
...file,
|
|
730
642
|
data,
|
|
731
643
|
metadata: {
|
|
@@ -737,8 +649,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
737
649
|
}
|
|
738
650
|
});
|
|
739
651
|
if (publish)
|
|
740
|
-
publishNode(file.ino);
|
|
741
|
-
};
|
|
652
|
+
yield* publishNode(file.ino);
|
|
653
|
+
});
|
|
742
654
|
const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
|
|
743
655
|
if (length > BigInt(maxFileBytes))
|
|
744
656
|
return yield* op.fail("FileTooLarge");
|
|
@@ -746,22 +658,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
746
658
|
yield* reserveBytes(op, BigInt(size - file.data.length));
|
|
747
659
|
const data = new Uint8Array(size);
|
|
748
660
|
data.set(file.data.subarray(0, size));
|
|
749
|
-
replaceContent(file, data, now, false);
|
|
661
|
+
yield* replaceContent(file, data, now, false);
|
|
750
662
|
});
|
|
751
663
|
const resize = Effect.fnUntraced(function* (file, length, op) {
|
|
752
664
|
if (!isLength(length))
|
|
753
665
|
return yield* op.fail("InvalidArgument");
|
|
754
666
|
yield* resizeAt(file, length, yield* timestamp(op), op);
|
|
755
|
-
publishNode(file.ino);
|
|
667
|
+
yield* publishNode(file.ino);
|
|
756
668
|
});
|
|
757
669
|
const fileHandle = (ref) => {
|
|
758
|
-
const get = (op, access)
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
670
|
+
const get = Effect.fnUntraced(function* (op, access) {
|
|
671
|
+
const ino = TokenRegistry.inode(ref);
|
|
672
|
+
const node = ino === undefined ? undefined : yield* view(ino);
|
|
673
|
+
if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
|
|
674
|
+
(access === "write" && ref.access === "read"))
|
|
675
|
+
return yield* op.fail("InvalidHandle");
|
|
676
|
+
return node;
|
|
677
|
+
});
|
|
765
678
|
const read = (maximum, position) => {
|
|
766
679
|
const op = OpContext.make(position === undefined ? "read" : "pread");
|
|
767
680
|
const readOp = OpContext.make("read");
|
|
@@ -783,9 +696,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
783
696
|
// other would start at the same offset.
|
|
784
697
|
if (position !== undefined)
|
|
785
698
|
return Effect.map(accessing(op, body), ({ bytes, eof }) => ({ bytes, eof }));
|
|
786
|
-
return coordinated(op, Effect.
|
|
787
|
-
const { bytes, eof, next } = refreshAccess(
|
|
788
|
-
|
|
699
|
+
return coordinated(op, Effect.gen(function* () {
|
|
700
|
+
const { bytes, eof, next } = yield* refreshAccess(yield* body);
|
|
701
|
+
const draft = yield* Draft;
|
|
702
|
+
yield* draft.afterInstall(() => {
|
|
789
703
|
ref.offset = next;
|
|
790
704
|
});
|
|
791
705
|
return { bytes, eof };
|
|
@@ -809,9 +723,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
809
723
|
if (offset >= BigInt(maxFileBytes))
|
|
810
724
|
return yield* writeOp.fail("FileTooLarge");
|
|
811
725
|
const start = Number(offset);
|
|
812
|
-
const
|
|
726
|
+
const draft = yield* Draft;
|
|
727
|
+
const free = limits.maxBytes === undefined
|
|
813
728
|
? BigInt(maxFileBytes)
|
|
814
|
-
: ByteSize.toBigInt(
|
|
729
|
+
: ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
|
|
815
730
|
const maximumEnd = BigInt(file.data.length) + free;
|
|
816
731
|
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
817
732
|
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
@@ -824,10 +739,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
824
739
|
data.set(file.data);
|
|
825
740
|
const now = yield* timestamp(writeOp);
|
|
826
741
|
data.set(bytes.subarray(0, count), start);
|
|
827
|
-
replaceContent(file, data, now);
|
|
742
|
+
yield* replaceContent(file, data, now);
|
|
828
743
|
if (position === undefined) {
|
|
829
744
|
const next = offset + BigInt(count);
|
|
830
|
-
|
|
745
|
+
yield* draft.afterInstall(() => {
|
|
831
746
|
ref.offset = next;
|
|
832
747
|
});
|
|
833
748
|
}
|
|
@@ -886,50 +801,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
886
801
|
sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))).pipe(Effect.withSpan("FileHandle.sync")),
|
|
887
802
|
close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref)), Effect.withSpan("FileHandle.close"))
|
|
888
803
|
});
|
|
889
|
-
|
|
804
|
+
registry.registerFile(handle, ref);
|
|
890
805
|
return handle;
|
|
891
806
|
};
|
|
892
807
|
const createCaller = (reference, identity, umask) => {
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (known === undefined)
|
|
900
|
-
return yield* op.fail("InvalidReference");
|
|
901
|
-
if (known.volume !== volumeIdentity) {
|
|
902
|
-
return yield* op.fail("ForeignReference");
|
|
903
|
-
}
|
|
904
|
-
const node = view(known.ino);
|
|
905
|
-
if (node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)) {
|
|
906
|
-
return yield* op.fail("StaleReference");
|
|
907
|
-
}
|
|
908
|
-
return node;
|
|
808
|
+
const resolver = Resolution.make({
|
|
809
|
+
caller: reference,
|
|
810
|
+
get: view,
|
|
811
|
+
identity,
|
|
812
|
+
maxPathBytes: limits.maxPathBytes,
|
|
813
|
+
registry
|
|
909
814
|
});
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
};
|
|
914
|
-
const
|
|
915
|
-
|
|
815
|
+
const prepare = resolver.prepare;
|
|
816
|
+
const prepareEntry = resolver.prepareEntry;
|
|
817
|
+
const entryName = Resolution.entryName;
|
|
818
|
+
const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
|
|
819
|
+
const resolveDirectory = resolver.directory;
|
|
820
|
+
const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
|
|
821
|
+
const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
|
|
822
|
+
const directoryNow = Effect.fnUntraced(function* (ino) {
|
|
823
|
+
const node = yield* view(ino);
|
|
916
824
|
if (node?.kind !== "directory") {
|
|
917
825
|
return dieInvariant("Directory left the inode table during a transition");
|
|
918
826
|
}
|
|
919
827
|
return node;
|
|
920
|
-
};
|
|
921
|
-
const nodeNow = (ino)
|
|
922
|
-
const node = view(ino);
|
|
828
|
+
});
|
|
829
|
+
const nodeNow = Effect.fnUntraced(function* (ino) {
|
|
830
|
+
const node = yield* view(ino);
|
|
923
831
|
if (node === undefined)
|
|
924
832
|
return dieInvariant("Inode left the table during a transition");
|
|
925
833
|
return node;
|
|
926
|
-
};
|
|
834
|
+
});
|
|
927
835
|
const creationTimes = (times, now) => ({
|
|
928
836
|
atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
|
|
929
837
|
mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
|
|
930
838
|
});
|
|
931
|
-
const newDirectory = (parent, mode, now, times)
|
|
932
|
-
const
|
|
839
|
+
const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
|
|
840
|
+
const draft = yield* Draft;
|
|
841
|
+
const ino = yield* draft.allocate;
|
|
933
842
|
return {
|
|
934
843
|
kind: "directory",
|
|
935
844
|
ino,
|
|
@@ -942,9 +851,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
942
851
|
},
|
|
943
852
|
revision: 0n
|
|
944
853
|
};
|
|
945
|
-
};
|
|
946
|
-
const newFile = (parent, data, mode, now, owner, times)
|
|
947
|
-
const
|
|
854
|
+
});
|
|
855
|
+
const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
|
|
856
|
+
const draft = yield* Draft;
|
|
857
|
+
const ino = yield* draft.allocate;
|
|
948
858
|
return {
|
|
949
859
|
kind: "file",
|
|
950
860
|
ino,
|
|
@@ -959,9 +869,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
959
869
|
},
|
|
960
870
|
revision: 0n
|
|
961
871
|
};
|
|
962
|
-
};
|
|
963
|
-
const newSymlink = (parent, target, now, times)
|
|
964
|
-
const
|
|
872
|
+
});
|
|
873
|
+
const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
|
|
874
|
+
const draft = yield* Draft;
|
|
875
|
+
const ino = yield* draft.allocate;
|
|
965
876
|
return {
|
|
966
877
|
kind: "symlink",
|
|
967
878
|
ino,
|
|
@@ -976,142 +887,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
976
887
|
},
|
|
977
888
|
revision: 0n
|
|
978
889
|
};
|
|
979
|
-
};
|
|
980
|
-
const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
|
|
981
|
-
const pathOp = op.at(path.input);
|
|
982
|
-
const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
|
|
983
|
-
if (reference.ino === undefined) {
|
|
984
|
-
return yield* pathOp.fail("ClosedCaller");
|
|
985
|
-
}
|
|
986
|
-
let current = path.absolute ? nodeNow(ROOT_INO) : yield* callerDirectory(pathOp);
|
|
987
|
-
if (!path.absolute && referencedBase !== undefined) {
|
|
988
|
-
current = referencedBase;
|
|
989
|
-
yield* authorize(current, identity, EXECUTE, op);
|
|
990
|
-
}
|
|
991
|
-
else if (!path.absolute && base !== undefined) {
|
|
992
|
-
const target = handles.get(base);
|
|
993
|
-
if (target === undefined) {
|
|
994
|
-
return yield* pathOp.fail("InvalidHandle");
|
|
995
|
-
}
|
|
996
|
-
if (target.volume !== volumeIdentity) {
|
|
997
|
-
return yield* pathOp.fail("ForeignHandle");
|
|
998
|
-
}
|
|
999
|
-
const directory = target.ino === undefined ? undefined : view(target.ino);
|
|
1000
|
-
if (directory?.kind !== "directory") {
|
|
1001
|
-
return yield* pathOp.fail("InvalidHandle");
|
|
1002
|
-
}
|
|
1003
|
-
current = directory;
|
|
1004
|
-
yield* authorize(current, identity, EXECUTE, pathOp);
|
|
1005
|
-
}
|
|
1006
|
-
if (current.metadata.nlink === 0) {
|
|
1007
|
-
return yield* pathOp.fail("NotFound");
|
|
1008
|
-
}
|
|
1009
|
-
let work = path;
|
|
1010
|
-
let parent;
|
|
1011
|
-
let name;
|
|
1012
|
-
let traversals = 0;
|
|
1013
|
-
// Leading components that came from a symbolic link's target. Only the components the caller wrote are
|
|
1014
|
-
// created, so a dangling link stays missing, as it does for mkdir -p.
|
|
1015
|
-
let linked = 0;
|
|
1016
|
-
for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
|
|
1017
|
-
if (current.kind !== "directory") {
|
|
1018
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1019
|
-
}
|
|
1020
|
-
yield* authorize(current, identity, EXECUTE, pathOp);
|
|
1021
|
-
const component = work.components[index];
|
|
1022
|
-
if (component === undefined)
|
|
1023
|
-
break;
|
|
1024
|
-
if (component === DOT_HEX)
|
|
1025
|
-
continue;
|
|
1026
|
-
if (component === DOT_DOT_HEX) {
|
|
1027
|
-
current = directoryNow(current.parent);
|
|
1028
|
-
parent = undefined;
|
|
1029
|
-
name = undefined;
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
parent = current;
|
|
1033
|
-
name = component;
|
|
1034
|
-
const childIno = current.entries.get(component);
|
|
1035
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1036
|
-
if (child === undefined) {
|
|
1037
|
-
if (allowMissing && index === work.components.length - 1) {
|
|
1038
|
-
return { node: undefined, parent, name };
|
|
1039
|
-
}
|
|
1040
|
-
if (createMissing !== undefined && index >= linked) {
|
|
1041
|
-
current = yield* createMissing(current, component, index === work.components.length - 1);
|
|
1042
|
-
continue;
|
|
1043
|
-
}
|
|
1044
|
-
return yield* pathOp.fail("NotFound");
|
|
1045
|
-
}
|
|
1046
|
-
if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
|
|
1047
|
-
if (child.target.length === 0) {
|
|
1048
|
-
return yield* pathOp.fail("NotFound");
|
|
1049
|
-
}
|
|
1050
|
-
if (++traversals > MAX_SYMLINK_TRAVERSALS) {
|
|
1051
|
-
return yield* pathOp.fail("SymlinkLoop");
|
|
1052
|
-
}
|
|
1053
|
-
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
1054
|
-
const remaining = work.components.length - index - 1;
|
|
1055
|
-
if (settings.maxPathBytes !== undefined &&
|
|
1056
|
-
ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
|
|
1057
|
-
return yield* pathOp.fail("PathTooLong");
|
|
1058
|
-
}
|
|
1059
|
-
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
1060
|
-
expansion.set(child.target);
|
|
1061
|
-
expansion.set(suffix, child.target.length);
|
|
1062
|
-
const expanded = preparePath(ownedPath(expansion), op.operation, settings.maxPathBytes);
|
|
1063
|
-
// The expansion is synthetic: its per-component limits are the caller's to hear about,
|
|
1064
|
-
// but the path in the error has to be the one the caller passed in, so the
|
|
1065
|
-
// expansion's own failure is not kept as the cause.
|
|
1066
|
-
if (Result.isFailure(expanded)) {
|
|
1067
|
-
return yield* pathOp.fail(expanded.failure.code);
|
|
1068
|
-
}
|
|
1069
|
-
work = expanded.success;
|
|
1070
|
-
linked = work.components.length - remaining + Math.max(0, linked - index - 1);
|
|
1071
|
-
if (work.absolute)
|
|
1072
|
-
current = nodeNow(ROOT_INO);
|
|
1073
|
-
index = -1;
|
|
1074
|
-
}
|
|
1075
|
-
else
|
|
1076
|
-
current = child;
|
|
1077
|
-
}
|
|
1078
|
-
if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
|
|
1079
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1080
|
-
}
|
|
1081
|
-
return { node: current, parent, name };
|
|
1082
|
-
});
|
|
1083
|
-
const resolveNode = Effect.fnUntraced(function* (path, base, op, options) {
|
|
1084
|
-
const pathOp = op.at(path.input);
|
|
1085
|
-
const result = yield* lookup(path, base, op, options);
|
|
1086
|
-
if (result.node === undefined) {
|
|
1087
|
-
return yield* pathOp.fail("NotFound");
|
|
1088
|
-
}
|
|
1089
|
-
return result.node;
|
|
1090
890
|
});
|
|
1091
|
-
const locate = Effect.fnUntraced(function* (path, base, op, options) {
|
|
1092
|
-
const pathOp = op.at(path.input);
|
|
1093
|
-
const result = yield* lookup(path, base, op, options);
|
|
1094
|
-
const node = result.node;
|
|
1095
|
-
if (node === undefined) {
|
|
1096
|
-
return yield* pathOp.fail("NotFound");
|
|
1097
|
-
}
|
|
1098
|
-
if (node.kind !== "directory") {
|
|
1099
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1100
|
-
}
|
|
1101
|
-
return node;
|
|
1102
|
-
});
|
|
1103
|
-
const ResolvedEntry = {
|
|
1104
|
-
fromPath: Effect.fnUntraced(function* (path, base, op) {
|
|
1105
|
-
const parent = yield* locate(path, base, op, { parentOnly: true });
|
|
1106
|
-
return {
|
|
1107
|
-
parent: parent.ino,
|
|
1108
|
-
name: path.components.at(-1),
|
|
1109
|
-
trailingSlash: path.trailingSlash,
|
|
1110
|
-
addressing: "path",
|
|
1111
|
-
op: op.at(path.input)
|
|
1112
|
-
};
|
|
1113
|
-
})
|
|
1114
|
-
};
|
|
1115
891
|
const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
|
|
1116
892
|
const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
|
|
1117
893
|
? Effect.fail(op.fail("NotPermitted"))
|
|
@@ -1137,7 +913,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1137
913
|
const resolving = resolve();
|
|
1138
914
|
return coordinated(op, Effect.gen(function* () {
|
|
1139
915
|
const resolved = yield* resolving;
|
|
1140
|
-
const node = nodeNow(resolved.ino);
|
|
916
|
+
const node = yield* nodeNow(resolved.ino);
|
|
1141
917
|
if (expected !== undefined && node.revision !== expected) {
|
|
1142
918
|
return yield* resolved.op.fail("StaleReference", { field: "expected" });
|
|
1143
919
|
}
|
|
@@ -1163,11 +939,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1163
939
|
const now = yield* timestamp(op);
|
|
1164
940
|
if (size !== undefined && node.kind === "file")
|
|
1165
941
|
yield* resizeAt(node, size, now, op);
|
|
1166
|
-
const sized = nodeNow(resolved.ino);
|
|
942
|
+
const sized = yield* nodeNow(resolved.ino);
|
|
1167
943
|
const gid = owner?.gid ?? sized.metadata.gid;
|
|
1168
944
|
const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
|
|
1169
945
|
if (chowned || mode !== undefined || timed) {
|
|
1170
|
-
|
|
946
|
+
yield* (yield* Draft).put({
|
|
1171
947
|
...sized,
|
|
1172
948
|
metadata: {
|
|
1173
949
|
...sized.metadata,
|
|
@@ -1180,7 +956,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1180
956
|
}
|
|
1181
957
|
});
|
|
1182
958
|
}
|
|
1183
|
-
publishNode(node.ino);
|
|
959
|
+
yield* publishNode(node.ino);
|
|
1184
960
|
}));
|
|
1185
961
|
};
|
|
1186
962
|
const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
|
|
@@ -1188,7 +964,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1188
964
|
? Effect.fail(op.fail("NotPermitted"))
|
|
1189
965
|
: Effect.void;
|
|
1190
966
|
const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
|
|
1191
|
-
const parent = directoryNow(entry.parent);
|
|
967
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1192
968
|
if (isDotComponent(entry.name)) {
|
|
1193
969
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
|
|
1194
970
|
}
|
|
@@ -1203,17 +979,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1203
979
|
const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
|
|
1204
980
|
const name = yield* claimName(entry);
|
|
1205
981
|
yield* reserveEntry(entry.op);
|
|
1206
|
-
const parent = directoryNow(entry.parent);
|
|
982
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1207
983
|
const before = parent.revision;
|
|
1208
984
|
const now = yield* timestamp(op);
|
|
1209
985
|
const mode = request.exactMode
|
|
1210
986
|
? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
|
|
1211
987
|
: (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
|
|
1212
|
-
const child = newDirectory(parent, mode, now, request.times);
|
|
1213
|
-
attach(parent, name, child, now);
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
988
|
+
const child = yield* newDirectory(parent, mode, now, request.times);
|
|
989
|
+
yield* attach(parent, name, child, now);
|
|
990
|
+
const draft = yield* Draft;
|
|
991
|
+
yield* draft.addEntries(1);
|
|
992
|
+
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
993
|
+
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
1217
994
|
});
|
|
1218
995
|
// Creates every missing directory a path names in the running transition, so a failure anywhere discards
|
|
1219
996
|
// them all. The mode applies to each directory created and the times to the one in the final position. An
|
|
@@ -1229,57 +1006,64 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1229
1006
|
const made = yield* Effect.result(makeDirectory(entry, request, op));
|
|
1230
1007
|
if (Result.isSuccess(made))
|
|
1231
1008
|
return made.success;
|
|
1232
|
-
const existingIno = entry.name === undefined
|
|
1233
|
-
|
|
1009
|
+
const existingIno = entry.name === undefined
|
|
1010
|
+
? undefined
|
|
1011
|
+
: (yield* directoryNow(entry.parent)).entries.get(entry.name);
|
|
1012
|
+
node = existingIno === undefined ? undefined : (yield* view(existingIno));
|
|
1234
1013
|
if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
|
|
1235
1014
|
return yield* made.failure;
|
|
1236
1015
|
}
|
|
1237
1016
|
else {
|
|
1238
1017
|
entryOp = op.at(prepared.path.input);
|
|
1239
|
-
const resolved = yield*
|
|
1240
|
-
|
|
1018
|
+
const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
|
|
1019
|
+
? { path: prepared.path.input }
|
|
1020
|
+
: { path: prepared.path.input, relativeTo: prepared.base }), {
|
|
1021
|
+
kind: "Node",
|
|
1022
|
+
createMissing: (parent, name, final) => Effect.flatMap(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
|
|
1241
1023
|
if (!revisionsBefore.has(parent.ino))
|
|
1242
1024
|
revisionsBefore.set(parent.ino, made.directory.before);
|
|
1243
1025
|
return directoryNow(made.child);
|
|
1244
1026
|
})
|
|
1245
|
-
});
|
|
1027
|
+
}, op);
|
|
1246
1028
|
node = resolved.node;
|
|
1247
1029
|
}
|
|
1248
1030
|
if (node?.kind !== "directory")
|
|
1249
1031
|
return yield* entryOp.fail("AlreadyExists");
|
|
1250
|
-
const after = directoryNow(node.parent).revision;
|
|
1032
|
+
const after = (yield* directoryNow(node.parent)).revision;
|
|
1251
1033
|
return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
|
|
1252
1034
|
});
|
|
1253
1035
|
const linkNode = Effect.fnUntraced(function* (node, entry, op) {
|
|
1254
1036
|
const name = yield* claimName(entry, "rejected");
|
|
1255
1037
|
yield* reserveEntry(entry.op);
|
|
1256
|
-
const parent = directoryNow(entry.parent);
|
|
1038
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1257
1039
|
const before = parent.revision;
|
|
1258
1040
|
const now = yield* timestamp(op);
|
|
1259
|
-
attach(parent, name, node, now);
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1041
|
+
yield* attach(parent, name, node, now);
|
|
1042
|
+
const draft = yield* Draft;
|
|
1043
|
+
yield* draft.addEntries(1);
|
|
1044
|
+
yield* publishEntry("Create", parent.ino, name, node.ino);
|
|
1045
|
+
return { before, after: draft.revision };
|
|
1263
1046
|
});
|
|
1264
1047
|
const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
|
|
1265
1048
|
const name = yield* claimName(entry, "rejected");
|
|
1266
1049
|
yield* reserveEntry(entry.op);
|
|
1267
1050
|
yield* reserveBytes(entry.op, BigInt(target.length));
|
|
1268
|
-
const parent = directoryNow(entry.parent);
|
|
1051
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1269
1052
|
const before = parent.revision;
|
|
1270
1053
|
const now = yield* timestamp(op);
|
|
1271
|
-
const child = newSymlink(parent, target, now, times);
|
|
1272
|
-
attach(parent, name, child, now);
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1054
|
+
const child = yield* newSymlink(parent, target, now, times);
|
|
1055
|
+
yield* attach(parent, name, child, now);
|
|
1056
|
+
const draft = yield* Draft;
|
|
1057
|
+
yield* draft.addEntries(1);
|
|
1058
|
+
yield* draft.addUsedBytes(BigInt(target.length));
|
|
1059
|
+
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
1060
|
+
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
1277
1061
|
});
|
|
1278
1062
|
const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
|
|
1279
1063
|
const before = parent.revision;
|
|
1280
1064
|
const now = yield* timestamp(op);
|
|
1281
|
-
const d =
|
|
1282
|
-
d.put(withEntries({
|
|
1065
|
+
const d = yield* Draft;
|
|
1066
|
+
yield* d.put(withEntries({
|
|
1283
1067
|
...parent,
|
|
1284
1068
|
metadata: {
|
|
1285
1069
|
...parent.metadata,
|
|
@@ -1288,22 +1072,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1288
1072
|
ctimeNs: now
|
|
1289
1073
|
}
|
|
1290
1074
|
}, (entries) => entries.delete(name)));
|
|
1291
|
-
publishEntry("Remove", parent.ino, name, child.ino);
|
|
1292
|
-
detach(child, parent.ino, name, now);
|
|
1293
|
-
d.
|
|
1075
|
+
yield* publishEntry("Remove", parent.ino, name, child.ino);
|
|
1076
|
+
yield* detach(child, parent.ino, name, now);
|
|
1077
|
+
yield* d.addEntries(-1);
|
|
1294
1078
|
return { before, after: d.revision };
|
|
1295
1079
|
});
|
|
1296
1080
|
// Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
|
|
1297
1081
|
// entry, and each verb reports it with its own code. Search permission comes before the lookup and write
|
|
1298
1082
|
// permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
|
|
1299
1083
|
const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
|
|
1300
|
-
const parent = directoryNow(entry.parent);
|
|
1084
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1301
1085
|
if (isDotComponent(entry.name)) {
|
|
1302
1086
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
|
|
1303
1087
|
}
|
|
1304
1088
|
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
1305
1089
|
const childIno = parent.entries.get(entry.name);
|
|
1306
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1090
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1307
1091
|
if (child === undefined)
|
|
1308
1092
|
return yield* entry.op.fail("NotFound");
|
|
1309
1093
|
const rejected = beforeWrite?.(child);
|
|
@@ -1338,8 +1122,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1338
1122
|
return yield* removeChild(parent, name, child, op);
|
|
1339
1123
|
});
|
|
1340
1124
|
const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
|
|
1341
|
-
const sourceDirectory = directoryNow(source.parent);
|
|
1342
|
-
const destinationDirectory = directoryNow(destination.parent);
|
|
1125
|
+
const sourceDirectory = yield* directoryNow(source.parent);
|
|
1126
|
+
const destinationDirectory = yield* directoryNow(destination.parent);
|
|
1343
1127
|
const sameDirectory = source.parent === destination.parent;
|
|
1344
1128
|
const sourceName = source.name;
|
|
1345
1129
|
const destinationName = destination.name;
|
|
@@ -1353,11 +1137,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1353
1137
|
const sourceBefore = sourceDirectory.revision;
|
|
1354
1138
|
const destinationBefore = destinationDirectory.revision;
|
|
1355
1139
|
const childIno = sourceDirectory.entries.get(sourceName);
|
|
1356
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1140
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1357
1141
|
if (child === undefined)
|
|
1358
1142
|
return yield* source.op.fail("NotFound");
|
|
1359
1143
|
const replacedIno = destinationDirectory.entries.get(destinationName);
|
|
1360
|
-
const replaced = replacedIno === undefined ? undefined : view(replacedIno);
|
|
1144
|
+
const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
|
|
1361
1145
|
// A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
|
|
1362
1146
|
// source is one, as Linux allows.
|
|
1363
1147
|
if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
|
|
@@ -1366,24 +1150,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1366
1150
|
if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
|
|
1367
1151
|
return yield* destination.op.fail("NotDirectory");
|
|
1368
1152
|
}
|
|
1369
|
-
const result = ()
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1153
|
+
const result = Effect.fnUntraced(function* () {
|
|
1154
|
+
return sameDirectory
|
|
1155
|
+
? {
|
|
1156
|
+
_tag: "SameDirectory",
|
|
1157
|
+
directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
|
|
1158
|
+
}
|
|
1159
|
+
: {
|
|
1160
|
+
_tag: "DifferentDirectories",
|
|
1161
|
+
sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
|
|
1162
|
+
destinationDirectory: {
|
|
1163
|
+
before: destinationBefore,
|
|
1164
|
+
after: (yield* directoryNow(destination.parent)).revision
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
|
|
1380
1169
|
if (ancestor.ino === child.ino)
|
|
1381
1170
|
return yield* destination.op.fail("InvalidArgument");
|
|
1382
1171
|
if (ancestor.ino === ROOT_INO)
|
|
1383
1172
|
break;
|
|
1384
1173
|
}
|
|
1385
1174
|
if (child.ino === replaced?.ino)
|
|
1386
|
-
return result();
|
|
1175
|
+
return yield* result();
|
|
1387
1176
|
yield* authorize(sourceDirectory, identity, WRITE, source.op);
|
|
1388
1177
|
yield* authorize(destinationDirectory, identity, WRITE, destination.op);
|
|
1389
1178
|
yield* authorizeRemoval(sourceDirectory, child, source.op);
|
|
@@ -1400,9 +1189,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1400
1189
|
}
|
|
1401
1190
|
}
|
|
1402
1191
|
const now = yield* timestamp(op);
|
|
1403
|
-
const d =
|
|
1404
|
-
publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
|
|
1405
|
-
d.put(withEntries({
|
|
1192
|
+
const d = yield* Draft;
|
|
1193
|
+
yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
|
|
1194
|
+
yield* d.put(withEntries({
|
|
1406
1195
|
...sourceDirectory,
|
|
1407
1196
|
metadata: {
|
|
1408
1197
|
...sourceDirectory.metadata,
|
|
@@ -1411,8 +1200,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1411
1200
|
ctimeNs: now
|
|
1412
1201
|
}
|
|
1413
1202
|
}, (entries) => entries.delete(sourceName)));
|
|
1414
|
-
const destinationNow = directoryNow(destination.parent);
|
|
1415
|
-
d.put(withEntries({
|
|
1203
|
+
const destinationNow = yield* directoryNow(destination.parent);
|
|
1204
|
+
yield* d.put(withEntries({
|
|
1416
1205
|
...destinationNow,
|
|
1417
1206
|
metadata: {
|
|
1418
1207
|
...destinationNow.metadata,
|
|
@@ -1421,9 +1210,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1421
1210
|
ctimeNs: now
|
|
1422
1211
|
}
|
|
1423
1212
|
}, (entries) => entries.set(destinationName, child.ino)));
|
|
1424
|
-
const moved = nodeNow(child.ino);
|
|
1213
|
+
const moved = yield* nodeNow(child.ino);
|
|
1425
1214
|
if (moved.kind === "directory") {
|
|
1426
|
-
d.put({
|
|
1215
|
+
yield* d.put({
|
|
1427
1216
|
...moved,
|
|
1428
1217
|
parent: destination.parent,
|
|
1429
1218
|
name: destinationName,
|
|
@@ -1431,7 +1220,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1431
1220
|
});
|
|
1432
1221
|
}
|
|
1433
1222
|
else {
|
|
1434
|
-
d.put({
|
|
1223
|
+
yield* d.put({
|
|
1435
1224
|
...moved,
|
|
1436
1225
|
links: [
|
|
1437
1226
|
...withoutLink(moved.links, source.parent, sourceName),
|
|
@@ -1441,11 +1230,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1441
1230
|
});
|
|
1442
1231
|
}
|
|
1443
1232
|
if (replaced !== undefined) {
|
|
1444
|
-
detach(nodeNow(replaced.ino), destination.parent, destinationName, now);
|
|
1445
|
-
d.
|
|
1233
|
+
yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
|
|
1234
|
+
yield* d.addEntries(-1);
|
|
1446
1235
|
}
|
|
1447
|
-
publishEntry("Create", destination.parent, destinationName, child.ino);
|
|
1448
|
-
return result();
|
|
1236
|
+
yield* publishEntry("Create", destination.parent, destinationName, child.ino);
|
|
1237
|
+
return yield* result();
|
|
1449
1238
|
});
|
|
1450
1239
|
const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
|
|
1451
1240
|
yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
|
|
@@ -1454,7 +1243,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1454
1243
|
});
|
|
1455
1244
|
const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
|
|
1456
1245
|
const { name } = entry;
|
|
1457
|
-
const parent = directoryNow(entry.parent);
|
|
1246
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1458
1247
|
const before = parent.revision;
|
|
1459
1248
|
let file = found;
|
|
1460
1249
|
let created = false;
|
|
@@ -1481,12 +1270,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1481
1270
|
const mode = request.exactMode
|
|
1482
1271
|
? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
|
|
1483
1272
|
: (request.mode ?? 0o666) & 0o777 & ~umask;
|
|
1484
|
-
file = newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
|
|
1485
|
-
attach(parent, name, file, now);
|
|
1486
|
-
|
|
1487
|
-
|
|
1273
|
+
file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
|
|
1274
|
+
yield* attach(parent, name, file, now);
|
|
1275
|
+
const draft = yield* Draft;
|
|
1276
|
+
yield* draft.addEntries(1);
|
|
1277
|
+
yield* draft.addUsedBytes(size);
|
|
1488
1278
|
created = true;
|
|
1489
|
-
publishEntry("Create", parent.ino, name, file.ino);
|
|
1279
|
+
yield* publishEntry("Create", parent.ino, name, file.ino);
|
|
1490
1280
|
}
|
|
1491
1281
|
else {
|
|
1492
1282
|
if (file.kind === "symlink")
|
|
@@ -1495,92 +1285,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1495
1285
|
return yield* entry.op.fail("IsDirectory");
|
|
1496
1286
|
yield* openExisting(file, request, entry.op, op);
|
|
1497
1287
|
}
|
|
1498
|
-
|
|
1499
|
-
bindFile(acquired, file.ino);
|
|
1500
|
-
return { ino: file.ino, created, directory: { before, after: directoryNow(entry.parent).revision } };
|
|
1288
|
+
yield* (yield* Draft).retain(file.ino);
|
|
1289
|
+
yield* bindFile(acquired, file.ino);
|
|
1290
|
+
return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
|
|
1501
1291
|
});
|
|
1502
|
-
const releasePending = (opened) => () => {
|
|
1292
|
+
const releasePending = (opened) => () => Effect.gen(function* () {
|
|
1503
1293
|
const ino = opened();
|
|
1504
1294
|
if (ino === undefined)
|
|
1505
1295
|
return;
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
};
|
|
1296
|
+
yield* releaseFileInode(ino);
|
|
1297
|
+
});
|
|
1509
1298
|
const acquireOpenedFile = (ref, op, acquire) => {
|
|
1510
1299
|
let opened;
|
|
1511
1300
|
return acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
|
|
1512
1301
|
opened = ino;
|
|
1513
1302
|
}), releasePending(() => opened), finalizeFile(ref));
|
|
1514
1303
|
};
|
|
1515
|
-
const prepare = (input, op) => preparePath(input, op.operation, settings.maxPathBytes);
|
|
1516
|
-
const handleNode = Effect.fnUntraced(function* (handle, op) {
|
|
1517
|
-
if (reference.ino === undefined)
|
|
1518
|
-
return yield* op.fail("ClosedCaller");
|
|
1519
|
-
const ref = isFileHandle(handle) ? files.get(handle) : handles.get(handle);
|
|
1520
|
-
if (ref === undefined)
|
|
1521
|
-
return yield* op.fail("InvalidHandle");
|
|
1522
|
-
if (ref.volume !== volumeIdentity)
|
|
1523
|
-
return yield* op.fail("ForeignHandle");
|
|
1524
|
-
const node = ref.ino === undefined ? undefined : view(ref.ino);
|
|
1525
|
-
if (node === undefined)
|
|
1526
|
-
return yield* op.fail("InvalidHandle");
|
|
1527
|
-
return node;
|
|
1528
|
-
});
|
|
1529
|
-
// A path prepares before the walk, so a malformed input fails before any lookup, and names the path on
|
|
1530
|
-
// every failure. A reference or handle names none. The verb's `followFinalSymlink` is the default a
|
|
1531
|
-
// path's own setting overrides; `final` makes the verb's setting win, for a verb such as `readLink` whose
|
|
1532
|
-
// meaning fixes it.
|
|
1533
|
-
const resolveTarget = (target, op, options) => Target.$match(target, {
|
|
1534
|
-
Path: ({ followFinalSymlink, path, relativeTo }) => Effect.flatMap(Effect.fromResult(prepare(path, op)), (prepared) => Effect.map(resolveNode(prepared, relativeTo, op, {
|
|
1535
|
-
followFinalSymlink: options?.final === true
|
|
1536
|
-
? options.followFinalSymlink ?? true
|
|
1537
|
-
: followFinalSymlink ?? options?.followFinalSymlink ?? true
|
|
1538
|
-
}), (node) => ({ node, op: op.at(path) }))),
|
|
1539
|
-
Reference: ({ reference }) => Effect.map(referencedNode(reference, op), (node) => ({ node, op })),
|
|
1540
|
-
Handle: ({ handle }) => Effect.map(handleNode(handle, op), (node) => ({ node, op }))
|
|
1541
|
-
});
|
|
1542
|
-
const resolveDirectory = Effect.fnUntraced(function* (target, op) {
|
|
1543
|
-
const resolved = yield* resolveTarget(target, op);
|
|
1544
|
-
if (resolved.node.kind !== "directory")
|
|
1545
|
-
return yield* resolved.op.fail("NotDirectory");
|
|
1546
|
-
return { node: resolved.node, op: resolved.op };
|
|
1547
|
-
});
|
|
1548
|
-
// The directory an entry names a child of. A removed directory that a handle still holds takes no new
|
|
1549
|
-
// children, as on Linux, where creating through a descriptor of a removed directory fails with ENOENT.
|
|
1550
|
-
const entryDirectory = Effect.fnUntraced(function* (target, op) {
|
|
1551
|
-
const directory = yield* resolveDirectory(target, op);
|
|
1552
|
-
if (directory.node.metadata.nlink === 0)
|
|
1553
|
-
return yield* directory.op.fail("NotFound");
|
|
1554
|
-
return directory;
|
|
1555
|
-
});
|
|
1556
1304
|
const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
|
|
1557
|
-
const entryName = (input, op) => {
|
|
1558
|
-
if (Predicate.isString(input) && !isWellFormed(input))
|
|
1559
|
-
return Result.fail(op.fail("InvalidPathEncoding"));
|
|
1560
|
-
const bytes = Predicate.isString(input) ? encoder.encode(input) : input;
|
|
1561
|
-
if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
|
|
1562
|
-
bytes.includes(SLASH_BYTE))
|
|
1563
|
-
return Result.fail(op.fail("InvalidArgument"));
|
|
1564
|
-
return Result.succeed(Encoding.encodeHex(new Uint8Array(bytes)));
|
|
1565
|
-
};
|
|
1566
|
-
const prepareEntry = (input, op) => isEntry(input)
|
|
1567
|
-
? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
|
|
1568
|
-
: isTarget(input)
|
|
1569
|
-
? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
|
|
1570
|
-
: Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
|
|
1571
|
-
const resolveEntry = Effect.fnUntraced(function* (prepared, op) {
|
|
1572
|
-
if (prepared.kind === "path") {
|
|
1573
|
-
return yield* ResolvedEntry.fromPath(prepared.path, prepared.base, op);
|
|
1574
|
-
}
|
|
1575
|
-
const directory = yield* entryDirectory(prepared.directory, op);
|
|
1576
|
-
return {
|
|
1577
|
-
parent: directory.node.ino,
|
|
1578
|
-
name: prepared.name,
|
|
1579
|
-
trailingSlash: false,
|
|
1580
|
-
addressing: "entry",
|
|
1581
|
-
op
|
|
1582
|
-
};
|
|
1583
|
-
});
|
|
1584
1305
|
const asTarget = (input) => Target.of(input);
|
|
1585
1306
|
const acquireDirectory = Effect.fnUntraced(function* (input, op) {
|
|
1586
1307
|
const target = asTarget(input);
|
|
@@ -1589,12 +1310,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1589
1310
|
const directory = yield* resolveDirectory(target, op);
|
|
1590
1311
|
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
1591
1312
|
holdDirectory(directory.node.ino);
|
|
1592
|
-
acquired
|
|
1313
|
+
TokenRegistry.publish(acquired, directory.node.ino);
|
|
1593
1314
|
return acquired;
|
|
1594
1315
|
}),
|
|
1595
1316
|
// Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
|
|
1596
1317
|
// every permit, where a detached directory may leave the table.
|
|
1597
|
-
() =>
|
|
1318
|
+
() => Effect.void, finalizeDirectory(acquired));
|
|
1598
1319
|
});
|
|
1599
1320
|
const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
|
|
1600
1321
|
const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
|
|
@@ -1612,29 +1333,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1612
1333
|
const follow = target.followFinalSymlink ?? true;
|
|
1613
1334
|
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1614
1335
|
const path = yield* Effect.fromResult(prepared);
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
const resolved = yield* lookup(path, target.relativeTo, op, {
|
|
1623
|
-
followFinalSymlink: follow,
|
|
1624
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1625
|
-
});
|
|
1626
|
-
const parent = resolved.parent;
|
|
1627
|
-
if (parent === undefined)
|
|
1628
|
-
return yield* pathOp.fail("IsDirectory");
|
|
1629
|
-
const name = resolved.name;
|
|
1630
|
-
if (isDotComponent(name))
|
|
1631
|
-
return yield* pathOp.fail("IsDirectory");
|
|
1632
|
-
if (resolved.node === undefined && path.trailingSlash)
|
|
1336
|
+
const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
|
|
1337
|
+
kind: "OrCreate",
|
|
1338
|
+
create: chosen.create ?? "never",
|
|
1339
|
+
finalSymlink: follow ? "follow" : "preserve"
|
|
1340
|
+
}, op);
|
|
1341
|
+
const { parent, name } = resolved;
|
|
1342
|
+
if (resolved.kind === "missing" && path.trailingSlash)
|
|
1633
1343
|
return yield* pathOp.fail("IsDirectory");
|
|
1634
1344
|
yield* authorize(parent, identity, EXECUTE, pathOp);
|
|
1635
|
-
const file = resolved.node;
|
|
1636
|
-
if (file !== undefined && chosen.create === "exclusive")
|
|
1637
|
-
return yield* pathOp.fail("AlreadyExists");
|
|
1345
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1638
1346
|
const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
|
|
1639
1347
|
opened(result.ino);
|
|
1640
1348
|
return fileHandle(acquired);
|
|
@@ -1656,8 +1364,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1656
1364
|
if (node.metadata.nlink === 0)
|
|
1657
1365
|
return yield* resolved.op.fail("StaleReference");
|
|
1658
1366
|
yield* openExisting(node, chosen, resolved.op, op);
|
|
1659
|
-
|
|
1660
|
-
|
|
1367
|
+
const draft = yield* Draft;
|
|
1368
|
+
yield* draft.retain(node.ino);
|
|
1369
|
+
yield* bindFile(acquired, node.ino);
|
|
1661
1370
|
opened(node.ino);
|
|
1662
1371
|
return fileHandle(acquired);
|
|
1663
1372
|
}));
|
|
@@ -1675,60 +1384,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1675
1384
|
}
|
|
1676
1385
|
if (chosen.exactMode && chosen.mode === undefined)
|
|
1677
1386
|
return yield* op.fail("InvalidArgument");
|
|
1678
|
-
const relativePath = preparePath(ownedPath(nameBytes(name)), op.operation, undefined);
|
|
1679
1387
|
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
1680
1388
|
const expected = chosen.expected;
|
|
1681
1389
|
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1390
|
+
const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
|
|
1391
|
+
kind: "OrCreate",
|
|
1392
|
+
create: chosen.create ?? "never",
|
|
1393
|
+
finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
|
|
1394
|
+
beforeFollow: Effect.fnUntraced(function* (_, direct) {
|
|
1395
|
+
if (expected !== undefined) {
|
|
1396
|
+
let expectedIno;
|
|
1397
|
+
if (expected !== null) {
|
|
1398
|
+
const observed = yield* Effect.result(referencedNode(expected, op));
|
|
1399
|
+
if (Result.isFailure(observed)) {
|
|
1400
|
+
return yield* op.fail("VolumeBusy", { cause: observed.failure });
|
|
1401
|
+
}
|
|
1402
|
+
expectedIno = observed.success.ino;
|
|
1403
|
+
}
|
|
1404
|
+
if (direct?.ino !== expectedIno)
|
|
1405
|
+
return yield* op.fail("VolumeBusy");
|
|
1694
1406
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
return yield* op.fail("VolumeBusy");
|
|
1699
|
-
}
|
|
1700
|
-
if (chosen.expectedChild === null) {
|
|
1701
|
-
if (direct !== undefined)
|
|
1702
|
-
return yield* op.fail("StaleReference");
|
|
1703
|
-
}
|
|
1704
|
-
else if (chosen.expectedChild !== undefined) {
|
|
1705
|
-
const expectedChild = chosen.expectedChild;
|
|
1706
|
-
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
1707
|
-
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
1708
|
-
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
1709
|
-
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
1710
|
-
return yield* op.fail("StaleReference");
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
if (direct !== undefined && chosen.create === "exclusive")
|
|
1714
|
-
return yield* op.fail("AlreadyExists");
|
|
1715
|
-
let file = direct;
|
|
1716
|
-
let mutationParent = parent.ino;
|
|
1717
|
-
let mutationName = name;
|
|
1718
|
-
if (file?.kind === "symlink" && chosen.followFinalSymlink !== false) {
|
|
1719
|
-
const resolved = yield* lookup(yield* Effect.fromResult(relativePath), undefined, op, {
|
|
1720
|
-
followFinalSymlink: true,
|
|
1721
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1722
|
-
}, parent);
|
|
1723
|
-
file = resolved.node;
|
|
1724
|
-
if (file === undefined) {
|
|
1725
|
-
if (resolved.parent === undefined || resolved.name === undefined) {
|
|
1726
|
-
return yield* op.fail("IsDirectory");
|
|
1407
|
+
if (chosen.expectedChild === null) {
|
|
1408
|
+
if (direct !== undefined)
|
|
1409
|
+
return yield* op.fail("StaleReference");
|
|
1727
1410
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1411
|
+
else if (chosen.expectedChild !== undefined) {
|
|
1412
|
+
const expectedChild = chosen.expectedChild;
|
|
1413
|
+
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
1414
|
+
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
1415
|
+
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
1416
|
+
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
1417
|
+
return yield* op.fail("StaleReference");
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
})
|
|
1421
|
+
}, op);
|
|
1422
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1423
|
+
const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
|
|
1424
|
+
const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
|
|
1732
1425
|
const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
|
|
1733
1426
|
opened(result.ino);
|
|
1734
1427
|
return {
|
|
@@ -1739,29 +1432,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1739
1432
|
};
|
|
1740
1433
|
}));
|
|
1741
1434
|
});
|
|
1742
|
-
const walkChildren = (directory, path, depth, up)
|
|
1435
|
+
const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
|
|
1743
1436
|
const listed = referenceFor(directory.ino);
|
|
1744
|
-
|
|
1745
|
-
|
|
1437
|
+
const frames = [];
|
|
1438
|
+
for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
|
|
1439
|
+
const child = yield* view(childIno);
|
|
1746
1440
|
if (child === undefined)
|
|
1747
|
-
|
|
1441
|
+
continue;
|
|
1748
1442
|
const bytes = nameBytes(name);
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1443
|
+
frames.push({
|
|
1444
|
+
ino: child.ino,
|
|
1445
|
+
kind: child.kind,
|
|
1446
|
+
name: bytes,
|
|
1447
|
+
key: name,
|
|
1448
|
+
path: joinPath(path, bytes),
|
|
1449
|
+
parent: directory.ino,
|
|
1450
|
+
up,
|
|
1451
|
+
depth: depth + 1,
|
|
1452
|
+
bytes: child.kind === "directory" ? 0n : child.metadata.size,
|
|
1453
|
+
reference: referenceFor(child.ino),
|
|
1454
|
+
directory: listed,
|
|
1455
|
+
listed: false
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
return frames;
|
|
1459
|
+
});
|
|
1765
1460
|
const anchorFrame = (parent, key, root) => ({
|
|
1766
1461
|
ino: root.ino,
|
|
1767
1462
|
kind: root.kind,
|
|
@@ -1782,13 +1477,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1782
1477
|
const prepared = prepare(target.path, op);
|
|
1783
1478
|
if (Result.isFailure(prepared))
|
|
1784
1479
|
return Effect.undefined;
|
|
1785
|
-
return
|
|
1786
|
-
const parent = view(entry.parent);
|
|
1480
|
+
return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
|
|
1787
1481
|
return entry.name !== undefined && parent?.kind === "directory" &&
|
|
1788
1482
|
parent.entries.get(entry.name) === root.ino
|
|
1789
1483
|
? anchorFrame(parent, entry.name, root)
|
|
1790
1484
|
: undefined;
|
|
1791
|
-
}), Effect.catch(() => Effect.undefined));
|
|
1485
|
+
})), Effect.catch(() => Effect.undefined));
|
|
1792
1486
|
};
|
|
1793
1487
|
const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
|
|
1794
1488
|
// The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
|
|
@@ -1801,13 +1495,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1801
1495
|
const chain = [];
|
|
1802
1496
|
for (let link = frame; link !== undefined; link = link.up)
|
|
1803
1497
|
chain.push(link);
|
|
1804
|
-
let node = view((chain.at(-1) ?? frame).parent);
|
|
1498
|
+
let node = yield* view((chain.at(-1) ?? frame).parent);
|
|
1805
1499
|
for (let index = chain.length - 1; index >= 0; index--) {
|
|
1806
1500
|
const link = chain[index];
|
|
1807
1501
|
if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
1808
1502
|
return undefined;
|
|
1809
1503
|
yield* authorize(node, identity, EXECUTE, at);
|
|
1810
|
-
node = node.entries.get(link.key) === link.ino ? view(link.ino) : undefined;
|
|
1504
|
+
node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
|
|
1811
1505
|
}
|
|
1812
1506
|
return node;
|
|
1813
1507
|
});
|
|
@@ -1823,27 +1517,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1823
1517
|
let bytes = 0n;
|
|
1824
1518
|
let failure;
|
|
1825
1519
|
const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
|
|
1826
|
-
if (reference
|
|
1520
|
+
if (TokenRegistry.inode(reference) === undefined)
|
|
1827
1521
|
return Effect.fail(op.fail("ClosedCaller"));
|
|
1828
1522
|
const at = op.at(locate(frame.path));
|
|
1829
1523
|
return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
|
|
1830
1524
|
? Effect.succeed([])
|
|
1831
|
-
: Effect.
|
|
1525
|
+
: Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
|
|
1832
1526
|
}));
|
|
1833
|
-
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1834
1527
|
const admit = (frame) => {
|
|
1835
|
-
|
|
1836
|
-
return exceeded(frame, "maxDepth");
|
|
1837
|
-
if (plan.maxEntries !== undefined && ++entries > plan.maxEntries)
|
|
1838
|
-
return exceeded(frame, "maxEntries");
|
|
1528
|
+
entries++;
|
|
1839
1529
|
bytes += frame.bytes;
|
|
1840
|
-
|
|
1841
|
-
return exceeded(frame, "maxBytes");
|
|
1842
|
-
return undefined;
|
|
1530
|
+
return plan.limit(frame, { entries, bytes }, "entry");
|
|
1843
1531
|
};
|
|
1844
1532
|
const step = Effect.gen(function* () {
|
|
1533
|
+
// Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
|
|
1534
|
+
// oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
|
|
1845
1535
|
if (failure !== undefined)
|
|
1846
|
-
return yield* failure;
|
|
1536
|
+
return yield* Effect.fail(failure);
|
|
1847
1537
|
if (pending === undefined)
|
|
1848
1538
|
pending = (yield* first).reverse();
|
|
1849
1539
|
const out = [];
|
|
@@ -1865,9 +1555,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1865
1555
|
out.push(frame);
|
|
1866
1556
|
}
|
|
1867
1557
|
if (unlisted) {
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1558
|
+
const rejected = plan.limit(frame, { entries, bytes }, "directory");
|
|
1559
|
+
if (rejected !== undefined)
|
|
1560
|
+
return yield* stop(rejected);
|
|
1871
1561
|
const listed = yield* Effect.result(list(frame));
|
|
1872
1562
|
if (Result.isFailure(listed))
|
|
1873
1563
|
return yield* stop(listed.failure);
|
|
@@ -1900,10 +1590,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1900
1590
|
let targetAt = preparedOp(prepared, op);
|
|
1901
1591
|
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
1902
1592
|
const entry = yield* resolveEntry(prepared, op);
|
|
1903
|
-
const parent = directoryNow(entry.parent);
|
|
1593
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1904
1594
|
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
1905
1595
|
const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
|
|
1906
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1596
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1907
1597
|
if (entry.name === undefined || child === undefined)
|
|
1908
1598
|
return yield* entry.op.fail("NotFound");
|
|
1909
1599
|
if (child.kind !== "directory")
|
|
@@ -1911,16 +1601,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1911
1601
|
yield* listable(child, entry.op);
|
|
1912
1602
|
anchor = anchorFrame(parent, entry.name, child);
|
|
1913
1603
|
targetAt = entry.op;
|
|
1914
|
-
return walkChildren(child, new Uint8Array(0), 0, anchor);
|
|
1604
|
+
return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
|
|
1915
1605
|
})).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
|
|
1916
1606
|
const seams = yield* VolumeTestSeams;
|
|
1917
1607
|
const removeFrame = (frame) => {
|
|
1918
1608
|
const at = op.at(locate(frame.path));
|
|
1919
|
-
return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.
|
|
1920
|
-
if (anchor !== undefined && !holds(view(anchor.parent), anchor)) {
|
|
1921
|
-
return force ? Effect.void : Effect.fail(targetAt.fail("NotFound"));
|
|
1609
|
+
return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
|
|
1610
|
+
if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
|
|
1611
|
+
return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
|
|
1922
1612
|
}
|
|
1923
|
-
return Effect.flatMap(frame.up === undefined ?
|
|
1613
|
+
return yield* Effect.flatMap(frame.up === undefined ? view(frame.parent) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
|
|
1924
1614
|
? Effect.fail(at.fail("NotFound"))
|
|
1925
1615
|
: removeEntry({
|
|
1926
1616
|
parent: parent.ino,
|
|
@@ -1931,7 +1621,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1931
1621
|
}, op));
|
|
1932
1622
|
})));
|
|
1933
1623
|
};
|
|
1934
|
-
const plan = { order: "post",
|
|
1624
|
+
const plan = { order: "post", limit: () => undefined };
|
|
1935
1625
|
return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
|
|
1936
1626
|
});
|
|
1937
1627
|
const rootOp = OpContext.make("root");
|
|
@@ -1940,9 +1630,40 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1940
1630
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1941
1631
|
return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
|
|
1942
1632
|
});
|
|
1633
|
+
const openDispatch = Effect.fn("Caller.open")(function* (input, options) {
|
|
1634
|
+
const op = OpContext.make("open");
|
|
1635
|
+
if (isEntry(input))
|
|
1636
|
+
return yield* openEntry(input, options, op);
|
|
1637
|
+
const target = asTarget(input);
|
|
1638
|
+
if (Target.$is("Path")(target))
|
|
1639
|
+
return yield* openPath(target, options, op);
|
|
1640
|
+
return yield* openNode(target, options, op);
|
|
1641
|
+
});
|
|
1642
|
+
function open(input, options) {
|
|
1643
|
+
return openDispatch(input, options);
|
|
1644
|
+
}
|
|
1645
|
+
const removeDispatch = Effect.fn("Caller.remove")(function* (input, options) {
|
|
1646
|
+
const op = OpContext.make("remove");
|
|
1647
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1648
|
+
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
1649
|
+
const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
|
|
1650
|
+
const target = (effect) => chosen.force === true
|
|
1651
|
+
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
1652
|
+
: effect;
|
|
1653
|
+
const removed = yield* Effect.result(target(once));
|
|
1654
|
+
if (Result.isSuccess(removed))
|
|
1655
|
+
return removed.success;
|
|
1656
|
+
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
1657
|
+
return yield* removed.failure;
|
|
1658
|
+
yield* emptyDirectory(prepared, op, chosen.force === true);
|
|
1659
|
+
return yield* target(once);
|
|
1660
|
+
});
|
|
1661
|
+
function remove(input, options) {
|
|
1662
|
+
return removeDispatch(input, options);
|
|
1663
|
+
}
|
|
1943
1664
|
const caller = Object.freeze({
|
|
1944
1665
|
[CallerId]: true,
|
|
1945
|
-
root: coordinatedRead(rootOp, Effect.suspend(() => reference
|
|
1666
|
+
root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
|
|
1946
1667
|
? Effect.fail(rootOp.fail("ClosedCaller"))
|
|
1947
1668
|
: Effect.succeed(referenceFor(ROOT_INO)))).pipe(Effect.withSpan("Caller.root")),
|
|
1948
1669
|
lookup: Effect.fn("Caller.lookup")(function* (input) {
|
|
@@ -1950,7 +1671,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1950
1671
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1951
1672
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
1952
1673
|
const entry = yield* resolveEntry(prepared, op);
|
|
1953
|
-
const directory = directoryNow(entry.parent);
|
|
1674
|
+
const directory = yield* directoryNow(entry.parent);
|
|
1954
1675
|
if (isDotComponent(entry.name)) {
|
|
1955
1676
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
|
|
1956
1677
|
}
|
|
@@ -1997,13 +1718,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1997
1718
|
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
1998
1719
|
const directory = yield* resolveDirectory(target, op);
|
|
1999
1720
|
yield* authorize(directory.node, identity, READ, directory.op);
|
|
2000
|
-
return walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
1721
|
+
return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
2001
1722
|
}));
|
|
1723
|
+
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1724
|
+
const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
|
|
2002
1725
|
const plan = {
|
|
2003
1726
|
order: chosen.order ?? "pre",
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
1727
|
+
limit: (frame, progress, phase) => {
|
|
1728
|
+
if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
|
|
1729
|
+
return exceeded(frame, "maxDepth");
|
|
1730
|
+
if (phase === "directory")
|
|
1731
|
+
return undefined;
|
|
1732
|
+
if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
|
|
1733
|
+
return exceeded(frame, "maxEntries");
|
|
1734
|
+
}
|
|
1735
|
+
return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
|
|
1736
|
+
}
|
|
2007
1737
|
};
|
|
2008
1738
|
return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
|
|
2009
1739
|
path: ownedPath(frame.path),
|
|
@@ -2031,17 +1761,21 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2031
1761
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2032
1762
|
if (Target.$is("Path")(target)) {
|
|
2033
1763
|
const pathOp = op.at(target.path);
|
|
2034
|
-
const result = yield*
|
|
2035
|
-
const directory = result.node
|
|
2036
|
-
|
|
1764
|
+
const result = yield* resolveTarget(target, op, { final: true });
|
|
1765
|
+
const directory = result.node.kind === "directory"
|
|
1766
|
+
? result.node
|
|
1767
|
+
: result.kind === "entry"
|
|
1768
|
+
? result.parent
|
|
1769
|
+
: undefined;
|
|
1770
|
+
const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
|
|
2037
1771
|
if (prefix === undefined)
|
|
2038
1772
|
return yield* pathOp.fail("NotFound");
|
|
2039
|
-
if (result.node
|
|
1773
|
+
if (result.node.kind === "directory" || result.kind === "node")
|
|
2040
1774
|
return ownedPath(nameBytes(prefix));
|
|
2041
1775
|
return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
|
|
2042
1776
|
}
|
|
2043
1777
|
const resolved = yield* resolveTarget(target, op);
|
|
2044
|
-
const path =
|
|
1778
|
+
const path = yield* pathOfCurrent(resolved.node.ino);
|
|
2045
1779
|
if (path === undefined)
|
|
2046
1780
|
return yield* resolved.op.fail("NotFound");
|
|
2047
1781
|
return ownedPath(nameBytes(path));
|
|
@@ -2091,66 +1825,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2091
1825
|
const captured = new Uint8Array(bytes);
|
|
2092
1826
|
const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
2093
1827
|
return yield* coordinated(op, Effect.gen(function* () {
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
return yield* entryOp.fail("AlreadyExists");
|
|
2106
|
-
if (exists.failure.code !== "NotFound")
|
|
2107
|
-
return yield* exists.failure;
|
|
2108
|
-
}
|
|
2109
|
-
const resolved = yield* lookup(path, prepared.base, op, {
|
|
2110
|
-
followFinalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false,
|
|
2111
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
2112
|
-
});
|
|
2113
|
-
if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
|
|
2114
|
-
return yield* entryOp.fail("IsDirectory");
|
|
2115
|
-
}
|
|
2116
|
-
parent = resolved.parent;
|
|
2117
|
-
name = resolved.name;
|
|
2118
|
-
found = resolved.node;
|
|
2119
|
-
trailingSlash = path.trailingSlash;
|
|
2120
|
-
}
|
|
2121
|
-
else {
|
|
2122
|
-
const directory = yield* entryDirectory(prepared.directory, op);
|
|
2123
|
-
if (isDotComponent(prepared.name))
|
|
2124
|
-
return yield* op.fail("InvalidArgument");
|
|
2125
|
-
yield* authorize(directory.node, identity, EXECUTE, op);
|
|
2126
|
-
parent = directory.node;
|
|
2127
|
-
name = prepared.name;
|
|
2128
|
-
const childIno = parent.entries.get(name);
|
|
2129
|
-
let child = childIno === undefined ? undefined : view(childIno);
|
|
2130
|
-
if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
|
|
2131
|
-
chosen.followFinalSymlink !== false) {
|
|
2132
|
-
const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
|
|
2133
|
-
followFinalSymlink: true,
|
|
2134
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
2135
|
-
}, parent);
|
|
2136
|
-
if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
|
|
2137
|
-
return yield* op.fail("IsDirectory");
|
|
2138
|
-
}
|
|
2139
|
-
parent = resolved.parent;
|
|
2140
|
-
name = resolved.name;
|
|
2141
|
-
child = resolved.node;
|
|
2142
|
-
}
|
|
2143
|
-
if (child === undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
2144
|
-
return yield* op.fail("NotFound");
|
|
2145
|
-
}
|
|
2146
|
-
if (child !== undefined && chosen.create === "exclusive")
|
|
2147
|
-
return yield* op.fail("AlreadyExists");
|
|
2148
|
-
if (child?.kind === "directory")
|
|
2149
|
-
return yield* op.fail("IsDirectory");
|
|
2150
|
-
found = child;
|
|
2151
|
-
}
|
|
2152
|
-
if (found === undefined && trailingSlash)
|
|
1828
|
+
const resolved = yield* resolver.resolve(prepared, {
|
|
1829
|
+
kind: "OrCreate",
|
|
1830
|
+
create: chosen.create ?? "never",
|
|
1831
|
+
finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
|
|
1832
|
+
? "follow"
|
|
1833
|
+
: "preserve",
|
|
1834
|
+
entryExclusive: "afterSymlink"
|
|
1835
|
+
}, op);
|
|
1836
|
+
const { parent, name, trailingSlash, op: entryOp } = resolved;
|
|
1837
|
+
const found = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1838
|
+
if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
|
|
2153
1839
|
return yield* entryOp.fail("IsDirectory");
|
|
1840
|
+
}
|
|
2154
1841
|
const replaced = found?.kind === "symlink" ? found : undefined;
|
|
2155
1842
|
if (replaced !== undefined && !chosen.replaceFinalSymlink)
|
|
2156
1843
|
return yield* entryOp.fail("SymlinkLoop");
|
|
@@ -2161,7 +1848,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2161
1848
|
yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
|
|
2162
1849
|
if (replaced !== undefined)
|
|
2163
1850
|
yield* authorizeRemoval(parent, replaced, entryOp);
|
|
2164
|
-
yield* replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp);
|
|
1851
|
+
yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
|
|
2165
1852
|
}
|
|
2166
1853
|
else {
|
|
2167
1854
|
yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
|
|
@@ -2186,8 +1873,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2186
1873
|
data.set(captured, position);
|
|
2187
1874
|
}
|
|
2188
1875
|
const now = yield* timestamp(op);
|
|
2189
|
-
const d =
|
|
2190
|
-
const node = file ??
|
|
1876
|
+
const d = yield* Draft;
|
|
1877
|
+
const node = file ??
|
|
1878
|
+
(yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
|
|
2191
1879
|
const written = {
|
|
2192
1880
|
...node,
|
|
2193
1881
|
data,
|
|
@@ -2199,32 +1887,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2199
1887
|
ctimeNs: now
|
|
2200
1888
|
}
|
|
2201
1889
|
};
|
|
2202
|
-
d.
|
|
1890
|
+
yield* d.addUsedBytes(BigInt(size - previous));
|
|
2203
1891
|
if (file === undefined) {
|
|
2204
1892
|
if (replaced !== undefined)
|
|
2205
|
-
detach(replaced, parent.ino, name, now);
|
|
2206
|
-
attach(directoryNow(parent.ino), name, written, now);
|
|
1893
|
+
yield* detach(replaced, parent.ino, name, now);
|
|
1894
|
+
yield* attach(yield* directoryNow(parent.ino), name, written, now);
|
|
2207
1895
|
if (replaced === undefined)
|
|
2208
|
-
d.
|
|
2209
|
-
publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
|
|
1896
|
+
yield* d.addEntries(1);
|
|
1897
|
+
yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
|
|
2210
1898
|
}
|
|
2211
1899
|
else {
|
|
2212
|
-
d.put(written);
|
|
2213
|
-
publishNode(written.ino);
|
|
1900
|
+
yield* d.put(written);
|
|
1901
|
+
yield* publishNode(written.ino);
|
|
2214
1902
|
}
|
|
2215
1903
|
}));
|
|
2216
1904
|
}),
|
|
2217
|
-
|
|
2218
|
-
// reject the other shape, so one implementation serves both.
|
|
2219
|
-
open: Effect.fn("Caller.open")(function* (input, options) {
|
|
2220
|
-
const op = OpContext.make("open");
|
|
2221
|
-
if (isEntry(input))
|
|
2222
|
-
return yield* openEntry(input, options, op);
|
|
2223
|
-
const target = asTarget(input);
|
|
2224
|
-
if (Target.$is("Path")(target))
|
|
2225
|
-
return yield* openPath(target, options, op);
|
|
2226
|
-
return yield* openNode(target, options, op);
|
|
2227
|
-
}),
|
|
1905
|
+
open,
|
|
2228
1906
|
mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
|
|
2229
1907
|
const op = OpContext.make("mkdir");
|
|
2230
1908
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
@@ -2282,23 +1960,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2282
1960
|
rmdir: Effect.fn("Caller.rmdir")(function* (input) {
|
|
2283
1961
|
return yield* entryVerb("rmdir", input, rmdirEntry);
|
|
2284
1962
|
}),
|
|
2285
|
-
|
|
2286
|
-
remove: Effect.fn("Caller.remove")(function* (input, options) {
|
|
2287
|
-
const op = OpContext.make("remove");
|
|
2288
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
2289
|
-
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
2290
|
-
const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
|
|
2291
|
-
const target = (effect) => chosen.force === true
|
|
2292
|
-
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
2293
|
-
: effect;
|
|
2294
|
-
const removed = yield* Effect.result(target(once));
|
|
2295
|
-
if (Result.isSuccess(removed))
|
|
2296
|
-
return removed.success;
|
|
2297
|
-
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
2298
|
-
return yield* removed.failure;
|
|
2299
|
-
yield* emptyDirectory(prepared, op, chosen.force === true);
|
|
2300
|
-
return yield* target(once);
|
|
2301
|
-
}),
|
|
1963
|
+
remove,
|
|
2302
1964
|
rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
|
|
2303
1965
|
const op = OpContext.make("rename");
|
|
2304
1966
|
const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
|
|
@@ -2371,21 +2033,21 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2371
2033
|
const statOp = OpContext.make("stat");
|
|
2372
2034
|
const handle = Object.freeze({
|
|
2373
2035
|
[DirectoryHandleId]: true,
|
|
2374
|
-
stat: coordinatedRead(statOp, Effect.
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2036
|
+
stat: coordinatedRead(statOp, Effect.gen(function* () {
|
|
2037
|
+
const ino = TokenRegistry.inode(acquired);
|
|
2038
|
+
const node = ino === undefined ? undefined : yield* view(ino);
|
|
2039
|
+
return yield* (node === undefined
|
|
2377
2040
|
? Effect.fail(statOp.fail("InvalidHandle"))
|
|
2378
|
-
: Effect.succeed(withMetadata(node));
|
|
2041
|
+
: Effect.succeed(withMetadata(node)));
|
|
2379
2042
|
})).pipe(Effect.withSpan("DirectoryHandle.stat")),
|
|
2380
2043
|
close: coordinatedCleanup(Effect.suspend(() => {
|
|
2381
|
-
if (acquired
|
|
2044
|
+
if (TokenRegistry.inode(acquired) === undefined) {
|
|
2382
2045
|
return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
|
|
2383
2046
|
}
|
|
2384
|
-
releaseDirectory(acquired);
|
|
2385
|
-
return Effect.void;
|
|
2047
|
+
return releaseDirectory(acquired);
|
|
2386
2048
|
})).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
|
|
2387
2049
|
});
|
|
2388
|
-
|
|
2050
|
+
registry.registerDirectory(handle, acquired);
|
|
2389
2051
|
return handle;
|
|
2390
2052
|
})
|
|
2391
2053
|
});
|
|
@@ -2416,32 +2078,27 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2416
2078
|
snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))).pipe(Effect.withSpan("Volume.snapshot")),
|
|
2417
2079
|
referenceKey: Effect.fn("Volume.referenceKey")(function* (reference) {
|
|
2418
2080
|
const op = OpContext.make("referenceKey");
|
|
2419
|
-
return yield* coordinatedRead(op, Effect.
|
|
2420
|
-
const
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
if (known.volume !== volumeIdentity)
|
|
2424
|
-
return Effect.fail(op.fail("ForeignReference"));
|
|
2425
|
-
if (!addressable(known.ino))
|
|
2426
|
-
return Effect.fail(op.fail("StaleReference"));
|
|
2427
|
-
const ino = BigInt(known.ino);
|
|
2428
|
-
return Effect.succeed({ identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) });
|
|
2081
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2082
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
|
|
2083
|
+
const ino = BigInt(node.ino);
|
|
2084
|
+
return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
|
|
2429
2085
|
}));
|
|
2430
2086
|
}),
|
|
2431
2087
|
resolveReferenceKey: Effect.fn("Volume.resolveReferenceKey")(function* (key) {
|
|
2432
2088
|
const op = OpContext.make("resolveReferenceKey");
|
|
2433
|
-
return yield* coordinatedRead(op, Effect.
|
|
2089
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2434
2090
|
if (!Schema.is(ReferenceKey)(key))
|
|
2435
|
-
return
|
|
2091
|
+
return yield* op.fail("InvalidReference");
|
|
2436
2092
|
// Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
|
|
2437
2093
|
if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
|
|
2438
|
-
return
|
|
2094
|
+
return yield* op.fail("ForeignReference");
|
|
2439
2095
|
}
|
|
2440
2096
|
// Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
|
|
2441
2097
|
if (!sameTag(key.tag, keyTag(key.ino)))
|
|
2442
|
-
return
|
|
2098
|
+
return yield* op.fail("InvalidReference");
|
|
2443
2099
|
const ino = Ino(Number(key.ino));
|
|
2444
|
-
|
|
2100
|
+
yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
|
|
2101
|
+
return referenceFor(ino);
|
|
2445
2102
|
}));
|
|
2446
2103
|
}),
|
|
2447
2104
|
caller: Effect.fn("Volume.caller")(function* (options) {
|
|
@@ -2455,47 +2112,37 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2455
2112
|
changes: coordinatedRead(OpContext.make("changes"), Effect.suspend(() => observeChanges(state))),
|
|
2456
2113
|
capture: coordinatedRead(OpContext.make("capture"), captureState())
|
|
2457
2114
|
});
|
|
2458
|
-
return Object.freeze({ volume, shutdown,
|
|
2115
|
+
return Object.freeze({ volume, shutdown, observe });
|
|
2459
2116
|
});
|
|
2460
2117
|
/** @internal */
|
|
2461
2118
|
export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
|
|
2462
|
-
return (yield* makeVolume(VolumeSource.Empty(), options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
|
|
2119
|
+
return (yield* makeVolume(VolumeSource.Empty(), memoryCommitProvider, options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
|
|
2463
2120
|
});
|
|
2464
2121
|
/** @internal */
|
|
2465
2122
|
export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2123
|
+
const decoded = yield* Effect.fromResult(Limits.configuration(options, "prepareEmptyImage"));
|
|
2124
|
+
const limits = decoded.limits;
|
|
2125
|
+
const identity = decoded.identity ?? VolumeIdentity.make(yield* randomHex128);
|
|
2126
|
+
const epoch = VolumeEpoch.make(yield* randomHex128);
|
|
2127
|
+
const keySecret = KeySecret.make(yield* randomHex128);
|
|
2128
|
+
const now = yield* Clock.currentTimeNanos;
|
|
2129
|
+
if (!isTimestamp(now))
|
|
2130
|
+
return yield* argumentFailure("prepareEmptyImage", "clock.currentTimeNanos");
|
|
2131
|
+
return yield* LiveImage.encode(emptyState(now), { identity, epoch, keySecret }, limits).pipe(Effect.mapError((error) => retargetFailure("prepareEmptyImage", error)));
|
|
2470
2132
|
});
|
|
2471
2133
|
/** @internal */
|
|
2472
|
-
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only") {
|
|
2134
|
+
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only", options) {
|
|
2473
2135
|
const restored = yield* LiveImage.decode(image, maxImageBytes);
|
|
2474
|
-
|
|
2475
|
-
const
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
maxPendingOperations: 64,
|
|
2483
|
-
maxWatchEvents: 256
|
|
2136
|
+
const requested = yield* Effect.fromResult(Limits.fromOptions(options, "openImage"));
|
|
2137
|
+
const limits = yield* Effect.fromResult(Limits.fromStored(restored.limits, requested));
|
|
2138
|
+
const provider = {
|
|
2139
|
+
mode: "durable",
|
|
2140
|
+
shutdown: Effect.void,
|
|
2141
|
+
commit: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => makeError({ code: "StorageRejected", operation: "commit", cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
|
|
2142
|
+
? Effect.fail(makeError({ code: "StorageRejected", operation: "commit" }))
|
|
2143
|
+
: commit(bytes)))
|
|
2484
2144
|
};
|
|
2485
|
-
const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }),
|
|
2486
|
-
prepare: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => commitOp.fail("StorageRejected", { cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
|
|
2487
|
-
? commitOp.fail("StorageRejected")
|
|
2488
|
-
: Effect.sync(() => {
|
|
2489
|
-
prepared = bytes;
|
|
2490
|
-
}))),
|
|
2491
|
-
commit: () => Effect.suspend(() => {
|
|
2492
|
-
const bytes = prepared;
|
|
2493
|
-
prepared = undefined;
|
|
2494
|
-
return bytes === undefined ? Effect.succeed("unknown") : commit(bytes);
|
|
2495
|
-
})
|
|
2496
|
-
}, undefined, durability);
|
|
2497
|
-
if (shutdown === undefined)
|
|
2498
|
-
return yield* imageFailure("openImage", "InvalidStructure", { field: "liveImage" });
|
|
2145
|
+
const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), provider, options, durability);
|
|
2499
2146
|
return Object.freeze({ volume, shutdown });
|
|
2500
2147
|
});
|
|
2501
2148
|
/** @internal */
|
|
@@ -2508,12 +2155,12 @@ const changeOptions = (options) => {
|
|
|
2508
2155
|
};
|
|
2509
2156
|
/** @internal */
|
|
2510
2157
|
export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
|
|
2511
|
-
return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), options)).volume;
|
|
2158
|
+
return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), memoryCommitProvider, options)).volume;
|
|
2512
2159
|
}, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
|
|
2513
2160
|
/** @internal */
|
|
2514
2161
|
export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
|
|
2515
2162
|
const value = yield* Image.valueOf(base);
|
|
2516
|
-
const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), options), (error) => retargetFailure("makeOverlay", error));
|
|
2163
|
+
const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), memoryCommitProvider, options), (error) => retargetFailure("makeOverlay", error));
|
|
2517
2164
|
const baseObservation = yield* observeChanges(value);
|
|
2518
2165
|
const overlay = Object.freeze({
|
|
2519
2166
|
...made.volume,
|