@effect-vfs/core 0.6.0 → 0.7.1
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 +14 -9
- package/dist/BytePath.d.ts +2 -1
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/LiveVolume.d.ts +6 -3
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +2 -0
- package/dist/Snapshot.d.ts +2 -1
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +2 -1
- package/dist/Testing.d.ts +9 -5
- package/dist/Testing.d.ts.map +1 -1
- package/dist/Testing.js +7 -18
- package/dist/VfsError.d.ts +2 -1
- package/dist/VfsError.d.ts.map +1 -1
- package/dist/VfsError.js +2 -1
- package/dist/VirtualFileSystem.d.ts +89 -47
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +62 -29
- package/dist/internal/canonicalBase64.js +3 -3
- package/dist/internal/fixture.d.ts +1 -1
- package/dist/internal/fixture.d.ts.map +1 -1
- package/dist/internal/overlayDiff.d.ts.map +1 -1
- package/dist/internal/overlayDiff.js +6 -14
- package/dist/internal/path.d.ts.map +1 -1
- package/dist/internal/path.js +3 -3
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +9 -15
- package/dist/internal/tree.d.ts.map +1 -1
- package/dist/internal/tree.js +14 -14
- package/dist/internal/virtualFileSystem.d.ts +13 -7
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +442 -399
- package/dist/internal/volumeState.d.ts +3 -2
- package/dist/internal/volumeState.d.ts.map +1 -1
- package/dist/internal/volumeState.js +28 -30
- package/package.json +7 -7
- package/dist/internal/stagedState.d.ts +0 -18
- package/dist/internal/stagedState.d.ts.map +0 -1
- package/dist/internal/stagedState.js +0 -18
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
1
2
|
import * as ByteSize from "effect/ByteSize";
|
|
2
3
|
import * as Clock from "effect/Clock";
|
|
4
|
+
import * as Context from "effect/Context";
|
|
3
5
|
import * as Crypto from "effect/Crypto";
|
|
4
6
|
import * as Data from "effect/Data";
|
|
5
7
|
import * as Effect from "effect/Effect";
|
|
6
|
-
import * as
|
|
8
|
+
import * as Hex from "effect/encoding/Hex";
|
|
7
9
|
import * as Exit from "effect/Exit";
|
|
10
|
+
import * as Match from "effect/Match";
|
|
11
|
+
import * as MutableRef from "effect/MutableRef";
|
|
8
12
|
import * as Option from "effect/Option";
|
|
9
13
|
import * as Predicate from "effect/Predicate";
|
|
10
|
-
import * as Random from "effect/Random";
|
|
11
14
|
import * as Result from "effect/Result";
|
|
12
15
|
import * as Schema from "effect/Schema";
|
|
13
16
|
import * as Scope from "effect/Scope";
|
|
@@ -31,10 +34,9 @@ import * as LiveImage from "./liveImage.js";
|
|
|
31
34
|
import * as MetadataDomain from "./metadata.js";
|
|
32
35
|
import { compareOverlay } from "./overlayDiff.js";
|
|
33
36
|
import { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, isWellFormed, joinPath, MAX_NAME_BYTES, MAX_SYMLINK_TRAVERSALS, nameBytes, ownedPath, preparePath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
|
|
34
|
-
import { offerCommit } from "./stagedState.js";
|
|
35
37
|
import { VolumeTestSeams } from "./testSeams.js";
|
|
36
38
|
import { makeTurnstile } from "./turnstile.js";
|
|
37
|
-
import { directoryMetadata, emptyRoot, getNode, Ino, MAX_FILE_BYTES, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
39
|
+
import { byEntryName, directoryMetadata, emptyRoot, getNode, Ino, MAX_FILE_BYTES, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
38
40
|
import * as WatchHub from "./watchHub.js";
|
|
39
41
|
const EXECUTE = 0o1;
|
|
40
42
|
const WRITE = 0o2;
|
|
@@ -43,6 +45,20 @@ const ANY_EXECUTE = 0o111;
|
|
|
43
45
|
const SET_ID_BITS = 0o6000;
|
|
44
46
|
const STICKY_BIT = 0o1000;
|
|
45
47
|
const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
|
|
48
|
+
// An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
|
|
49
|
+
const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
|
|
50
|
+
// TODO(#186): Preserve the cause of a provider defect or interruption.
|
|
51
|
+
if (Exit.isFailure(committed)) {
|
|
52
|
+
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
53
|
+
}
|
|
54
|
+
if (committed.value === "rejected") {
|
|
55
|
+
return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
|
|
56
|
+
}
|
|
57
|
+
if (committed.value === "unknown") {
|
|
58
|
+
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
59
|
+
}
|
|
60
|
+
return { available: true, failure: undefined };
|
|
61
|
+
});
|
|
46
62
|
const WALK_CHUNK_ENTRIES = 128;
|
|
47
63
|
const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
|
|
48
64
|
const isMode = Schema.is(Mode);
|
|
@@ -55,7 +71,7 @@ const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "
|
|
|
55
71
|
const decodeTimes = Schema.decodeEffect(Times, { onExcessProperty: "error" });
|
|
56
72
|
const decodeExpected = Schema.decodeEffect(SetattrOptions.fields.expected.schema, { onExcessProperty: "error" });
|
|
57
73
|
const SETATTR_FIELDS = Object.keys(SetattrOptions.fields);
|
|
58
|
-
const timeAt = (update, value, now) => update === undefined
|
|
74
|
+
const timeAt = (update, value, now) => update === undefined ? value : Match.value(update).pipe(Match.discriminator("kind")("omit", () => value), Match.discriminator("kind")("now", () => now), Match.discriminator("kind")("value", ({ nanoseconds }) => nanoseconds), Match.exhaustive);
|
|
59
75
|
const decodeWriteFileOptions = Schema.decodeEffect(WriteFileOptions, { onExcessProperty: "error" });
|
|
60
76
|
const decodeOpenOptions = Schema.decodeEffect(OpenOptions, { onExcessProperty: "error" });
|
|
61
77
|
const decodeMkdirOptions = Schema.decodeEffect(MkdirOptions, { onExcessProperty: "error" });
|
|
@@ -70,100 +86,115 @@ const encoder = new TextEncoder();
|
|
|
70
86
|
const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
|
|
71
87
|
now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
|
|
72
88
|
const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
+
class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
|
|
90
|
+
make: Effect.fnUntraced(function* (base) {
|
|
91
|
+
const revision = base.revision + 1n;
|
|
92
|
+
const cell = MutableRef.make({
|
|
93
|
+
owner: Symbol(),
|
|
94
|
+
pending: new Map(),
|
|
95
|
+
opens: undefined,
|
|
96
|
+
nextInode: base.nextInode,
|
|
97
|
+
changed: false,
|
|
98
|
+
finished: undefined,
|
|
99
|
+
entries: base.entries,
|
|
100
|
+
usedBytes: base.usedBytes,
|
|
101
|
+
events: [],
|
|
102
|
+
after: [],
|
|
103
|
+
removed: []
|
|
104
|
+
});
|
|
105
|
+
const get = (ino) => Effect.sync(() => {
|
|
106
|
+
const draft = MutableRef.get(cell);
|
|
107
|
+
return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
|
|
108
|
+
});
|
|
109
|
+
const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
|
|
110
|
+
const put = (node, changed) => Effect.sync(() => {
|
|
111
|
+
MutableRef.update(cell, (draft) => {
|
|
112
|
+
// This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
|
|
113
|
+
draft.pending.set(node.ino, changed ? { ...node, revision } : node);
|
|
114
|
+
return { ...draft, changed: draft.changed || changed };
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
revision,
|
|
119
|
+
get,
|
|
120
|
+
put: (node) => put(node, true),
|
|
121
|
+
putQuiet: (node) => put(node, false),
|
|
122
|
+
unchanged: Effect.sync(() => {
|
|
123
|
+
const draft = MutableRef.get(cell);
|
|
124
|
+
return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
|
|
125
|
+
draft.entries === base.entries && draft.usedBytes === base.usedBytes;
|
|
126
|
+
}),
|
|
127
|
+
remove: (ino) => Effect.sync(() => {
|
|
128
|
+
const draft = MutableRef.get(cell);
|
|
129
|
+
draft.pending.set(ino, undefined);
|
|
130
|
+
draft.removed.push(ino);
|
|
131
|
+
}),
|
|
132
|
+
allocate: Effect.sync(() => {
|
|
133
|
+
const next = MutableRef.get(cell).nextInode;
|
|
134
|
+
MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
|
|
135
|
+
return next;
|
|
136
|
+
}),
|
|
137
|
+
canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
|
|
138
|
+
openCount,
|
|
139
|
+
retain: (ino) => Effect.sync(() => {
|
|
140
|
+
const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
|
|
141
|
+
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
|
|
142
|
+
}),
|
|
143
|
+
release: (ino) => Effect.sync(() => {
|
|
144
|
+
const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
|
|
145
|
+
MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
|
|
146
|
+
return count;
|
|
147
|
+
}),
|
|
148
|
+
entries: Effect.sync(() => MutableRef.get(cell).entries),
|
|
149
|
+
addEntries: (amount) => Effect.sync(() => {
|
|
150
|
+
MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
|
|
151
|
+
}),
|
|
152
|
+
usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
|
|
153
|
+
addUsedBytes: (amount) => Effect.sync(() => {
|
|
154
|
+
MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
|
|
155
|
+
}),
|
|
156
|
+
publish: (event) => Effect.sync(() => {
|
|
157
|
+
MutableRef.get(cell).events.push(event);
|
|
158
|
+
}),
|
|
159
|
+
afterInstall: (action) => Effect.sync(() => {
|
|
160
|
+
MutableRef.get(cell).after.push(action);
|
|
161
|
+
}),
|
|
162
|
+
finish: Effect.sync(() => {
|
|
163
|
+
const draft = MutableRef.get(cell);
|
|
164
|
+
if (draft.finished !== undefined)
|
|
165
|
+
return draft.finished;
|
|
166
|
+
let inodes = base.inodes;
|
|
167
|
+
for (const [ino, node] of draft.pending)
|
|
168
|
+
inodes = InodeTable.set(inodes, ino, node, draft.owner);
|
|
169
|
+
let open = base.open;
|
|
170
|
+
if (draft.opens !== undefined) {
|
|
171
|
+
const next = new Map(base.open);
|
|
172
|
+
for (const [ino, count] of draft.opens) {
|
|
173
|
+
if (count > 0)
|
|
174
|
+
next.set(ino, count);
|
|
175
|
+
else
|
|
176
|
+
next.delete(ino);
|
|
177
|
+
}
|
|
178
|
+
open = next;
|
|
179
|
+
}
|
|
180
|
+
const finished = {
|
|
181
|
+
inodes,
|
|
182
|
+
open,
|
|
183
|
+
nextInode: draft.nextInode,
|
|
184
|
+
revision: draft.changed ? revision : base.revision,
|
|
185
|
+
entries: draft.entries,
|
|
186
|
+
usedBytes: draft.usedBytes
|
|
187
|
+
};
|
|
188
|
+
MutableRef.update(cell, (current) => ({ ...current, finished }));
|
|
189
|
+
return finished;
|
|
190
|
+
}),
|
|
191
|
+
actions: Effect.sync(() => {
|
|
192
|
+
const { events, after, removed } = MutableRef.get(cell);
|
|
193
|
+
return { events, after, removed };
|
|
194
|
+
})
|
|
164
195
|
};
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
})
|
|
197
|
+
}) {
|
|
167
198
|
}
|
|
168
199
|
const objectReferences = new WeakMap();
|
|
169
200
|
const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
|
|
@@ -212,28 +243,14 @@ const observeChanges = Effect.fnUntraced(function* (captured) {
|
|
|
212
243
|
return observation;
|
|
213
244
|
});
|
|
214
245
|
const randomHex128 = Effect.gen(function* () {
|
|
215
|
-
const crypto = yield*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let hex = "";
|
|
219
|
-
for (let draw = 0; draw < 4; draw++) {
|
|
220
|
-
hex += (yield* Random.nextIntBetween(0, 0x100000000, { halfOpen: true })).toString(16).padStart(8, "0");
|
|
221
|
-
}
|
|
222
|
-
return hex;
|
|
223
|
-
});
|
|
224
|
-
// The 128-bit secret behind every key's tag, always from the platform's cryptographically secure generator. Unlike an
|
|
225
|
-
// identity or an epoch it must not follow a seedable Random: every key hands out the identity and epoch, so a
|
|
226
|
-
// reproducible secret would let one key forge the tag of any other inode. Web Crypto is global in every supported
|
|
227
|
-
// runtime, so reading it adds no service requirement; a runtime without it is a defect, not a recoverable failure.
|
|
228
|
-
const randomKeySecret = Effect.sync(() => {
|
|
229
|
-
const webCrypto = globalThis.crypto;
|
|
230
|
-
if (webCrypto === undefined) {
|
|
231
|
-
throw new Error("globalThis.crypto is unavailable, so a volume cannot draw its reference-key secret");
|
|
232
|
-
}
|
|
233
|
-
return Encoding.encodeHex(webCrypto.getRandomValues(new Uint8Array(16)));
|
|
234
|
-
});
|
|
246
|
+
const crypto = yield* Crypto.Crypto;
|
|
247
|
+
return Hex.encode(yield* crypto.randomBytes(16));
|
|
248
|
+
}).pipe(Effect.orDie);
|
|
235
249
|
/** @internal */
|
|
236
250
|
export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial, durability = "memory-only") {
|
|
251
|
+
const services = yield* Effect.context();
|
|
252
|
+
// These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
|
|
253
|
+
const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
|
|
237
254
|
const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
|
|
238
255
|
let restoredOptions = options;
|
|
239
256
|
if (live !== undefined) {
|
|
@@ -255,10 +272,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
255
272
|
: VolumeIdentity.make(settings.identity);
|
|
256
273
|
const incarnation = VolumeIncarnation.make(yield* randomHex128);
|
|
257
274
|
const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
|
|
258
|
-
const keySecret = live === undefined ? KeySecret.make(yield*
|
|
259
|
-
const identityBytes = Result.getOrThrow(
|
|
260
|
-
const epochBytes = Result.getOrThrow(
|
|
261
|
-
const keySecretBytes = Result.getOrThrow(
|
|
275
|
+
const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
|
|
276
|
+
const identityBytes = Result.getOrThrow(Hex.decode(identity));
|
|
277
|
+
const epochBytes = Result.getOrThrow(Hex.decode(epoch));
|
|
278
|
+
const keySecretBytes = Result.getOrThrow(Hex.decode(keySecret));
|
|
262
279
|
const clock = yield* Clock.clockWith(Effect.succeed);
|
|
263
280
|
const initialTime = clock.currentTimeNanosUnsafe();
|
|
264
281
|
if (!isTimestamp(initialTime)) {
|
|
@@ -339,91 +356,76 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
339
356
|
const initialImage = captureInitial === undefined
|
|
340
357
|
? undefined
|
|
341
358
|
: yield* captureInitial(state, { identity, epoch, keySecret }, limits);
|
|
342
|
-
|
|
343
|
-
const view = (ino) => draft === undefined ? getNode(state, ino) : draft.get(ino);
|
|
359
|
+
const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
|
|
344
360
|
let available = true;
|
|
345
361
|
const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
|
|
346
362
|
const watchCoordinate = (effect) => changing(effect);
|
|
347
363
|
const watchHub = yield* WatchHub.make(watchCoordinate, settings.maxWatchEvents ?? 256);
|
|
348
|
-
const install = (finished)
|
|
364
|
+
const install = Effect.fnUntraced(function* (finished) {
|
|
349
365
|
const before = state;
|
|
350
|
-
state = finished.finish
|
|
351
|
-
|
|
366
|
+
state = yield* finished.finish;
|
|
367
|
+
const actions = yield* finished.actions;
|
|
368
|
+
for (const ino of actions.removed)
|
|
352
369
|
tokens.delete(ino);
|
|
353
|
-
for (const apply of
|
|
370
|
+
for (const apply of actions.after)
|
|
354
371
|
apply();
|
|
355
372
|
const after = state;
|
|
356
|
-
watchHub.publishUnsafe(() =>
|
|
357
|
-
};
|
|
358
|
-
const transition =
|
|
359
|
-
const current =
|
|
360
|
-
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), { before, after });
|
|
374
|
+
});
|
|
375
|
+
const transition = Effect.fnUntraced(function* (change) {
|
|
376
|
+
const current = yield* Draft.make(state);
|
|
377
|
+
const value = yield* Effect.provideService(change, Draft, current);
|
|
378
|
+
return [value, current];
|
|
379
|
+
});
|
|
380
|
+
const applyDirect = Effect.fnUntraced(function* (change) {
|
|
381
|
+
const current = yield* Draft.make(state);
|
|
382
|
+
yield* Effect.provideService(change(current), Draft, current);
|
|
383
|
+
yield* install(current);
|
|
364
384
|
});
|
|
365
|
-
const applyDirect = (change) => {
|
|
366
|
-
const current = new Draft(state);
|
|
367
|
-
draft = current;
|
|
368
|
-
try {
|
|
369
|
-
change(current);
|
|
370
|
-
}
|
|
371
|
-
finally {
|
|
372
|
-
draft = undefined;
|
|
373
|
-
}
|
|
374
|
-
install(current);
|
|
375
|
-
};
|
|
376
385
|
// Offers the finished draft's value to the store before installing it. A rejected candidate is discarded and
|
|
377
386
|
// the volume stays available; an uncertain answer stops the volume. A change that is a cleanup runs its
|
|
378
387
|
// release even when the store refuses, since the handle must not stay open.
|
|
379
388
|
const committed = Effect.fnUntraced(function* (op, provider, finished, onStorageFailure) {
|
|
380
|
-
if (finished.unchanged)
|
|
381
|
-
return install(finished);
|
|
382
|
-
const next = finished.finish
|
|
389
|
+
if (yield* finished.unchanged)
|
|
390
|
+
return yield* install(finished);
|
|
391
|
+
const next = yield* finished.finish;
|
|
383
392
|
if (provider.prepare !== undefined)
|
|
384
393
|
yield* provider.prepare(next);
|
|
385
394
|
const answer = yield* offerCommit(provider, op.operation, next, onStorageFailure !== undefined);
|
|
386
395
|
if (!answer.available)
|
|
387
396
|
available = false;
|
|
388
397
|
if (answer.failure !== undefined) {
|
|
389
|
-
if (!answer.available)
|
|
390
|
-
onStorageFailure
|
|
398
|
+
if (!answer.available && onStorageFailure !== undefined)
|
|
399
|
+
yield* onStorageFailure();
|
|
391
400
|
return yield* answer.failure;
|
|
392
401
|
}
|
|
393
|
-
install(finished);
|
|
402
|
+
yield* install(finished);
|
|
394
403
|
});
|
|
395
404
|
const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
|
|
396
405
|
// Permit waits stay interruptible. A change and its publication run under every permit; with a store, the
|
|
397
406
|
// change itself stays interruptible and only the commit and installation are not.
|
|
398
407
|
const coordinated = (op, effect, onStorageFailure) => admit(op, changing(commitProvider === undefined
|
|
399
|
-
? Effect.uninterruptible(Effect.
|
|
400
|
-
install(finished);
|
|
401
|
-
return value;
|
|
402
|
-
}))
|
|
408
|
+
? Effect.uninterruptible(Effect.flatMap(transition(Effect.andThen(checkAvailable(op.operation), effect)), ([value, finished]) => Effect.as(install(finished), value)))
|
|
403
409
|
: 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));
|
|
404
410
|
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)
|
|
411
|
+
const refreshAccess = Effect.fnUntraced(function* (read) {
|
|
406
412
|
if (refreshDue(read)) {
|
|
407
413
|
const { node, now } = read.access;
|
|
408
|
-
|
|
414
|
+
const draft = yield* Draft;
|
|
415
|
+
yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
|
|
409
416
|
}
|
|
410
417
|
return read.value;
|
|
411
|
-
};
|
|
418
|
+
});
|
|
412
419
|
// A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
|
|
413
420
|
// only when the access time is due does it run again as a change. The change repeats every check and the rule,
|
|
414
421
|
// since another read may have refreshed the time meanwhile, and its result is the one returned.
|
|
415
422
|
const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
|
|
416
|
-
? coordinated(op, Effect.
|
|
423
|
+
? coordinated(op, Effect.flatMap(read, refreshAccess))
|
|
417
424
|
: Effect.succeed(observed.value));
|
|
418
425
|
const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
|
|
419
426
|
const shutdown = commitProvider === undefined ? undefined : changing(Effect.sync(() => {
|
|
420
427
|
available = false;
|
|
421
428
|
}));
|
|
422
|
-
const current = () => {
|
|
423
|
-
if (draft === undefined)
|
|
424
|
-
throw new Error("Volume mutation outside a transition");
|
|
425
|
-
return draft;
|
|
426
|
-
};
|
|
427
429
|
const pathOf = (get, ino) => {
|
|
428
430
|
const names = [];
|
|
429
431
|
let node = get(ino);
|
|
@@ -438,19 +440,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
438
440
|
}
|
|
439
441
|
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
440
442
|
};
|
|
443
|
+
const pathOfCurrent = Effect.fnUntraced(function* (ino) {
|
|
444
|
+
const names = [];
|
|
445
|
+
const first = yield* view(ino);
|
|
446
|
+
if (first?.kind !== "directory" || first.metadata.nlink === 0)
|
|
447
|
+
return undefined;
|
|
448
|
+
let node = first;
|
|
449
|
+
while (node.ino !== ROOT_INO) {
|
|
450
|
+
names.push(node.name);
|
|
451
|
+
const parent = yield* view(node.parent);
|
|
452
|
+
if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
|
|
453
|
+
return undefined;
|
|
454
|
+
node = parent;
|
|
455
|
+
}
|
|
456
|
+
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
457
|
+
});
|
|
441
458
|
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);
|
|
459
|
+
const publishEntry = (_tag, parent, name, ino) => Effect.flatMap(Draft, (draft) => draft.publish((installed) => {
|
|
460
|
+
const prefix = pathOf((at) => getNode(installed, at), parent);
|
|
461
|
+
return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
|
|
462
|
+
}));
|
|
463
|
+
const publishNode = Effect.fnUntraced(function* (ino) {
|
|
464
|
+
const draft = yield* Draft;
|
|
465
|
+
const target = yield* draft.get(ino);
|
|
450
466
|
if (target === undefined)
|
|
451
467
|
return;
|
|
452
468
|
if (target.kind === "directory") {
|
|
453
|
-
|
|
469
|
+
yield* draft.publish((installed) => {
|
|
454
470
|
const directory = getNode(installed, ino);
|
|
455
471
|
const path = pathOf((at) => getNode(installed, at), ino);
|
|
456
472
|
return path === undefined || directory?.kind !== "directory"
|
|
@@ -460,7 +476,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
460
476
|
return;
|
|
461
477
|
}
|
|
462
478
|
const links = target.links;
|
|
463
|
-
|
|
479
|
+
yield* draft.publish((installed) => {
|
|
464
480
|
const events = [];
|
|
465
481
|
for (const link of links) {
|
|
466
482
|
const prefix = pathOf((at) => getNode(installed, at), link.parent);
|
|
@@ -474,7 +490,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
474
490
|
}
|
|
475
491
|
return events;
|
|
476
492
|
});
|
|
477
|
-
};
|
|
493
|
+
});
|
|
478
494
|
const pathsOf = (installed, ino) => {
|
|
479
495
|
const node = getNode(installed, ino);
|
|
480
496
|
if (node === undefined)
|
|
@@ -573,7 +589,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
573
589
|
const holdDirectory = (ino) => {
|
|
574
590
|
directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
|
|
575
591
|
};
|
|
576
|
-
const unholdDirectory = (ino)
|
|
592
|
+
const unholdDirectory = Effect.fnUntraced(function* (ino) {
|
|
577
593
|
const count = (directoryHolds.get(ino) ?? 1) - 1;
|
|
578
594
|
if (count > 0) {
|
|
579
595
|
directoryHolds.set(ino, count);
|
|
@@ -581,19 +597,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
581
597
|
}
|
|
582
598
|
directoryHolds.delete(ino);
|
|
583
599
|
const node = getNode(state, ino);
|
|
584
|
-
if (node?.kind === "directory" && node.metadata.nlink === 0)
|
|
585
|
-
applyDirect((d) => d.remove(ino));
|
|
586
|
-
|
|
587
|
-
|
|
600
|
+
if (node?.kind === "directory" && node.metadata.nlink === 0) {
|
|
601
|
+
yield* applyDirect((d) => d.remove(ino));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
const releaseDirectory = Effect.fnUntraced(function* (reference) {
|
|
588
605
|
const ino = reference.ino;
|
|
589
606
|
reference.ino = undefined;
|
|
590
607
|
reference.closed = true;
|
|
591
608
|
if (ino !== undefined)
|
|
592
|
-
unholdDirectory(ino);
|
|
593
|
-
};
|
|
609
|
+
yield* unholdDirectory(ino);
|
|
610
|
+
});
|
|
594
611
|
const finalizeDirectory = (reference) => Effect.uninterruptible(Effect.suspend(() => reference.closed
|
|
595
612
|
? Effect.void
|
|
596
|
-
: coordinatedCleanup(
|
|
613
|
+
: coordinatedCleanup(releaseDirectory(reference))));
|
|
597
614
|
const closeReleasedScope = (reference) => Effect.suspend(() => !reference.closed || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
|
|
598
615
|
const permitted = (node, identity) => {
|
|
599
616
|
const metadata = node.metadata;
|
|
@@ -607,33 +624,45 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
607
624
|
const authorize = (node, identity, bits, op) => identity.privileged || (permitted(node, identity) & bits) === bits
|
|
608
625
|
? Effect.void
|
|
609
626
|
: Effect.fail(op.fail("AccessDenied"));
|
|
610
|
-
const reclaim = (d, ino)
|
|
611
|
-
const node = d.get(ino);
|
|
627
|
+
const reclaim = Effect.fnUntraced(function* (d, ino) {
|
|
628
|
+
const node = yield* d.get(ino);
|
|
612
629
|
if (node === undefined)
|
|
613
630
|
return;
|
|
614
631
|
if (node.kind === "file") {
|
|
615
|
-
if (node.links.length > 0 || d.openCount(ino) > 0)
|
|
632
|
+
if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
|
|
616
633
|
return;
|
|
617
|
-
d.
|
|
634
|
+
yield* d.addUsedBytes(-BigInt(node.data.length));
|
|
618
635
|
}
|
|
619
636
|
else if (node.kind === "symlink") {
|
|
620
637
|
if (node.links.length > 0)
|
|
621
638
|
return;
|
|
622
|
-
d.
|
|
639
|
+
yield* d.addUsedBytes(-BigInt(node.target.length));
|
|
623
640
|
}
|
|
624
641
|
else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
|
|
625
642
|
return;
|
|
626
|
-
d.remove(ino);
|
|
627
|
-
};
|
|
628
|
-
const atEntryLimit = (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
643
|
+
yield* d.remove(ino);
|
|
644
|
+
});
|
|
645
|
+
const atEntryLimit = Effect.gen(function* () {
|
|
646
|
+
const draft = yield* Draft;
|
|
647
|
+
return settings.maxEntries !== undefined && (yield* draft.entries) >= settings.maxEntries;
|
|
648
|
+
});
|
|
649
|
+
const reserveEntry = Effect.fnUntraced(function* (op) {
|
|
650
|
+
const draft = yield* Draft;
|
|
651
|
+
if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
|
|
652
|
+
return yield* op.fail("NoSpace");
|
|
653
|
+
});
|
|
654
|
+
const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
|
|
655
|
+
const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
|
|
656
|
+
if (settings.maxBytes === undefined)
|
|
657
|
+
return;
|
|
658
|
+
const draft = yield* Draft;
|
|
659
|
+
if (bytes > ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes)) {
|
|
660
|
+
return yield* op.fail("NoSpace");
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
const attach = Effect.fnUntraced(function* (parent, name, child, now) {
|
|
664
|
+
const d = yield* Draft;
|
|
665
|
+
yield* d.put(withEntries({
|
|
637
666
|
...parent,
|
|
638
667
|
metadata: {
|
|
639
668
|
...parent.metadata,
|
|
@@ -643,51 +672,49 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
643
672
|
}
|
|
644
673
|
}, (entries) => entries.set(name, child.ino)));
|
|
645
674
|
if (child.kind === "directory")
|
|
646
|
-
d.put({ ...child, parent: parent.ino, name });
|
|
675
|
+
yield* d.put({ ...child, parent: parent.ino, name });
|
|
647
676
|
else {
|
|
648
|
-
d.put({
|
|
677
|
+
yield* d.put({
|
|
649
678
|
...child,
|
|
650
679
|
links: [...child.links, { parent: parent.ino, name }],
|
|
651
680
|
metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
|
|
652
681
|
});
|
|
653
682
|
}
|
|
654
|
-
};
|
|
655
|
-
const detach = (child, parent, name, now)
|
|
656
|
-
const d =
|
|
683
|
+
});
|
|
684
|
+
const detach = Effect.fnUntraced(function* (child, parent, name, now) {
|
|
685
|
+
const d = yield* Draft;
|
|
657
686
|
if (child.kind === "directory") {
|
|
658
|
-
d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
|
|
687
|
+
yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
|
|
659
688
|
}
|
|
660
689
|
else {
|
|
661
690
|
const links = withoutLink(child.links, parent, name);
|
|
662
|
-
d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
|
|
691
|
+
yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
|
|
663
692
|
}
|
|
664
|
-
reclaim(d, child.ino);
|
|
665
|
-
};
|
|
666
|
-
const releaseFile = (ref)
|
|
693
|
+
yield* reclaim(d, child.ino);
|
|
694
|
+
});
|
|
695
|
+
const releaseFile = Effect.fnUntraced(function* (ref) {
|
|
667
696
|
const ino = ref.ino;
|
|
668
697
|
if (ino !== undefined) {
|
|
669
|
-
const d =
|
|
670
|
-
d.release(ino);
|
|
671
|
-
reclaim(d, ino);
|
|
698
|
+
const d = yield* Draft;
|
|
699
|
+
yield* d.release(ino);
|
|
700
|
+
yield* reclaim(d, ino);
|
|
672
701
|
}
|
|
673
702
|
ref.ino = undefined;
|
|
674
703
|
ref.closed = true;
|
|
675
|
-
};
|
|
676
|
-
const releaseOpenFile = (ref) => coordinatedCleanup(Effect.
|
|
677
|
-
if (!ref.closed)
|
|
678
|
-
applyDirect(() => releaseFile(ref));
|
|
679
|
-
}));
|
|
704
|
+
});
|
|
705
|
+
const releaseOpenFile = (ref) => coordinatedCleanup(Effect.suspend(() => ref.closed ? Effect.void : applyDirect(() => releaseFile(ref))));
|
|
680
706
|
// Explicit close and scope cleanup share one release. A release whose commit fails still completes as
|
|
681
707
|
// cleanup, so the handle never stays open; only an explicit close reports the failure. A close refused
|
|
682
708
|
// 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.
|
|
709
|
+
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, Effect.gen(function* () {
|
|
684
710
|
const ino = ref.ino;
|
|
685
711
|
if (ino !== undefined) {
|
|
686
|
-
const d =
|
|
687
|
-
d.release(ino);
|
|
688
|
-
reclaim(d, ino);
|
|
712
|
+
const d = yield* Draft;
|
|
713
|
+
yield* d.release(ino);
|
|
714
|
+
yield* reclaim(d, ino);
|
|
689
715
|
}
|
|
690
|
-
|
|
716
|
+
const d = yield* Draft;
|
|
717
|
+
yield* d.afterInstall(() => {
|
|
691
718
|
ref.ino = undefined;
|
|
692
719
|
ref.closed = true;
|
|
693
720
|
});
|
|
@@ -713,19 +740,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
713
740
|
return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquire).pipe(Effect.tap(() => Effect.suspend(() => {
|
|
714
741
|
if (!closed())
|
|
715
742
|
return Effect.void;
|
|
716
|
-
releaseAcquired();
|
|
717
|
-
return Effect.interrupt;
|
|
743
|
+
return Effect.andThen(releaseAcquired(), Effect.interrupt);
|
|
718
744
|
})))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalize, Scope.close(scope, Exit.void))));
|
|
719
745
|
});
|
|
720
|
-
const bindFile = (ref, ino) => {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
d.usedBytes += BigInt(data.length - file.data.length);
|
|
728
|
-
d.put({
|
|
746
|
+
const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => {
|
|
747
|
+
ref.ino = ino;
|
|
748
|
+
}));
|
|
749
|
+
const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
|
|
750
|
+
const d = yield* Draft;
|
|
751
|
+
yield* d.addUsedBytes(BigInt(data.length - file.data.length));
|
|
752
|
+
yield* d.put({
|
|
729
753
|
...file,
|
|
730
754
|
data,
|
|
731
755
|
metadata: {
|
|
@@ -737,8 +761,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
737
761
|
}
|
|
738
762
|
});
|
|
739
763
|
if (publish)
|
|
740
|
-
publishNode(file.ino);
|
|
741
|
-
};
|
|
764
|
+
yield* publishNode(file.ino);
|
|
765
|
+
});
|
|
742
766
|
const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
|
|
743
767
|
if (length > BigInt(maxFileBytes))
|
|
744
768
|
return yield* op.fail("FileTooLarge");
|
|
@@ -746,22 +770,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
746
770
|
yield* reserveBytes(op, BigInt(size - file.data.length));
|
|
747
771
|
const data = new Uint8Array(size);
|
|
748
772
|
data.set(file.data.subarray(0, size));
|
|
749
|
-
replaceContent(file, data, now, false);
|
|
773
|
+
yield* replaceContent(file, data, now, false);
|
|
750
774
|
});
|
|
751
775
|
const resize = Effect.fnUntraced(function* (file, length, op) {
|
|
752
776
|
if (!isLength(length))
|
|
753
777
|
return yield* op.fail("InvalidArgument");
|
|
754
778
|
yield* resizeAt(file, length, yield* timestamp(op), op);
|
|
755
|
-
publishNode(file.ino);
|
|
779
|
+
yield* publishNode(file.ino);
|
|
756
780
|
});
|
|
757
781
|
const fileHandle = (ref) => {
|
|
758
|
-
const get = (op, access)
|
|
759
|
-
const node = ref.ino === undefined ? undefined : view(ref.ino);
|
|
760
|
-
|
|
761
|
-
(access === "write" && ref.access === "read")
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
};
|
|
782
|
+
const get = Effect.fnUntraced(function* (op, access) {
|
|
783
|
+
const node = ref.ino === undefined ? undefined : yield* view(ref.ino);
|
|
784
|
+
if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
|
|
785
|
+
(access === "write" && ref.access === "read"))
|
|
786
|
+
return yield* op.fail("InvalidHandle");
|
|
787
|
+
return node;
|
|
788
|
+
});
|
|
765
789
|
const read = (maximum, position) => {
|
|
766
790
|
const op = OpContext.make(position === undefined ? "read" : "pread");
|
|
767
791
|
const readOp = OpContext.make("read");
|
|
@@ -783,9 +807,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
783
807
|
// other would start at the same offset.
|
|
784
808
|
if (position !== undefined)
|
|
785
809
|
return Effect.map(accessing(op, body), ({ bytes, eof }) => ({ bytes, eof }));
|
|
786
|
-
return coordinated(op, Effect.
|
|
787
|
-
const { bytes, eof, next } = refreshAccess(
|
|
788
|
-
|
|
810
|
+
return coordinated(op, Effect.gen(function* () {
|
|
811
|
+
const { bytes, eof, next } = yield* refreshAccess(yield* body);
|
|
812
|
+
const draft = yield* Draft;
|
|
813
|
+
yield* draft.afterInstall(() => {
|
|
789
814
|
ref.offset = next;
|
|
790
815
|
});
|
|
791
816
|
return { bytes, eof };
|
|
@@ -809,9 +834,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
809
834
|
if (offset >= BigInt(maxFileBytes))
|
|
810
835
|
return yield* writeOp.fail("FileTooLarge");
|
|
811
836
|
const start = Number(offset);
|
|
837
|
+
const draft = yield* Draft;
|
|
812
838
|
const free = settings.maxBytes === undefined
|
|
813
839
|
? BigInt(maxFileBytes)
|
|
814
|
-
: ByteSize.toBigInt(settings.maxBytes) -
|
|
840
|
+
: ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes);
|
|
815
841
|
const maximumEnd = BigInt(file.data.length) + free;
|
|
816
842
|
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
817
843
|
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
@@ -824,10 +850,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
824
850
|
data.set(file.data);
|
|
825
851
|
const now = yield* timestamp(writeOp);
|
|
826
852
|
data.set(bytes.subarray(0, count), start);
|
|
827
|
-
replaceContent(file, data, now);
|
|
853
|
+
yield* replaceContent(file, data, now);
|
|
828
854
|
if (position === undefined) {
|
|
829
855
|
const next = offset + BigInt(count);
|
|
830
|
-
|
|
856
|
+
yield* draft.afterInstall(() => {
|
|
831
857
|
ref.offset = next;
|
|
832
858
|
});
|
|
833
859
|
}
|
|
@@ -901,34 +927,38 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
901
927
|
if (known.volume !== volumeIdentity) {
|
|
902
928
|
return yield* op.fail("ForeignReference");
|
|
903
929
|
}
|
|
904
|
-
const node = view(known.ino);
|
|
930
|
+
const node = yield* view(known.ino);
|
|
905
931
|
if (node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)) {
|
|
906
932
|
return yield* op.fail("StaleReference");
|
|
907
933
|
}
|
|
908
934
|
return node;
|
|
909
935
|
});
|
|
910
|
-
const callerDirectory = (op)
|
|
911
|
-
const node = reference.ino === undefined ? undefined : view(reference.ino);
|
|
912
|
-
return node?.kind === "directory" ? Effect.succeed(node) : Effect.fail(op.fail("ClosedCaller"));
|
|
913
|
-
};
|
|
914
|
-
const directoryNow = (ino) => {
|
|
915
|
-
const node = view(ino);
|
|
936
|
+
const callerDirectory = Effect.fnUntraced(function* (op) {
|
|
937
|
+
const node = reference.ino === undefined ? undefined : yield* view(reference.ino);
|
|
916
938
|
if (node?.kind !== "directory")
|
|
917
|
-
|
|
939
|
+
return yield* op.fail("ClosedCaller");
|
|
918
940
|
return node;
|
|
919
|
-
};
|
|
920
|
-
const
|
|
921
|
-
const node = view(ino);
|
|
941
|
+
});
|
|
942
|
+
const directoryNow = Effect.fnUntraced(function* (ino) {
|
|
943
|
+
const node = yield* view(ino);
|
|
944
|
+
if (node?.kind !== "directory") {
|
|
945
|
+
return dieInvariant("Directory left the inode table during a transition");
|
|
946
|
+
}
|
|
947
|
+
return node;
|
|
948
|
+
});
|
|
949
|
+
const nodeNow = Effect.fnUntraced(function* (ino) {
|
|
950
|
+
const node = yield* view(ino);
|
|
922
951
|
if (node === undefined)
|
|
923
|
-
|
|
952
|
+
return dieInvariant("Inode left the table during a transition");
|
|
924
953
|
return node;
|
|
925
|
-
};
|
|
954
|
+
});
|
|
926
955
|
const creationTimes = (times, now) => ({
|
|
927
956
|
atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
|
|
928
957
|
mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
|
|
929
958
|
});
|
|
930
|
-
const newDirectory = (parent, mode, now, times)
|
|
931
|
-
const
|
|
959
|
+
const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
|
|
960
|
+
const draft = yield* Draft;
|
|
961
|
+
const ino = yield* draft.allocate;
|
|
932
962
|
return {
|
|
933
963
|
kind: "directory",
|
|
934
964
|
ino,
|
|
@@ -941,9 +971,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
941
971
|
},
|
|
942
972
|
revision: 0n
|
|
943
973
|
};
|
|
944
|
-
};
|
|
945
|
-
const newFile = (parent, data, mode, now, owner, times)
|
|
946
|
-
const
|
|
974
|
+
});
|
|
975
|
+
const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
|
|
976
|
+
const draft = yield* Draft;
|
|
977
|
+
const ino = yield* draft.allocate;
|
|
947
978
|
return {
|
|
948
979
|
kind: "file",
|
|
949
980
|
ino,
|
|
@@ -958,9 +989,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
958
989
|
},
|
|
959
990
|
revision: 0n
|
|
960
991
|
};
|
|
961
|
-
};
|
|
962
|
-
const newSymlink = (parent, target, now, times)
|
|
963
|
-
const
|
|
992
|
+
});
|
|
993
|
+
const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
|
|
994
|
+
const draft = yield* Draft;
|
|
995
|
+
const ino = yield* draft.allocate;
|
|
964
996
|
return {
|
|
965
997
|
kind: "symlink",
|
|
966
998
|
ino,
|
|
@@ -975,14 +1007,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
975
1007
|
},
|
|
976
1008
|
revision: 0n
|
|
977
1009
|
};
|
|
978
|
-
};
|
|
1010
|
+
});
|
|
979
1011
|
const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
|
|
980
1012
|
const pathOp = op.at(path.input);
|
|
981
1013
|
const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
|
|
982
1014
|
if (reference.ino === undefined) {
|
|
983
1015
|
return yield* pathOp.fail("ClosedCaller");
|
|
984
1016
|
}
|
|
985
|
-
let current = path.absolute ? nodeNow(ROOT_INO) : yield* callerDirectory(pathOp);
|
|
1017
|
+
let current = path.absolute ? (yield* nodeNow(ROOT_INO)) : yield* callerDirectory(pathOp);
|
|
986
1018
|
if (!path.absolute && referencedBase !== undefined) {
|
|
987
1019
|
current = referencedBase;
|
|
988
1020
|
yield* authorize(current, identity, EXECUTE, op);
|
|
@@ -995,7 +1027,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
995
1027
|
if (target.volume !== volumeIdentity) {
|
|
996
1028
|
return yield* pathOp.fail("ForeignHandle");
|
|
997
1029
|
}
|
|
998
|
-
const directory = target.ino === undefined ? undefined : view(target.ino);
|
|
1030
|
+
const directory = target.ino === undefined ? undefined : (yield* view(target.ino));
|
|
999
1031
|
if (directory?.kind !== "directory") {
|
|
1000
1032
|
return yield* pathOp.fail("InvalidHandle");
|
|
1001
1033
|
}
|
|
@@ -1023,7 +1055,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1023
1055
|
if (component === DOT_HEX)
|
|
1024
1056
|
continue;
|
|
1025
1057
|
if (component === DOT_DOT_HEX) {
|
|
1026
|
-
current = directoryNow(current.parent);
|
|
1058
|
+
current = yield* directoryNow(current.parent);
|
|
1027
1059
|
parent = undefined;
|
|
1028
1060
|
name = undefined;
|
|
1029
1061
|
continue;
|
|
@@ -1031,7 +1063,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1031
1063
|
parent = current;
|
|
1032
1064
|
name = component;
|
|
1033
1065
|
const childIno = current.entries.get(component);
|
|
1034
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1066
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1035
1067
|
if (child === undefined) {
|
|
1036
1068
|
if (allowMissing && index === work.components.length - 1) {
|
|
1037
1069
|
return { node: undefined, parent, name };
|
|
@@ -1068,7 +1100,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1068
1100
|
work = expanded.success;
|
|
1069
1101
|
linked = work.components.length - remaining + Math.max(0, linked - index - 1);
|
|
1070
1102
|
if (work.absolute)
|
|
1071
|
-
current = nodeNow(ROOT_INO);
|
|
1103
|
+
current = yield* nodeNow(ROOT_INO);
|
|
1072
1104
|
index = -1;
|
|
1073
1105
|
}
|
|
1074
1106
|
else
|
|
@@ -1136,7 +1168,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1136
1168
|
const resolving = resolve();
|
|
1137
1169
|
return coordinated(op, Effect.gen(function* () {
|
|
1138
1170
|
const resolved = yield* resolving;
|
|
1139
|
-
const node = nodeNow(resolved.ino);
|
|
1171
|
+
const node = yield* nodeNow(resolved.ino);
|
|
1140
1172
|
if (expected !== undefined && node.revision !== expected) {
|
|
1141
1173
|
return yield* resolved.op.fail("StaleReference", { field: "expected" });
|
|
1142
1174
|
}
|
|
@@ -1162,11 +1194,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1162
1194
|
const now = yield* timestamp(op);
|
|
1163
1195
|
if (size !== undefined && node.kind === "file")
|
|
1164
1196
|
yield* resizeAt(node, size, now, op);
|
|
1165
|
-
const sized = nodeNow(resolved.ino);
|
|
1197
|
+
const sized = yield* nodeNow(resolved.ino);
|
|
1166
1198
|
const gid = owner?.gid ?? sized.metadata.gid;
|
|
1167
1199
|
const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
|
|
1168
1200
|
if (chowned || mode !== undefined || timed) {
|
|
1169
|
-
|
|
1201
|
+
yield* (yield* Draft).put({
|
|
1170
1202
|
...sized,
|
|
1171
1203
|
metadata: {
|
|
1172
1204
|
...sized.metadata,
|
|
@@ -1179,7 +1211,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1179
1211
|
}
|
|
1180
1212
|
});
|
|
1181
1213
|
}
|
|
1182
|
-
publishNode(node.ino);
|
|
1214
|
+
yield* publishNode(node.ino);
|
|
1183
1215
|
}));
|
|
1184
1216
|
};
|
|
1185
1217
|
const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
|
|
@@ -1187,7 +1219,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1187
1219
|
? Effect.fail(op.fail("NotPermitted"))
|
|
1188
1220
|
: Effect.void;
|
|
1189
1221
|
const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
|
|
1190
|
-
const parent = directoryNow(entry.parent);
|
|
1222
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1191
1223
|
if (isDotComponent(entry.name)) {
|
|
1192
1224
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
|
|
1193
1225
|
}
|
|
@@ -1202,17 +1234,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1202
1234
|
const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
|
|
1203
1235
|
const name = yield* claimName(entry);
|
|
1204
1236
|
yield* reserveEntry(entry.op);
|
|
1205
|
-
const parent = directoryNow(entry.parent);
|
|
1237
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1206
1238
|
const before = parent.revision;
|
|
1207
1239
|
const now = yield* timestamp(op);
|
|
1208
1240
|
const mode = request.exactMode
|
|
1209
1241
|
? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
|
|
1210
1242
|
: (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
|
|
1211
|
-
const child = newDirectory(parent, mode, now, request.times);
|
|
1212
|
-
attach(parent, name, child, now);
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1243
|
+
const child = yield* newDirectory(parent, mode, now, request.times);
|
|
1244
|
+
yield* attach(parent, name, child, now);
|
|
1245
|
+
const draft = yield* Draft;
|
|
1246
|
+
yield* draft.addEntries(1);
|
|
1247
|
+
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
1248
|
+
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
1216
1249
|
});
|
|
1217
1250
|
// Creates every missing directory a path names in the running transition, so a failure anywhere discards
|
|
1218
1251
|
// them all. The mode applies to each directory created and the times to the one in the final position. An
|
|
@@ -1228,15 +1261,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1228
1261
|
const made = yield* Effect.result(makeDirectory(entry, request, op));
|
|
1229
1262
|
if (Result.isSuccess(made))
|
|
1230
1263
|
return made.success;
|
|
1231
|
-
const existingIno = entry.name === undefined
|
|
1232
|
-
|
|
1264
|
+
const existingIno = entry.name === undefined
|
|
1265
|
+
? undefined
|
|
1266
|
+
: (yield* directoryNow(entry.parent)).entries.get(entry.name);
|
|
1267
|
+
node = existingIno === undefined ? undefined : (yield* view(existingIno));
|
|
1233
1268
|
if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
|
|
1234
1269
|
return yield* made.failure;
|
|
1235
1270
|
}
|
|
1236
1271
|
else {
|
|
1237
1272
|
entryOp = op.at(prepared.path.input);
|
|
1238
1273
|
const resolved = yield* lookup(prepared.path, prepared.base, op, {
|
|
1239
|
-
createMissing: (parent, name, final) => Effect.
|
|
1274
|
+
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) => {
|
|
1240
1275
|
if (!revisionsBefore.has(parent.ino))
|
|
1241
1276
|
revisionsBefore.set(parent.ino, made.directory.before);
|
|
1242
1277
|
return directoryNow(made.child);
|
|
@@ -1246,39 +1281,41 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1246
1281
|
}
|
|
1247
1282
|
if (node?.kind !== "directory")
|
|
1248
1283
|
return yield* entryOp.fail("AlreadyExists");
|
|
1249
|
-
const after = directoryNow(node.parent).revision;
|
|
1284
|
+
const after = (yield* directoryNow(node.parent)).revision;
|
|
1250
1285
|
return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
|
|
1251
1286
|
});
|
|
1252
1287
|
const linkNode = Effect.fnUntraced(function* (node, entry, op) {
|
|
1253
1288
|
const name = yield* claimName(entry, "rejected");
|
|
1254
1289
|
yield* reserveEntry(entry.op);
|
|
1255
|
-
const parent = directoryNow(entry.parent);
|
|
1290
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1256
1291
|
const before = parent.revision;
|
|
1257
1292
|
const now = yield* timestamp(op);
|
|
1258
|
-
attach(parent, name, node, now);
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1293
|
+
yield* attach(parent, name, node, now);
|
|
1294
|
+
const draft = yield* Draft;
|
|
1295
|
+
yield* draft.addEntries(1);
|
|
1296
|
+
yield* publishEntry("Create", parent.ino, name, node.ino);
|
|
1297
|
+
return { before, after: draft.revision };
|
|
1262
1298
|
});
|
|
1263
1299
|
const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
|
|
1264
1300
|
const name = yield* claimName(entry, "rejected");
|
|
1265
1301
|
yield* reserveEntry(entry.op);
|
|
1266
1302
|
yield* reserveBytes(entry.op, BigInt(target.length));
|
|
1267
|
-
const parent = directoryNow(entry.parent);
|
|
1303
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1268
1304
|
const before = parent.revision;
|
|
1269
1305
|
const now = yield* timestamp(op);
|
|
1270
|
-
const child = newSymlink(parent, target, now, times);
|
|
1271
|
-
attach(parent, name, child, now);
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1306
|
+
const child = yield* newSymlink(parent, target, now, times);
|
|
1307
|
+
yield* attach(parent, name, child, now);
|
|
1308
|
+
const draft = yield* Draft;
|
|
1309
|
+
yield* draft.addEntries(1);
|
|
1310
|
+
yield* draft.addUsedBytes(BigInt(target.length));
|
|
1311
|
+
yield* publishEntry("Create", parent.ino, name, child.ino);
|
|
1312
|
+
return { child: child.ino, directory: { before, after: draft.revision } };
|
|
1276
1313
|
});
|
|
1277
1314
|
const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
|
|
1278
1315
|
const before = parent.revision;
|
|
1279
1316
|
const now = yield* timestamp(op);
|
|
1280
|
-
const d =
|
|
1281
|
-
d.put(withEntries({
|
|
1317
|
+
const d = yield* Draft;
|
|
1318
|
+
yield* d.put(withEntries({
|
|
1282
1319
|
...parent,
|
|
1283
1320
|
metadata: {
|
|
1284
1321
|
...parent.metadata,
|
|
@@ -1287,22 +1324,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1287
1324
|
ctimeNs: now
|
|
1288
1325
|
}
|
|
1289
1326
|
}, (entries) => entries.delete(name)));
|
|
1290
|
-
publishEntry("Remove", parent.ino, name, child.ino);
|
|
1291
|
-
detach(child, parent.ino, name, now);
|
|
1292
|
-
d.
|
|
1327
|
+
yield* publishEntry("Remove", parent.ino, name, child.ino);
|
|
1328
|
+
yield* detach(child, parent.ino, name, now);
|
|
1329
|
+
yield* d.addEntries(-1);
|
|
1293
1330
|
return { before, after: d.revision };
|
|
1294
1331
|
});
|
|
1295
1332
|
// Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
|
|
1296
1333
|
// entry, and each verb reports it with its own code. Search permission comes before the lookup and write
|
|
1297
1334
|
// permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
|
|
1298
1335
|
const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
|
|
1299
|
-
const parent = directoryNow(entry.parent);
|
|
1336
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1300
1337
|
if (isDotComponent(entry.name)) {
|
|
1301
1338
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
|
|
1302
1339
|
}
|
|
1303
1340
|
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
1304
1341
|
const childIno = parent.entries.get(entry.name);
|
|
1305
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1342
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1306
1343
|
if (child === undefined)
|
|
1307
1344
|
return yield* entry.op.fail("NotFound");
|
|
1308
1345
|
const rejected = beforeWrite?.(child);
|
|
@@ -1337,8 +1374,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1337
1374
|
return yield* removeChild(parent, name, child, op);
|
|
1338
1375
|
});
|
|
1339
1376
|
const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
|
|
1340
|
-
const sourceDirectory = directoryNow(source.parent);
|
|
1341
|
-
const destinationDirectory = directoryNow(destination.parent);
|
|
1377
|
+
const sourceDirectory = yield* directoryNow(source.parent);
|
|
1378
|
+
const destinationDirectory = yield* directoryNow(destination.parent);
|
|
1342
1379
|
const sameDirectory = source.parent === destination.parent;
|
|
1343
1380
|
const sourceName = source.name;
|
|
1344
1381
|
const destinationName = destination.name;
|
|
@@ -1352,11 +1389,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1352
1389
|
const sourceBefore = sourceDirectory.revision;
|
|
1353
1390
|
const destinationBefore = destinationDirectory.revision;
|
|
1354
1391
|
const childIno = sourceDirectory.entries.get(sourceName);
|
|
1355
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1392
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1356
1393
|
if (child === undefined)
|
|
1357
1394
|
return yield* source.op.fail("NotFound");
|
|
1358
1395
|
const replacedIno = destinationDirectory.entries.get(destinationName);
|
|
1359
|
-
const replaced = replacedIno === undefined ? undefined : view(replacedIno);
|
|
1396
|
+
const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
|
|
1360
1397
|
// A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
|
|
1361
1398
|
// source is one, as Linux allows.
|
|
1362
1399
|
if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
|
|
@@ -1365,24 +1402,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1365
1402
|
if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
|
|
1366
1403
|
return yield* destination.op.fail("NotDirectory");
|
|
1367
1404
|
}
|
|
1368
|
-
const result = ()
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1405
|
+
const result = Effect.fnUntraced(function* () {
|
|
1406
|
+
return sameDirectory
|
|
1407
|
+
? {
|
|
1408
|
+
_tag: "SameDirectory",
|
|
1409
|
+
directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
|
|
1410
|
+
}
|
|
1411
|
+
: {
|
|
1412
|
+
_tag: "DifferentDirectories",
|
|
1413
|
+
sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
|
|
1414
|
+
destinationDirectory: {
|
|
1415
|
+
before: destinationBefore,
|
|
1416
|
+
after: (yield* directoryNow(destination.parent)).revision
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
});
|
|
1420
|
+
for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
|
|
1379
1421
|
if (ancestor.ino === child.ino)
|
|
1380
1422
|
return yield* destination.op.fail("InvalidArgument");
|
|
1381
1423
|
if (ancestor.ino === ROOT_INO)
|
|
1382
1424
|
break;
|
|
1383
1425
|
}
|
|
1384
1426
|
if (child.ino === replaced?.ino)
|
|
1385
|
-
return result();
|
|
1427
|
+
return yield* result();
|
|
1386
1428
|
yield* authorize(sourceDirectory, identity, WRITE, source.op);
|
|
1387
1429
|
yield* authorize(destinationDirectory, identity, WRITE, destination.op);
|
|
1388
1430
|
yield* authorizeRemoval(sourceDirectory, child, source.op);
|
|
@@ -1399,9 +1441,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1399
1441
|
}
|
|
1400
1442
|
}
|
|
1401
1443
|
const now = yield* timestamp(op);
|
|
1402
|
-
const d =
|
|
1403
|
-
publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
|
|
1404
|
-
d.put(withEntries({
|
|
1444
|
+
const d = yield* Draft;
|
|
1445
|
+
yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
|
|
1446
|
+
yield* d.put(withEntries({
|
|
1405
1447
|
...sourceDirectory,
|
|
1406
1448
|
metadata: {
|
|
1407
1449
|
...sourceDirectory.metadata,
|
|
@@ -1410,8 +1452,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1410
1452
|
ctimeNs: now
|
|
1411
1453
|
}
|
|
1412
1454
|
}, (entries) => entries.delete(sourceName)));
|
|
1413
|
-
const destinationNow = directoryNow(destination.parent);
|
|
1414
|
-
d.put(withEntries({
|
|
1455
|
+
const destinationNow = yield* directoryNow(destination.parent);
|
|
1456
|
+
yield* d.put(withEntries({
|
|
1415
1457
|
...destinationNow,
|
|
1416
1458
|
metadata: {
|
|
1417
1459
|
...destinationNow.metadata,
|
|
@@ -1420,9 +1462,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1420
1462
|
ctimeNs: now
|
|
1421
1463
|
}
|
|
1422
1464
|
}, (entries) => entries.set(destinationName, child.ino)));
|
|
1423
|
-
const moved = nodeNow(child.ino);
|
|
1465
|
+
const moved = yield* nodeNow(child.ino);
|
|
1424
1466
|
if (moved.kind === "directory") {
|
|
1425
|
-
d.put({
|
|
1467
|
+
yield* d.put({
|
|
1426
1468
|
...moved,
|
|
1427
1469
|
parent: destination.parent,
|
|
1428
1470
|
name: destinationName,
|
|
@@ -1430,7 +1472,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1430
1472
|
});
|
|
1431
1473
|
}
|
|
1432
1474
|
else {
|
|
1433
|
-
d.put({
|
|
1475
|
+
yield* d.put({
|
|
1434
1476
|
...moved,
|
|
1435
1477
|
links: [
|
|
1436
1478
|
...withoutLink(moved.links, source.parent, sourceName),
|
|
@@ -1440,11 +1482,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1440
1482
|
});
|
|
1441
1483
|
}
|
|
1442
1484
|
if (replaced !== undefined) {
|
|
1443
|
-
detach(nodeNow(replaced.ino), destination.parent, destinationName, now);
|
|
1444
|
-
d.
|
|
1485
|
+
yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
|
|
1486
|
+
yield* d.addEntries(-1);
|
|
1445
1487
|
}
|
|
1446
|
-
publishEntry("Create", destination.parent, destinationName, child.ino);
|
|
1447
|
-
return result();
|
|
1488
|
+
yield* publishEntry("Create", destination.parent, destinationName, child.ino);
|
|
1489
|
+
return yield* result();
|
|
1448
1490
|
});
|
|
1449
1491
|
const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
|
|
1450
1492
|
yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
|
|
@@ -1453,7 +1495,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1453
1495
|
});
|
|
1454
1496
|
const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
|
|
1455
1497
|
const { name } = entry;
|
|
1456
|
-
const parent = directoryNow(entry.parent);
|
|
1498
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1457
1499
|
const before = parent.revision;
|
|
1458
1500
|
let file = found;
|
|
1459
1501
|
let created = false;
|
|
@@ -1480,12 +1522,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1480
1522
|
const mode = request.exactMode
|
|
1481
1523
|
? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
|
|
1482
1524
|
: (request.mode ?? 0o666) & 0o777 & ~umask;
|
|
1483
|
-
file = newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
|
|
1484
|
-
attach(parent, name, file, now);
|
|
1485
|
-
|
|
1486
|
-
|
|
1525
|
+
file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
|
|
1526
|
+
yield* attach(parent, name, file, now);
|
|
1527
|
+
const draft = yield* Draft;
|
|
1528
|
+
yield* draft.addEntries(1);
|
|
1529
|
+
yield* draft.addUsedBytes(size);
|
|
1487
1530
|
created = true;
|
|
1488
|
-
publishEntry("Create", parent.ino, name, file.ino);
|
|
1531
|
+
yield* publishEntry("Create", parent.ino, name, file.ino);
|
|
1489
1532
|
}
|
|
1490
1533
|
else {
|
|
1491
1534
|
if (file.kind === "symlink")
|
|
@@ -1494,17 +1537,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1494
1537
|
return yield* entry.op.fail("IsDirectory");
|
|
1495
1538
|
yield* openExisting(file, request, entry.op, op);
|
|
1496
1539
|
}
|
|
1497
|
-
|
|
1498
|
-
bindFile(acquired, file.ino);
|
|
1499
|
-
return { ino: file.ino, created, directory: { before, after: directoryNow(entry.parent).revision } };
|
|
1540
|
+
yield* (yield* Draft).retain(file.ino);
|
|
1541
|
+
yield* bindFile(acquired, file.ino);
|
|
1542
|
+
return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
|
|
1500
1543
|
});
|
|
1501
|
-
const releasePending = (opened) => () => {
|
|
1544
|
+
const releasePending = (opened) => () => Effect.gen(function* () {
|
|
1502
1545
|
const ino = opened();
|
|
1503
1546
|
if (ino === undefined)
|
|
1504
1547
|
return;
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1548
|
+
const draft = yield* Draft;
|
|
1549
|
+
yield* draft.release(ino);
|
|
1550
|
+
yield* reclaim(draft, ino);
|
|
1551
|
+
});
|
|
1508
1552
|
const acquireOpenedFile = (ref, op, acquire) => {
|
|
1509
1553
|
let opened;
|
|
1510
1554
|
return acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
|
|
@@ -1520,7 +1564,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1520
1564
|
return yield* op.fail("InvalidHandle");
|
|
1521
1565
|
if (ref.volume !== volumeIdentity)
|
|
1522
1566
|
return yield* op.fail("ForeignHandle");
|
|
1523
|
-
const node = ref.ino === undefined ? undefined : view(ref.ino);
|
|
1567
|
+
const node = ref.ino === undefined ? undefined : (yield* view(ref.ino));
|
|
1524
1568
|
if (node === undefined)
|
|
1525
1569
|
return yield* op.fail("InvalidHandle");
|
|
1526
1570
|
return node;
|
|
@@ -1560,7 +1604,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1560
1604
|
if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
|
|
1561
1605
|
bytes.includes(SLASH_BYTE))
|
|
1562
1606
|
return Result.fail(op.fail("InvalidArgument"));
|
|
1563
|
-
return Result.succeed(
|
|
1607
|
+
return Result.succeed(Hex.encode(new Uint8Array(bytes)));
|
|
1564
1608
|
};
|
|
1565
1609
|
const prepareEntry = (input, op) => isEntry(input)
|
|
1566
1610
|
? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
|
|
@@ -1593,7 +1637,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1593
1637
|
}),
|
|
1594
1638
|
// Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
|
|
1595
1639
|
// every permit, where a detached directory may leave the table.
|
|
1596
|
-
() =>
|
|
1640
|
+
() => Effect.void, finalizeDirectory(acquired));
|
|
1597
1641
|
});
|
|
1598
1642
|
const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
|
|
1599
1643
|
const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
|
|
@@ -1655,8 +1699,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1655
1699
|
if (node.metadata.nlink === 0)
|
|
1656
1700
|
return yield* resolved.op.fail("StaleReference");
|
|
1657
1701
|
yield* openExisting(node, chosen, resolved.op, op);
|
|
1658
|
-
|
|
1659
|
-
|
|
1702
|
+
const draft = yield* Draft;
|
|
1703
|
+
yield* draft.retain(node.ino);
|
|
1704
|
+
yield* bindFile(acquired, node.ino);
|
|
1660
1705
|
opened(node.ino);
|
|
1661
1706
|
return fileHandle(acquired);
|
|
1662
1707
|
}));
|
|
@@ -1683,7 +1728,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1683
1728
|
return yield* op.fail("InvalidArgument");
|
|
1684
1729
|
yield* authorize(parent, identity, EXECUTE, op);
|
|
1685
1730
|
const directIno = parent.entries.get(name);
|
|
1686
|
-
const direct = directIno === undefined ? undefined : view(directIno);
|
|
1731
|
+
const direct = directIno === undefined ? undefined : (yield* view(directIno));
|
|
1687
1732
|
if (expected !== undefined) {
|
|
1688
1733
|
let expectedIno;
|
|
1689
1734
|
if (expected !== null) {
|
|
@@ -1738,12 +1783,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1738
1783
|
};
|
|
1739
1784
|
}));
|
|
1740
1785
|
});
|
|
1741
|
-
const walkChildren = (directory, path, depth, up)
|
|
1786
|
+
const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
|
|
1742
1787
|
const listed = referenceFor(directory.ino);
|
|
1743
1788
|
const frames = [];
|
|
1744
|
-
for (const name of
|
|
1745
|
-
const
|
|
1746
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1789
|
+
for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
|
|
1790
|
+
const child = yield* view(childIno);
|
|
1747
1791
|
if (child === undefined)
|
|
1748
1792
|
continue;
|
|
1749
1793
|
const bytes = nameBytes(name);
|
|
@@ -1763,7 +1807,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1763
1807
|
});
|
|
1764
1808
|
}
|
|
1765
1809
|
return frames;
|
|
1766
|
-
};
|
|
1810
|
+
});
|
|
1767
1811
|
const anchorFrame = (parent, key, root) => ({
|
|
1768
1812
|
ino: root.ino,
|
|
1769
1813
|
kind: root.kind,
|
|
@@ -1784,13 +1828,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1784
1828
|
const prepared = prepare(target.path, op);
|
|
1785
1829
|
if (Result.isFailure(prepared))
|
|
1786
1830
|
return Effect.undefined;
|
|
1787
|
-
return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.map((entry) => {
|
|
1788
|
-
const parent = view(entry.parent);
|
|
1831
|
+
return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
|
|
1789
1832
|
return entry.name !== undefined && parent?.kind === "directory" &&
|
|
1790
1833
|
parent.entries.get(entry.name) === root.ino
|
|
1791
1834
|
? anchorFrame(parent, entry.name, root)
|
|
1792
1835
|
: undefined;
|
|
1793
|
-
}), Effect.catch(() => Effect.undefined));
|
|
1836
|
+
})), Effect.catch(() => Effect.undefined));
|
|
1794
1837
|
};
|
|
1795
1838
|
const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
|
|
1796
1839
|
// The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
|
|
@@ -1803,13 +1846,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1803
1846
|
const chain = [];
|
|
1804
1847
|
for (let link = frame; link !== undefined; link = link.up)
|
|
1805
1848
|
chain.push(link);
|
|
1806
|
-
let node = view((chain.at(-1) ?? frame).parent);
|
|
1849
|
+
let node = yield* view((chain.at(-1) ?? frame).parent);
|
|
1807
1850
|
for (let index = chain.length - 1; index >= 0; index--) {
|
|
1808
1851
|
const link = chain[index];
|
|
1809
1852
|
if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
|
|
1810
1853
|
return undefined;
|
|
1811
1854
|
yield* authorize(node, identity, EXECUTE, at);
|
|
1812
|
-
node = node.entries.get(link.key) === link.ino ? view(link.ino) : undefined;
|
|
1855
|
+
node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
|
|
1813
1856
|
}
|
|
1814
1857
|
return node;
|
|
1815
1858
|
});
|
|
@@ -1830,7 +1873,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1830
1873
|
const at = op.at(locate(frame.path));
|
|
1831
1874
|
return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
|
|
1832
1875
|
? Effect.succeed([])
|
|
1833
|
-
: Effect.
|
|
1876
|
+
: Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
|
|
1834
1877
|
}));
|
|
1835
1878
|
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1836
1879
|
const admit = (frame) => {
|
|
@@ -1902,10 +1945,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1902
1945
|
let targetAt = preparedOp(prepared, op);
|
|
1903
1946
|
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
1904
1947
|
const entry = yield* resolveEntry(prepared, op);
|
|
1905
|
-
const parent = directoryNow(entry.parent);
|
|
1948
|
+
const parent = yield* directoryNow(entry.parent);
|
|
1906
1949
|
yield* authorize(parent, identity, EXECUTE, entry.op);
|
|
1907
1950
|
const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
|
|
1908
|
-
const child = childIno === undefined ? undefined : view(childIno);
|
|
1951
|
+
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1909
1952
|
if (entry.name === undefined || child === undefined)
|
|
1910
1953
|
return yield* entry.op.fail("NotFound");
|
|
1911
1954
|
if (child.kind !== "directory")
|
|
@@ -1913,16 +1956,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1913
1956
|
yield* listable(child, entry.op);
|
|
1914
1957
|
anchor = anchorFrame(parent, entry.name, child);
|
|
1915
1958
|
targetAt = entry.op;
|
|
1916
|
-
return walkChildren(child, new Uint8Array(0), 0, anchor);
|
|
1959
|
+
return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
|
|
1917
1960
|
})).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
|
|
1918
1961
|
const seams = yield* VolumeTestSeams;
|
|
1919
1962
|
const removeFrame = (frame) => {
|
|
1920
1963
|
const at = op.at(locate(frame.path));
|
|
1921
|
-
return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.
|
|
1922
|
-
if (anchor !== undefined && !holds(view(anchor.parent), anchor)) {
|
|
1923
|
-
return force ? Effect.void : Effect.fail(targetAt.fail("NotFound"));
|
|
1964
|
+
return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
|
|
1965
|
+
if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
|
|
1966
|
+
return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
|
|
1924
1967
|
}
|
|
1925
|
-
return Effect.flatMap(frame.up === undefined ?
|
|
1968
|
+
return yield* Effect.flatMap(frame.up === undefined ? view(frame.parent) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
|
|
1926
1969
|
? Effect.fail(at.fail("NotFound"))
|
|
1927
1970
|
: removeEntry({
|
|
1928
1971
|
parent: parent.ino,
|
|
@@ -1952,7 +1995,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1952
1995
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1953
1996
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
1954
1997
|
const entry = yield* resolveEntry(prepared, op);
|
|
1955
|
-
const directory = directoryNow(entry.parent);
|
|
1998
|
+
const directory = yield* directoryNow(entry.parent);
|
|
1956
1999
|
if (isDotComponent(entry.name)) {
|
|
1957
2000
|
return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
|
|
1958
2001
|
}
|
|
@@ -1999,7 +2042,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1999
2042
|
const first = coordinatedRead(op, Effect.gen(function* () {
|
|
2000
2043
|
const directory = yield* resolveDirectory(target, op);
|
|
2001
2044
|
yield* authorize(directory.node, identity, READ, directory.op);
|
|
2002
|
-
return walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
2045
|
+
return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
2003
2046
|
}));
|
|
2004
2047
|
const plan = {
|
|
2005
2048
|
order: chosen.order ?? "pre",
|
|
@@ -2035,7 +2078,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2035
2078
|
const pathOp = op.at(target.path);
|
|
2036
2079
|
const result = yield* lookup(yield* Effect.fromResult(prepare(target.path, op)), target.relativeTo, op);
|
|
2037
2080
|
const directory = result.node?.kind === "directory" ? result.node : result.parent;
|
|
2038
|
-
const prefix = directory === undefined ? SLASH_HEX :
|
|
2081
|
+
const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
|
|
2039
2082
|
if (prefix === undefined)
|
|
2040
2083
|
return yield* pathOp.fail("NotFound");
|
|
2041
2084
|
if (result.node?.kind === "directory" || result.name === undefined)
|
|
@@ -2043,7 +2086,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2043
2086
|
return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
|
|
2044
2087
|
}
|
|
2045
2088
|
const resolved = yield* resolveTarget(target, op);
|
|
2046
|
-
const path =
|
|
2089
|
+
const path = yield* pathOfCurrent(resolved.node.ino);
|
|
2047
2090
|
if (path === undefined)
|
|
2048
2091
|
return yield* resolved.op.fail("NotFound");
|
|
2049
2092
|
return ownedPath(nameBytes(path));
|
|
@@ -2128,7 +2171,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2128
2171
|
parent = directory.node;
|
|
2129
2172
|
name = prepared.name;
|
|
2130
2173
|
const childIno = parent.entries.get(name);
|
|
2131
|
-
let child = childIno === undefined ? undefined : view(childIno);
|
|
2174
|
+
let child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
2132
2175
|
if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
|
|
2133
2176
|
chosen.followFinalSymlink !== false) {
|
|
2134
2177
|
const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
|
|
@@ -2163,7 +2206,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2163
2206
|
yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
|
|
2164
2207
|
if (replaced !== undefined)
|
|
2165
2208
|
yield* authorizeRemoval(parent, replaced, entryOp);
|
|
2166
|
-
yield* replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp);
|
|
2209
|
+
yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
|
|
2167
2210
|
}
|
|
2168
2211
|
else {
|
|
2169
2212
|
yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
|
|
@@ -2188,8 +2231,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2188
2231
|
data.set(captured, position);
|
|
2189
2232
|
}
|
|
2190
2233
|
const now = yield* timestamp(op);
|
|
2191
|
-
const d =
|
|
2192
|
-
const node = file ??
|
|
2234
|
+
const d = yield* Draft;
|
|
2235
|
+
const node = file ??
|
|
2236
|
+
(yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
|
|
2193
2237
|
const written = {
|
|
2194
2238
|
...node,
|
|
2195
2239
|
data,
|
|
@@ -2201,18 +2245,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2201
2245
|
ctimeNs: now
|
|
2202
2246
|
}
|
|
2203
2247
|
};
|
|
2204
|
-
d.
|
|
2248
|
+
yield* d.addUsedBytes(BigInt(size - previous));
|
|
2205
2249
|
if (file === undefined) {
|
|
2206
2250
|
if (replaced !== undefined)
|
|
2207
|
-
detach(replaced, parent.ino, name, now);
|
|
2208
|
-
attach(directoryNow(parent.ino), name, written, now);
|
|
2251
|
+
yield* detach(replaced, parent.ino, name, now);
|
|
2252
|
+
yield* attach(yield* directoryNow(parent.ino), name, written, now);
|
|
2209
2253
|
if (replaced === undefined)
|
|
2210
|
-
d.
|
|
2211
|
-
publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
|
|
2254
|
+
yield* d.addEntries(1);
|
|
2255
|
+
yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
|
|
2212
2256
|
}
|
|
2213
2257
|
else {
|
|
2214
|
-
d.put(written);
|
|
2215
|
-
publishNode(written.ino);
|
|
2258
|
+
yield* d.put(written);
|
|
2259
|
+
yield* publishNode(written.ino);
|
|
2216
2260
|
}
|
|
2217
2261
|
}));
|
|
2218
2262
|
}),
|
|
@@ -2373,18 +2417,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2373
2417
|
const statOp = OpContext.make("stat");
|
|
2374
2418
|
const handle = Object.freeze({
|
|
2375
2419
|
[DirectoryHandleId]: true,
|
|
2376
|
-
stat: coordinatedRead(statOp, Effect.
|
|
2377
|
-
const node = acquired.ino === undefined ? undefined : view(acquired.ino);
|
|
2378
|
-
return node === undefined
|
|
2420
|
+
stat: coordinatedRead(statOp, Effect.gen(function* () {
|
|
2421
|
+
const node = acquired.ino === undefined ? undefined : yield* view(acquired.ino);
|
|
2422
|
+
return yield* (node === undefined
|
|
2379
2423
|
? Effect.fail(statOp.fail("InvalidHandle"))
|
|
2380
|
-
: Effect.succeed(withMetadata(node));
|
|
2424
|
+
: Effect.succeed(withMetadata(node)));
|
|
2381
2425
|
})).pipe(Effect.withSpan("DirectoryHandle.stat")),
|
|
2382
2426
|
close: coordinatedCleanup(Effect.suspend(() => {
|
|
2383
2427
|
if (acquired.ino === undefined) {
|
|
2384
2428
|
return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
|
|
2385
2429
|
}
|
|
2386
|
-
releaseDirectory(acquired);
|
|
2387
|
-
return Effect.void;
|
|
2430
|
+
return releaseDirectory(acquired);
|
|
2388
2431
|
})).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
|
|
2389
2432
|
});
|
|
2390
2433
|
handles.set(handle, acquired);
|