@effect-vfs/core 0.3.1 → 0.5.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 +41 -150
- package/dist/BytePath.d.ts +2 -1
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/LiveVolume.d.ts +104 -0
- package/dist/LiveVolume.d.ts.map +1 -0
- package/dist/LiveVolume.js +73 -0
- package/dist/Snapshot.d.ts +4 -2
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +4 -2
- package/dist/VirtualFileSystem.d.ts +333 -108
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +122 -32
- package/dist/VirtualFileSystemError.d.ts +54 -0
- package/dist/VirtualFileSystemError.d.ts.map +1 -0
- package/dist/VirtualFileSystemError.js +49 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/internal/errors.d.ts +12 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/{virtualFileSystem/errors.js → errors.js} +11 -15
- package/dist/internal/{virtualFileSystem/fixture.d.ts → fixture.d.ts} +11 -8
- package/dist/internal/fixture.d.ts.map +1 -0
- package/dist/internal/{virtualFileSystem/fixture.js → fixture.js} +11 -5
- package/dist/internal/liveImage.d.ts +286 -0
- package/dist/internal/liveImage.d.ts.map +1 -0
- package/dist/internal/liveImage.js +176 -0
- package/dist/internal/{virtualFileSystem/overlayContent.d.ts → overlayContent.d.ts} +2 -2
- package/dist/internal/overlayContent.d.ts.map +1 -0
- package/dist/internal/{virtualFileSystem/overlayContent.js → overlayContent.js} +2 -2
- package/dist/internal/{virtualFileSystem/overlayDiff.d.ts → overlayDiff.d.ts} +1 -1
- package/dist/internal/overlayDiff.d.ts.map +1 -0
- package/dist/internal/{virtualFileSystem/overlayDiff.js → overlayDiff.js} +1 -1
- package/dist/internal/{virtualFileSystem/path.d.ts → path.d.ts} +2 -2
- package/dist/internal/path.d.ts.map +1 -0
- package/dist/internal/{virtualFileSystem/path.js → path.js} +1 -1
- package/dist/internal/stagedState.d.ts +22 -0
- package/dist/internal/stagedState.d.ts.map +1 -0
- package/dist/internal/stagedState.js +62 -0
- package/dist/internal/{virtualFileSystem/testHooks.d.ts → testHooks.d.ts} +2 -2
- package/dist/internal/testHooks.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem.d.ts +224 -16
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +1294 -159
- package/dist/internal/{virtualFileSystem/watchHub.d.ts → watchHub.d.ts} +3 -3
- package/dist/internal/watchHub.d.ts.map +1 -0
- package/dist/internal/watchHub.js +54 -0
- package/package.json +14 -6
- package/dist/internal/virtualFileSystem/errors.d.ts +0 -31
- package/dist/internal/virtualFileSystem/errors.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/fixture.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/path.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/testHooks.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/watchHub.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/watchHub.js +0 -33
- /package/dist/internal/{virtualFileSystem/testHooks.js → testHooks.js} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Runtime definitions and cohesive live virtual filesystem engine.
|
|
2
2
|
import * as ByteSize from "effect/ByteSize";
|
|
3
3
|
import * as Clock from "effect/Clock";
|
|
4
|
-
import * as
|
|
4
|
+
import * as Crypto from "effect/Crypto";
|
|
5
5
|
import * as Data from "effect/Data";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Encoding from "effect/Encoding";
|
|
8
|
+
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as Order from "effect/Order";
|
|
8
10
|
import * as Predicate from "effect/Predicate";
|
|
9
11
|
import * as Result from "effect/Result";
|
|
10
12
|
import * as Schema from "effect/Schema";
|
|
@@ -12,14 +14,16 @@ import * as Semaphore from "effect/Semaphore";
|
|
|
12
14
|
import { BytePath } from "../BytePath.js";
|
|
13
15
|
import { ImageError } from "../Snapshot.js";
|
|
14
16
|
import { CanonicalBase64 } from "./canonicalBase64.js";
|
|
17
|
+
import { ConfigurationError, decodeConfiguration, FsCode as FsCodeSchema, FsError } from "./errors.js";
|
|
15
18
|
import * as Image from "./image.js";
|
|
19
|
+
import * as LiveImage from "./liveImage.js";
|
|
16
20
|
import * as MetadataDomain from "./metadata.js";
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import * as TestHooks from "./
|
|
22
|
-
import * as WatchHub from "./
|
|
21
|
+
import * as Content from "./overlayContent.js";
|
|
22
|
+
import { compareOverlay } from "./overlayDiff.js";
|
|
23
|
+
import { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, MAX_NAME_BYTES, nameBytes, ownedPath, preparePath, SLASH_BYTE, SLASH_HEX, strictString } from "./path.js";
|
|
24
|
+
import { makeStagedState } from "./stagedState.js";
|
|
25
|
+
import * as TestHooks from "./testHooks.js";
|
|
26
|
+
import * as WatchHub from "./watchHub.js";
|
|
23
27
|
/** @internal */
|
|
24
28
|
export const VolumeId = Symbol("@effect-vfs/core/Volume");
|
|
25
29
|
/** @internal */
|
|
@@ -60,9 +64,34 @@ export const RootCallerOptions = Schema.Struct({
|
|
|
60
64
|
identity: Schema.optionalKey(Identity),
|
|
61
65
|
umask: Schema.optionalKey(Schema.Natural.check(Schema.isLessThanOrEqualTo(0o777)))
|
|
62
66
|
});
|
|
67
|
+
const Hex128 = Schema.String.check(Schema.isPattern(/^[0-9a-f]{32}$/));
|
|
68
|
+
/** @internal */
|
|
69
|
+
export const VolumeDurability = Schema.Literals([
|
|
70
|
+
"memory-only",
|
|
71
|
+
"survives-process-crash",
|
|
72
|
+
"survives-operating-system-crash",
|
|
73
|
+
"survives-power-loss"
|
|
74
|
+
]);
|
|
75
|
+
const durabilityRank = {
|
|
76
|
+
"memory-only": 0,
|
|
77
|
+
"survives-process-crash": 1,
|
|
78
|
+
"survives-operating-system-crash": 2,
|
|
79
|
+
"survives-power-loss": 3
|
|
80
|
+
};
|
|
81
|
+
/** @internal */
|
|
82
|
+
export const VolumeDurabilityOrder = Order.mapInput(Order.Number, (durability) => durabilityRank[durability]);
|
|
83
|
+
/** @internal */
|
|
84
|
+
export const isVolumeDurabilityAtLeast = (actual, required) => VolumeDurabilityOrder(actual, required) >= 0;
|
|
85
|
+
/** @internal */
|
|
86
|
+
export const VolumeIdentity = Hex128.pipe(Schema.brand("@effect-vfs/core/VolumeIdentity"));
|
|
87
|
+
/** @internal */
|
|
88
|
+
export const VolumeIncarnation = Hex128.pipe(Schema.brand("@effect-vfs/core/VolumeIncarnation"));
|
|
63
89
|
/** @internal */
|
|
64
90
|
export const VolumeOptions = Schema.Struct({
|
|
91
|
+
identity: Schema.optionalKey(VolumeIdentity),
|
|
65
92
|
maxEntries: Schema.optionalKey(Schema.Natural),
|
|
93
|
+
maxPendingOperations: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
|
|
94
|
+
maxWatchEvents: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(2))),
|
|
66
95
|
maxBytes: Schema.optionalKey(Schema.ByteSize),
|
|
67
96
|
maxFileBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isLessThanOrEqualTo(size, ByteSize.bytes(0xffffffff)) ? undefined : "must be at most 4294967295 bytes"))),
|
|
68
97
|
maxPathBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isGreaterThanOrEqualTo(size, ByteSize.bytes(1)) ? undefined : "must be at least 1 byte")))
|
|
@@ -107,6 +136,49 @@ export const OpenSettings = Schema.Struct({
|
|
|
107
136
|
truncate: Schema.optionalKey(Schema.Boolean),
|
|
108
137
|
followFinalSymlink: Schema.optionalKey(Schema.Boolean)
|
|
109
138
|
});
|
|
139
|
+
/** @internal */
|
|
140
|
+
export const DirectoryChange = Schema.Struct({
|
|
141
|
+
before: Schema.BigInt,
|
|
142
|
+
after: Schema.BigInt
|
|
143
|
+
});
|
|
144
|
+
/** @internal */
|
|
145
|
+
export const RenameReferenceResult = Schema.TaggedUnion({
|
|
146
|
+
SameDirectory: { directory: DirectoryChange },
|
|
147
|
+
DifferentDirectories: {
|
|
148
|
+
sourceDirectory: DirectoryChange,
|
|
149
|
+
destinationDirectory: DirectoryChange
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
/** @internal */
|
|
153
|
+
export const MkdirReferenceSettings = Schema.Struct({
|
|
154
|
+
mode: Schema.optionalKey(Mode),
|
|
155
|
+
exactMode: Schema.optionalKey(Schema.Boolean),
|
|
156
|
+
times: Schema.optionalKey(Times)
|
|
157
|
+
});
|
|
158
|
+
/** @internal */
|
|
159
|
+
export const SymlinkReferenceSettings = Schema.Struct({
|
|
160
|
+
times: Schema.optionalKey(Times)
|
|
161
|
+
});
|
|
162
|
+
/** @internal */
|
|
163
|
+
export const OpenReferenceSettings = Schema.Struct({
|
|
164
|
+
access: Schema.Literals(["read", "write", "readWrite"]),
|
|
165
|
+
append: Schema.optionalKey(Schema.Boolean),
|
|
166
|
+
truncate: Schema.optionalKey(Schema.Boolean)
|
|
167
|
+
});
|
|
168
|
+
/** @internal */
|
|
169
|
+
export const OpenChildReferenceSettings = Schema.Struct({
|
|
170
|
+
...OpenSettings.fields,
|
|
171
|
+
times: Schema.optionalKey(Times),
|
|
172
|
+
initialSize: Schema.optionalKey(Schema.BigInt),
|
|
173
|
+
exactMode: Schema.optionalKey(Schema.Boolean),
|
|
174
|
+
owner: Schema.optionalKey(OwnerUpdate),
|
|
175
|
+
expectedChild: Schema.optionalKey(Schema.NullOr(Schema.Struct({
|
|
176
|
+
reference: Schema.declare((input) => Predicate.hasProperty(ObjectReferenceId)(input) && input[ObjectReferenceId] === true),
|
|
177
|
+
revision: Schema.BigInt,
|
|
178
|
+
atimeNs: MetadataDomain.Timestamp,
|
|
179
|
+
mtimeNs: MetadataDomain.Timestamp
|
|
180
|
+
})))
|
|
181
|
+
});
|
|
110
182
|
const WriteFileSettings = Schema.Struct({
|
|
111
183
|
...OpenSettings.fields,
|
|
112
184
|
replaceFinalSymlink: Schema.optionalKey(Schema.Boolean),
|
|
@@ -116,6 +188,10 @@ const decodeOwnerUpdate = Schema.decodeResult(OwnerUpdate, { onExcessProperty: "
|
|
|
116
188
|
const decodeTimes = Schema.decodeResult(Times, { onExcessProperty: "error" });
|
|
117
189
|
const decodeWriteFileSettings = Schema.decodeResult(WriteFileSettings, { onExcessProperty: "error" });
|
|
118
190
|
const decodeOpenSettings = Schema.decodeResult(OpenSettings, { onExcessProperty: "error" });
|
|
191
|
+
const decodeMkdirReferenceSettings = Schema.decodeResult(MkdirReferenceSettings, { onExcessProperty: "error" });
|
|
192
|
+
const decodeSymlinkReferenceSettings = Schema.decodeResult(SymlinkReferenceSettings, { onExcessProperty: "error" });
|
|
193
|
+
const decodeOpenReferenceSettings = Schema.decodeResult(OpenReferenceSettings, { onExcessProperty: "error" });
|
|
194
|
+
const decodeOpenChildReferenceSettings = Schema.decodeResult(OpenChildReferenceSettings, { onExcessProperty: "error" });
|
|
119
195
|
/** @internal */
|
|
120
196
|
export const OverlayNodeKind = Schema.Literals(["directory", "file", "symlink"]);
|
|
121
197
|
/** @internal */
|
|
@@ -154,9 +230,6 @@ export const OverlayChangesOptions = Schema.Struct({
|
|
|
154
230
|
includeTimestamps: Schema.optionalKey(Schema.Boolean)
|
|
155
231
|
});
|
|
156
232
|
/** @internal */
|
|
157
|
-
export class CurrentFileSystem extends Context.Service()("@effect-vfs/core/CurrentFileSystem") {
|
|
158
|
-
}
|
|
159
|
-
/** @internal */
|
|
160
233
|
export const FixtureMetadata = Schema.Struct({
|
|
161
234
|
uid: Schema.optionalKey(Schema.Natural),
|
|
162
235
|
gid: Schema.optionalKey(Schema.Natural),
|
|
@@ -196,6 +269,68 @@ export const Fixture = Schema.Struct({
|
|
|
196
269
|
rootMetadata: Schema.optionalKey(FixtureMetadata),
|
|
197
270
|
entries: Schema.Array(FixtureEntry)
|
|
198
271
|
});
|
|
272
|
+
/** @internal */
|
|
273
|
+
export const captureLiveImage = Effect.fnUntraced(function* (state, identity, limits) {
|
|
274
|
+
const records = [];
|
|
275
|
+
const visited = new Set();
|
|
276
|
+
const pending = [state.root, ...state.retainedFiles.values()];
|
|
277
|
+
for (let index = 0; index < pending.length; index++) {
|
|
278
|
+
if (index % WALK_YIELD_INTERVAL === 0)
|
|
279
|
+
yield* Effect.yieldNow;
|
|
280
|
+
const node = pending[index];
|
|
281
|
+
if (node === undefined || visited.has(node.metadata.ino))
|
|
282
|
+
continue;
|
|
283
|
+
visited.add(node.metadata.ino);
|
|
284
|
+
const common = {
|
|
285
|
+
ino: node.metadata.ino,
|
|
286
|
+
revision: node.revision,
|
|
287
|
+
metadata: {
|
|
288
|
+
...storedMetadata(node.metadata),
|
|
289
|
+
nlink: node.metadata.nlink,
|
|
290
|
+
size: node.metadata.size
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
if (node.lineage !== undefined)
|
|
294
|
+
common.lineage = node.lineage;
|
|
295
|
+
if (node.kind === "directory") {
|
|
296
|
+
const entries = [];
|
|
297
|
+
for (const [name, child] of node.entries) {
|
|
298
|
+
entries.push({ name: CanonicalBase64.encode(nameBytes(name)), target: child.metadata.ino });
|
|
299
|
+
pending.push(child);
|
|
300
|
+
}
|
|
301
|
+
records.push(LiveImage.Record.cases.directory.make({ ...common, entries }));
|
|
302
|
+
}
|
|
303
|
+
else if (node.kind === "file") {
|
|
304
|
+
records.push(LiveImage.Record.cases.file.make({ ...common, data: CanonicalBase64.encode(node.data.bytes) }));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
records.push(LiveImage.Record.cases.symlink.make({ ...common, target: CanonicalBase64.encode(node.target) }));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const storedLimits = {};
|
|
311
|
+
if (limits.maxEntries !== undefined)
|
|
312
|
+
storedLimits.maxEntries = limits.maxEntries;
|
|
313
|
+
if (limits.maxBytes !== undefined)
|
|
314
|
+
storedLimits.maxBytes = ByteSize.toBigInt(limits.maxBytes);
|
|
315
|
+
if (limits.maxFileBytes !== undefined)
|
|
316
|
+
storedLimits.maxFileBytes = ByteSize.toBigInt(limits.maxFileBytes);
|
|
317
|
+
if (limits.maxPathBytes !== undefined)
|
|
318
|
+
storedLimits.maxPathBytes = ByteSize.toBigInt(limits.maxPathBytes);
|
|
319
|
+
const document = {
|
|
320
|
+
format: "effect-vfs-live",
|
|
321
|
+
version: 1,
|
|
322
|
+
identity,
|
|
323
|
+
root: state.root.metadata.ino,
|
|
324
|
+
nextInode: state.nextInode,
|
|
325
|
+
revisionCounter: state.revisionCounter,
|
|
326
|
+
entries: state.entries,
|
|
327
|
+
usedBytes: state.usedBytes,
|
|
328
|
+
limits: storedLimits,
|
|
329
|
+
retainedFiles: [...state.retainedFiles.keys()],
|
|
330
|
+
records
|
|
331
|
+
};
|
|
332
|
+
return yield* LiveImage.encode(document);
|
|
333
|
+
});
|
|
199
334
|
const objectReferences = new WeakMap();
|
|
200
335
|
const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
|
|
201
336
|
// Only a PathInput target names a path; a handle-based call has none to report in its error.
|
|
@@ -228,14 +363,34 @@ const storedMetadata = (metadata) => ({
|
|
|
228
363
|
/** @internal */
|
|
229
364
|
export const VolumeSource = Data.taggedEnum();
|
|
230
365
|
const UpdateChange = Schema.TaggedStruct("Update", { path: BytePath });
|
|
366
|
+
const RescanChange = Schema.TaggedStruct("Rescan", { path: BytePath });
|
|
231
367
|
// Each execution constructs a fresh volume and captures its Clock.
|
|
232
368
|
/** @internal */
|
|
233
|
-
export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
369
|
+
export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial, durability = "memory-only") {
|
|
234
370
|
const image = "image" in source ? source.image : undefined;
|
|
235
|
-
const
|
|
371
|
+
const live = Predicate.isTagged("Live")(source) ? source.document : undefined;
|
|
372
|
+
let restoredOptions = options;
|
|
373
|
+
if (live !== undefined) {
|
|
374
|
+
const recovered = { identity: VolumeIdentity.make(live.identity) };
|
|
375
|
+
if (live.limits.maxEntries !== undefined)
|
|
376
|
+
recovered.maxEntries = live.limits.maxEntries;
|
|
377
|
+
if (live.limits.maxBytes !== undefined)
|
|
378
|
+
recovered.maxBytes = ByteSize.bytes(live.limits.maxBytes);
|
|
379
|
+
if (live.limits.maxFileBytes !== undefined)
|
|
380
|
+
recovered.maxFileBytes = ByteSize.bytes(live.limits.maxFileBytes);
|
|
381
|
+
if (live.limits.maxPathBytes !== undefined)
|
|
382
|
+
recovered.maxPathBytes = ByteSize.bytes(live.limits.maxPathBytes);
|
|
383
|
+
restoredOptions = recovered;
|
|
384
|
+
}
|
|
385
|
+
const decoded = decodeConfiguration(VolumeOptions, restoredOptions === undefined ? {} : restoredOptions);
|
|
236
386
|
if (Result.isFailure(decoded))
|
|
237
387
|
return yield* decoded.failure;
|
|
238
388
|
const settings = { ...decoded.success };
|
|
389
|
+
const crypto = yield* Crypto.Crypto;
|
|
390
|
+
const identity = settings.identity === undefined
|
|
391
|
+
? VolumeIdentity.make(Encoding.encodeHex(yield* crypto.randomBytes(16)))
|
|
392
|
+
: VolumeIdentity.make(settings.identity);
|
|
393
|
+
const incarnation = VolumeIncarnation.make(Encoding.encodeHex(yield* crypto.randomBytes(16)));
|
|
239
394
|
const clock = yield* Clock.clockWith(Effect.succeed);
|
|
240
395
|
const initialTime = clock.currentTimeNanosUnsafe();
|
|
241
396
|
if (!isTimestamp(initialTime)) {
|
|
@@ -248,28 +403,177 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
248
403
|
: Effect.fail(new FsError({ code: "InvalidArgument", operation: operation }));
|
|
249
404
|
});
|
|
250
405
|
const volumeIdentity = Symbol();
|
|
406
|
+
let activeStage;
|
|
407
|
+
const fileReferences = new Set();
|
|
408
|
+
const directoryReferences = new Set();
|
|
409
|
+
// A capability holds a cell instead of a particular node object. Publication can replace
|
|
410
|
+
// the node behind the cell without replacing the capability held by a caller.
|
|
411
|
+
const cells = new WeakMap();
|
|
412
|
+
const cellFor = (node) => {
|
|
413
|
+
const existing = cells.get(node);
|
|
414
|
+
if (existing !== undefined)
|
|
415
|
+
return existing;
|
|
416
|
+
const previous = activeStage?.previous.get(node);
|
|
417
|
+
const retained = previous === undefined ? undefined : cells.get(previous);
|
|
418
|
+
if (retained !== undefined) {
|
|
419
|
+
cells.set(node, retained);
|
|
420
|
+
return retained;
|
|
421
|
+
}
|
|
422
|
+
let current = node;
|
|
423
|
+
const cell = {
|
|
424
|
+
get node() {
|
|
425
|
+
return activeStage?.nodes.get(current) ?? current;
|
|
426
|
+
},
|
|
427
|
+
set node(next) {
|
|
428
|
+
current = next;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
cells.set(node, cell);
|
|
432
|
+
return cell;
|
|
433
|
+
};
|
|
434
|
+
const makeFileReference = (access, append) => {
|
|
435
|
+
const live = { cell: undefined, closed: false, offset: 0n };
|
|
436
|
+
let reference;
|
|
437
|
+
const record = () => {
|
|
438
|
+
if (activeStage === undefined)
|
|
439
|
+
return live;
|
|
440
|
+
let staged = activeStage.files.get(reference);
|
|
441
|
+
if (staged === undefined) {
|
|
442
|
+
staged = { ...live };
|
|
443
|
+
activeStage.files.set(reference, staged);
|
|
444
|
+
const pending = staged;
|
|
445
|
+
activeStage.apply.push(() => {
|
|
446
|
+
Object.assign(live, pending);
|
|
447
|
+
if (live.cell === undefined)
|
|
448
|
+
fileReferences.delete(reference);
|
|
449
|
+
else
|
|
450
|
+
fileReferences.add(reference);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return staged;
|
|
454
|
+
};
|
|
455
|
+
reference = {
|
|
456
|
+
volume: volumeIdentity,
|
|
457
|
+
get file() {
|
|
458
|
+
const node = record().cell?.node;
|
|
459
|
+
return node?.kind === "file" ? node : undefined;
|
|
460
|
+
},
|
|
461
|
+
set file(file) {
|
|
462
|
+
record().cell = file === undefined ? undefined : cellFor(file);
|
|
463
|
+
if (activeStage === undefined) {
|
|
464
|
+
if (file === undefined)
|
|
465
|
+
fileReferences.delete(reference);
|
|
466
|
+
else
|
|
467
|
+
fileReferences.add(reference);
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
get closed() {
|
|
471
|
+
return record().closed;
|
|
472
|
+
},
|
|
473
|
+
set closed(value) {
|
|
474
|
+
record().closed = value;
|
|
475
|
+
},
|
|
476
|
+
get offset() {
|
|
477
|
+
return record().offset;
|
|
478
|
+
},
|
|
479
|
+
set offset(value) {
|
|
480
|
+
record().offset = value;
|
|
481
|
+
},
|
|
482
|
+
access,
|
|
483
|
+
append
|
|
484
|
+
};
|
|
485
|
+
return reference;
|
|
486
|
+
};
|
|
487
|
+
const makeDirectoryReference = (directory) => {
|
|
488
|
+
const live = {
|
|
489
|
+
cell: directory === undefined ? undefined : cellFor(directory),
|
|
490
|
+
closed: false
|
|
491
|
+
};
|
|
492
|
+
let reference;
|
|
493
|
+
const record = () => {
|
|
494
|
+
if (activeStage === undefined)
|
|
495
|
+
return live;
|
|
496
|
+
let staged = activeStage.directories.get(reference);
|
|
497
|
+
if (staged === undefined) {
|
|
498
|
+
staged = { ...live };
|
|
499
|
+
activeStage.directories.set(reference, staged);
|
|
500
|
+
const pending = staged;
|
|
501
|
+
activeStage.apply.push(() => {
|
|
502
|
+
Object.assign(live, pending);
|
|
503
|
+
if (live.cell === undefined || live.cell.node === state.root)
|
|
504
|
+
directoryReferences.delete(reference);
|
|
505
|
+
else
|
|
506
|
+
directoryReferences.add(reference);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return staged;
|
|
510
|
+
};
|
|
511
|
+
reference = {
|
|
512
|
+
volume: volumeIdentity,
|
|
513
|
+
get directory() {
|
|
514
|
+
const node = record().cell?.node;
|
|
515
|
+
return node?.kind === "directory" ? node : undefined;
|
|
516
|
+
},
|
|
517
|
+
set directory(directory) {
|
|
518
|
+
record().cell = directory === undefined ? undefined : cellFor(directory);
|
|
519
|
+
if (activeStage === undefined) {
|
|
520
|
+
if (directory === undefined || directory === state.root)
|
|
521
|
+
directoryReferences.delete(reference);
|
|
522
|
+
else
|
|
523
|
+
directoryReferences.add(reference);
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
get closed() {
|
|
527
|
+
return record().closed;
|
|
528
|
+
},
|
|
529
|
+
set closed(value) {
|
|
530
|
+
record().closed = value;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
if (directory !== undefined && directory !== state.root && activeStage === undefined) {
|
|
534
|
+
directoryReferences.add(reference);
|
|
535
|
+
}
|
|
536
|
+
return reference;
|
|
537
|
+
};
|
|
251
538
|
const gate = Semaphore.makeUnsafe(1);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
539
|
+
const maxPendingOperations = settings.maxPendingOperations ?? 64;
|
|
540
|
+
let admitted = 0;
|
|
541
|
+
const admit = (operation, effect) => Effect.suspend(() => {
|
|
542
|
+
if (admitted >= maxPendingOperations + 1) {
|
|
543
|
+
return Effect.fail(new FsError({ code: "VolumeBusy", operation }));
|
|
544
|
+
}
|
|
545
|
+
admitted += 1;
|
|
546
|
+
return effect.pipe(Effect.ensuring(Effect.sync(() => {
|
|
547
|
+
admitted -= 1;
|
|
548
|
+
})));
|
|
549
|
+
});
|
|
550
|
+
let state = {
|
|
551
|
+
root: {
|
|
552
|
+
kind: "directory",
|
|
553
|
+
lineage: image?.root,
|
|
554
|
+
parent: undefined,
|
|
555
|
+
entries: new Map(),
|
|
556
|
+
metadata: directoryMetadata(1n, 0, 0, 0o755, initialTime),
|
|
557
|
+
revision: 1n,
|
|
558
|
+
objectReference: undefined
|
|
559
|
+
},
|
|
560
|
+
retainedFiles: new Map(),
|
|
561
|
+
revisionCounter: 1n,
|
|
562
|
+
nextInode: 2n,
|
|
563
|
+
entries: 0,
|
|
564
|
+
usedBytes: 0n
|
|
262
565
|
};
|
|
263
|
-
|
|
264
|
-
let entries = 0;
|
|
265
|
-
let usedBytes = 0n;
|
|
566
|
+
const nextRevision = () => ++state.revisionCounter;
|
|
266
567
|
// The schema caps this value at uint32, so this boundary conversion is exact.
|
|
267
568
|
const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(0xffffffff)));
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
569
|
+
const limits = Object.freeze({
|
|
570
|
+
maxBytes: settings.maxBytes,
|
|
571
|
+
maxFileBytes: ByteSize.bytes(maxFileBytes),
|
|
572
|
+
maxEntries: settings.maxEntries,
|
|
573
|
+
maxPathBytes: settings.maxPathBytes,
|
|
574
|
+
maxPendingOperations: settings.maxPendingOperations ?? 64,
|
|
575
|
+
maxWatchEvents: settings.maxWatchEvents ?? 256
|
|
576
|
+
});
|
|
273
577
|
if (image !== undefined) {
|
|
274
578
|
const incoming = new Map();
|
|
275
579
|
let content = 0n;
|
|
@@ -294,7 +598,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
294
598
|
const metadata = {
|
|
295
599
|
...record.metadata,
|
|
296
600
|
kind: record._tag,
|
|
297
|
-
ino: record.id === image.root ? 1n : nextInode++,
|
|
601
|
+
ino: record.id === image.root ? 1n : state.nextInode++,
|
|
298
602
|
nlink: Image.Record.guards.directory(record) ? 2 : 0,
|
|
299
603
|
size: 0n,
|
|
300
604
|
atimeNs: record.metadata.atimeNs,
|
|
@@ -304,7 +608,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
304
608
|
};
|
|
305
609
|
if (Image.Record.guards.directory(record)) {
|
|
306
610
|
const node = record.id === image.root
|
|
307
|
-
? root
|
|
611
|
+
? state.root
|
|
308
612
|
: {
|
|
309
613
|
kind: "directory",
|
|
310
614
|
lineage: record.id,
|
|
@@ -360,10 +664,191 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
360
664
|
node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1 };
|
|
361
665
|
}
|
|
362
666
|
}
|
|
363
|
-
entries = count;
|
|
364
|
-
usedBytes = content;
|
|
667
|
+
state.entries = count;
|
|
668
|
+
state.usedBytes = content;
|
|
365
669
|
}
|
|
366
|
-
|
|
670
|
+
if (live !== undefined) {
|
|
671
|
+
const incoming = new Map();
|
|
672
|
+
for (const record of live.records) {
|
|
673
|
+
const metadata = {
|
|
674
|
+
...record.metadata,
|
|
675
|
+
kind: record._tag,
|
|
676
|
+
ino: record.ino
|
|
677
|
+
};
|
|
678
|
+
if (LiveImage.Record.guards.directory(record)) {
|
|
679
|
+
incoming.set(record.ino, {
|
|
680
|
+
kind: "directory",
|
|
681
|
+
lineage: record.lineage,
|
|
682
|
+
parent: undefined,
|
|
683
|
+
entries: new Map(),
|
|
684
|
+
metadata,
|
|
685
|
+
revision: record.revision,
|
|
686
|
+
objectReference: undefined
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
else if (LiveImage.Record.guards.file(record)) {
|
|
690
|
+
incoming.set(record.ino, {
|
|
691
|
+
kind: "file",
|
|
692
|
+
lineage: record.lineage,
|
|
693
|
+
data: Content.make(yield* CanonicalBase64.decode(record.data)),
|
|
694
|
+
openCount: 0,
|
|
695
|
+
metadata,
|
|
696
|
+
revision: record.revision,
|
|
697
|
+
objectReference: undefined
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
incoming.set(record.ino, {
|
|
702
|
+
kind: "symlink",
|
|
703
|
+
lineage: record.lineage,
|
|
704
|
+
target: yield* CanonicalBase64.decode(record.target),
|
|
705
|
+
metadata,
|
|
706
|
+
revision: record.revision,
|
|
707
|
+
objectReference: undefined
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for (const record of live.records) {
|
|
712
|
+
if (!LiveImage.Record.guards.directory(record))
|
|
713
|
+
continue;
|
|
714
|
+
const parent = incoming.get(record.ino);
|
|
715
|
+
if (parent?.kind !== "directory")
|
|
716
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
717
|
+
for (const entry of record.entries) {
|
|
718
|
+
const child = incoming.get(entry.target);
|
|
719
|
+
if (child === undefined)
|
|
720
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
721
|
+
parent.entries.set(Encoding.encodeHex(yield* CanonicalBase64.decode(entry.name)), child);
|
|
722
|
+
if (child.kind === "directory")
|
|
723
|
+
child.parent = parent;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const root = incoming.get(live.root);
|
|
727
|
+
if (root?.kind !== "directory")
|
|
728
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
729
|
+
state.root = root;
|
|
730
|
+
state.nextInode = live.nextInode;
|
|
731
|
+
state.revisionCounter = live.revisionCounter;
|
|
732
|
+
state.entries = live.entries;
|
|
733
|
+
state.usedBytes = live.usedBytes;
|
|
734
|
+
// A previous process's handles no longer exist. Their zero-link files
|
|
735
|
+
// remain in the stored image but are reclaimed from this runtime state.
|
|
736
|
+
for (const ino of live.retainedFiles) {
|
|
737
|
+
const orphan = incoming.get(ino);
|
|
738
|
+
if (orphan?.kind !== "file")
|
|
739
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
740
|
+
state.usedBytes -= BigInt(orphan.data.bytes.length);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (captureInitial !== undefined)
|
|
744
|
+
yield* captureInitial(state, identity, limits);
|
|
745
|
+
const contexts = new WeakMap();
|
|
746
|
+
const copyState = (current) => Effect.sync(() => {
|
|
747
|
+
const nodes = new Map();
|
|
748
|
+
const copyDirectory = (node) => {
|
|
749
|
+
const existing = nodes.get(node);
|
|
750
|
+
if (existing?.kind === "directory")
|
|
751
|
+
return existing;
|
|
752
|
+
const copy = { ...node, entries: new Map(), metadata: { ...node.metadata }, parent: undefined };
|
|
753
|
+
nodes.set(node, copy);
|
|
754
|
+
copy.parent = node.parent === undefined ? undefined : copyDirectory(node.parent);
|
|
755
|
+
for (const [name, child] of node.entries)
|
|
756
|
+
copy.entries.set(name, copyNode(child));
|
|
757
|
+
return copy;
|
|
758
|
+
};
|
|
759
|
+
const copyNode = (node) => {
|
|
760
|
+
if (node.kind === "directory")
|
|
761
|
+
return copyDirectory(node);
|
|
762
|
+
const existing = nodes.get(node);
|
|
763
|
+
if (existing !== undefined)
|
|
764
|
+
return existing;
|
|
765
|
+
const copy = { ...node, metadata: { ...node.metadata } };
|
|
766
|
+
nodes.set(node, copy);
|
|
767
|
+
return copy;
|
|
768
|
+
};
|
|
769
|
+
const candidate = { ...current, root: copyDirectory(current.root), retainedFiles: new Map() };
|
|
770
|
+
for (const [ino, file] of current.retainedFiles) {
|
|
771
|
+
const copy = copyNode(file);
|
|
772
|
+
if (copy.kind === "file")
|
|
773
|
+
candidate.retainedFiles.set(ino, copy);
|
|
774
|
+
}
|
|
775
|
+
// Open resources keep zero-link objects alive after the namespace stops reaching them.
|
|
776
|
+
for (const reference of fileReferences) {
|
|
777
|
+
const file = reference.file;
|
|
778
|
+
if (file !== undefined)
|
|
779
|
+
copyNode(file);
|
|
780
|
+
}
|
|
781
|
+
for (const reference of directoryReferences) {
|
|
782
|
+
const directory = reference.directory;
|
|
783
|
+
if (directory !== undefined)
|
|
784
|
+
copyDirectory(directory);
|
|
785
|
+
}
|
|
786
|
+
contexts.set(candidate, {
|
|
787
|
+
nodes,
|
|
788
|
+
previous: new Map([...nodes].map(([prior, next]) => [next, prior])),
|
|
789
|
+
files: new Map(),
|
|
790
|
+
directories: new Map(),
|
|
791
|
+
invalidated: new Set(),
|
|
792
|
+
events: [],
|
|
793
|
+
apply: []
|
|
794
|
+
});
|
|
795
|
+
return candidate;
|
|
796
|
+
});
|
|
797
|
+
const staged = commitProvider === undefined ? undefined : makeStagedState(state, copyState, commitProvider, (candidate, events) => {
|
|
798
|
+
const context = contexts.get(candidate);
|
|
799
|
+
if (context === undefined)
|
|
800
|
+
throw new Error("Missing staged engine state");
|
|
801
|
+
state = candidate;
|
|
802
|
+
for (const [previous, next] of context.nodes) {
|
|
803
|
+
const cell = cells.get(previous);
|
|
804
|
+
if (cell !== undefined) {
|
|
805
|
+
cell.node = next;
|
|
806
|
+
cells.set(next, cell);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
for (const apply of context.apply)
|
|
810
|
+
apply();
|
|
811
|
+
for (const reference of context.invalidated) {
|
|
812
|
+
reference.active = false;
|
|
813
|
+
reference.cell = undefined;
|
|
814
|
+
}
|
|
815
|
+
for (const event of events)
|
|
816
|
+
event();
|
|
817
|
+
});
|
|
818
|
+
if (staged !== undefined)
|
|
819
|
+
commitProvider?.onReady?.(staged.shutdown);
|
|
820
|
+
// Permit waits stay interruptible. Changes and their publication run under one permit.
|
|
821
|
+
const coordinated = (operation, effect, onStorageFailure) => admit(operation, staged === undefined
|
|
822
|
+
? gate.withPermit(Effect.uninterruptible(effect))
|
|
823
|
+
: staged.mutate(operation, (candidate, emit) => Effect.gen(function* () {
|
|
824
|
+
const previous = state;
|
|
825
|
+
const context = contexts.get(candidate);
|
|
826
|
+
if (context === undefined)
|
|
827
|
+
return yield* Effect.die("Missing staged engine state");
|
|
828
|
+
state = candidate;
|
|
829
|
+
activeStage = context;
|
|
830
|
+
const result = yield* Effect.exit(effect);
|
|
831
|
+
state = previous;
|
|
832
|
+
activeStage = undefined;
|
|
833
|
+
if (Exit.isFailure(result))
|
|
834
|
+
return yield* Effect.failCause(result.cause);
|
|
835
|
+
for (const event of context.events)
|
|
836
|
+
emit(event);
|
|
837
|
+
return result.value;
|
|
838
|
+
}), onStorageFailure));
|
|
839
|
+
// Pure observations share the permit without making a candidate or calling the provider.
|
|
840
|
+
const coordinatedRead = (operation, effect) => admit(operation, staged === undefined ? gate.withPermit(effect) : staged.read(operation, () => effect));
|
|
841
|
+
const coordinatedCleanup = (effect) => staged === undefined
|
|
842
|
+
? gate.withPermit(Effect.uninterruptible(effect))
|
|
843
|
+
: staged.coordinate(Effect.uninterruptible(effect));
|
|
844
|
+
const watchCoordinate = (effect) => staged === undefined ? gate.withPermit(effect) : staged.coordinate(effect);
|
|
845
|
+
const watchHub = yield* WatchHub.make(watchCoordinate, settings.maxWatchEvents ?? 256, () => RescanChange.make({ path: ownedPath(new Uint8Array([47])) }), staged?.checkAvailable("watch"));
|
|
846
|
+
const queuePublish = (publish) => {
|
|
847
|
+
if (activeStage === undefined)
|
|
848
|
+
publish();
|
|
849
|
+
else
|
|
850
|
+
activeStage.events.push(publish);
|
|
851
|
+
};
|
|
367
852
|
const directoryHex = (directory) => {
|
|
368
853
|
const names = [];
|
|
369
854
|
let current = directory;
|
|
@@ -378,10 +863,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
378
863
|
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
379
864
|
};
|
|
380
865
|
const publishEntry = (_tag, parent, name) => {
|
|
381
|
-
watchHub.publishUnsafe(() => {
|
|
866
|
+
queuePublish(() => watchHub.publishUnsafe(() => {
|
|
382
867
|
const prefix = directoryHex(parent);
|
|
383
868
|
return { _tag, path: ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name)) };
|
|
384
|
-
});
|
|
869
|
+
}));
|
|
385
870
|
};
|
|
386
871
|
const publishNode = (target) => {
|
|
387
872
|
// Directories are never hard linked, so one name reaches them and the parent chain resolves it.
|
|
@@ -391,12 +876,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
391
876
|
// The file scan below stops on the same signal, and `lookup` already reads it as NotFound.
|
|
392
877
|
if (target.metadata.nlink === 0)
|
|
393
878
|
return;
|
|
394
|
-
watchHub.publishUnsafe(() => UpdateChange.make({ path: ownedPath(nameBytes(directoryHex(target))) }));
|
|
879
|
+
queuePublish(() => watchHub.publishUnsafe(() => UpdateChange.make({ path: ownedPath(nameBytes(directoryHex(target))) })));
|
|
395
880
|
return;
|
|
396
881
|
}
|
|
397
|
-
watchHub.publishManyUnsafe(() => {
|
|
882
|
+
queuePublish(() => watchHub.publishManyUnsafe(() => {
|
|
398
883
|
const changes = [];
|
|
399
|
-
const pending = [[root, SLASH_HEX]];
|
|
884
|
+
const pending = [[state.root, SLASH_HEX]];
|
|
400
885
|
// nlink counts the names bound to this node, so the scan stops once it has found them all.
|
|
401
886
|
while (pending.length > 0 && changes.length < target.metadata.nlink) {
|
|
402
887
|
const next = pending.pop();
|
|
@@ -413,11 +898,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
413
898
|
}
|
|
414
899
|
}
|
|
415
900
|
return changes;
|
|
416
|
-
});
|
|
901
|
+
}));
|
|
417
902
|
};
|
|
418
903
|
const captureSnapshot = Effect.fnUntraced(function* () {
|
|
419
|
-
const ids = new Map([[root, "0"]]);
|
|
420
|
-
const pending = [root];
|
|
904
|
+
const ids = new Map([[state.root, "0"]]);
|
|
905
|
+
const pending = [state.root];
|
|
421
906
|
const records = [];
|
|
422
907
|
for (let index = 0; index < pending.length; index++) {
|
|
423
908
|
if (index % WALK_YIELD_INTERVAL === 0)
|
|
@@ -453,7 +938,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
453
938
|
});
|
|
454
939
|
const observeChanges = Effect.fnUntraced(function* () {
|
|
455
940
|
const observation = [];
|
|
456
|
-
const paths = [[root, new Uint8Array([SLASH_BYTE])]];
|
|
941
|
+
const paths = [[state.root, new Uint8Array([SLASH_BYTE])]];
|
|
457
942
|
for (let index = 0; index < paths.length; index++) {
|
|
458
943
|
if (index % WALK_YIELD_INTERVAL === 0)
|
|
459
944
|
yield* Effect.yieldNow;
|
|
@@ -496,7 +981,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
496
981
|
if (node.objectReference !== undefined)
|
|
497
982
|
return node.objectReference;
|
|
498
983
|
const reference = Object.freeze({ [ObjectReferenceId]: true });
|
|
499
|
-
objectReferences.set(reference, { volume: volumeIdentity, node });
|
|
984
|
+
objectReferences.set(reference, { volume: volumeIdentity, cell: cellFor(node), active: true });
|
|
500
985
|
node.objectReference = reference;
|
|
501
986
|
return reference;
|
|
502
987
|
};
|
|
@@ -505,11 +990,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
505
990
|
if (reference === undefined)
|
|
506
991
|
return;
|
|
507
992
|
const state = objectReferences.get(reference);
|
|
508
|
-
if (state !== undefined)
|
|
509
|
-
|
|
993
|
+
if (state !== undefined) {
|
|
994
|
+
if (activeStage === undefined) {
|
|
995
|
+
state.active = false;
|
|
996
|
+
state.cell = undefined;
|
|
997
|
+
}
|
|
998
|
+
else
|
|
999
|
+
activeStage.invalidated.add(state);
|
|
1000
|
+
}
|
|
510
1001
|
node.objectReference = undefined;
|
|
511
1002
|
};
|
|
512
|
-
const release = (reference) =>
|
|
1003
|
+
const release = (reference) => coordinatedCleanup(Effect.sync(() => {
|
|
513
1004
|
reference.directory = undefined;
|
|
514
1005
|
reference.closed = true;
|
|
515
1006
|
}));
|
|
@@ -530,13 +1021,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
530
1021
|
};
|
|
531
1022
|
const reclaim = (file) => {
|
|
532
1023
|
if (file.metadata.nlink === 0 && file.openCount === 0) {
|
|
533
|
-
usedBytes -= BigInt(file.data.bytes.length);
|
|
1024
|
+
state.usedBytes -= BigInt(file.data.bytes.length);
|
|
534
1025
|
file.data = Content.empty();
|
|
1026
|
+
state.retainedFiles.delete(file.metadata.ino);
|
|
535
1027
|
invalidateReference(file);
|
|
536
1028
|
}
|
|
537
1029
|
};
|
|
538
1030
|
// Whether the volume's entry quota leaves room for one more name.
|
|
539
|
-
const atEntryLimit = () => settings.maxEntries !== undefined && entries >= settings.maxEntries;
|
|
1031
|
+
const atEntryLimit = () => settings.maxEntries !== undefined && state.entries >= settings.maxEntries;
|
|
540
1032
|
// A new subdirectory's ".." entry is a second link to the parent; other node kinds add none.
|
|
541
1033
|
const attach = (parent, name, node, now) => {
|
|
542
1034
|
parent.entries.set(name, node);
|
|
@@ -558,10 +1050,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
558
1050
|
else {
|
|
559
1051
|
node.metadata = { ...node.metadata, nlink: node.metadata.nlink - 1, ctimeNs: now };
|
|
560
1052
|
advanceRevision(node);
|
|
561
|
-
if (node.kind === "file")
|
|
1053
|
+
if (node.kind === "file") {
|
|
1054
|
+
if (node.metadata.nlink === 0 && node.openCount > 0)
|
|
1055
|
+
state.retainedFiles.set(node.metadata.ino, node);
|
|
562
1056
|
reclaim(node);
|
|
1057
|
+
}
|
|
563
1058
|
else if (node.metadata.nlink === 0) {
|
|
564
|
-
usedBytes -= BigInt(node.target.length);
|
|
1059
|
+
state.usedBytes -= BigInt(node.target.length);
|
|
565
1060
|
invalidateReference(node);
|
|
566
1061
|
}
|
|
567
1062
|
}
|
|
@@ -574,9 +1069,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
574
1069
|
}
|
|
575
1070
|
ref.closed = true;
|
|
576
1071
|
};
|
|
1072
|
+
const finalizeFile = (ref) => Effect.suspend(() => ref.closed
|
|
1073
|
+
? Effect.void
|
|
1074
|
+
: ref.file === undefined
|
|
1075
|
+
? Effect.sync(() => {
|
|
1076
|
+
ref.closed = true;
|
|
1077
|
+
})
|
|
1078
|
+
: coordinated("close", Effect.sync(() => releaseFile(ref)), () => releaseFile(ref)).pipe(Effect.catch(() => coordinatedCleanup(Effect.sync(() => {
|
|
1079
|
+
if (!ref.closed)
|
|
1080
|
+
releaseFile(ref);
|
|
1081
|
+
})))));
|
|
577
1082
|
// Replacing a payload clears setuid and setgid, and charges the volume for the size delta.
|
|
578
|
-
const replaceContent = (file, data, now) => {
|
|
579
|
-
usedBytes += BigInt(data.length - file.data.bytes.length);
|
|
1083
|
+
const replaceContent = (file, data, now, publish = true) => {
|
|
1084
|
+
state.usedBytes += BigInt(data.length - file.data.bytes.length);
|
|
580
1085
|
file.data = Content.make(data);
|
|
581
1086
|
file.metadata = {
|
|
582
1087
|
...file.metadata,
|
|
@@ -586,9 +1091,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
586
1091
|
ctimeNs: now
|
|
587
1092
|
};
|
|
588
1093
|
advanceRevision(file);
|
|
589
|
-
|
|
1094
|
+
if (publish)
|
|
1095
|
+
publishNode(file);
|
|
590
1096
|
};
|
|
591
|
-
const resize = Effect.fnUntraced(function* (file, length, operation) {
|
|
1097
|
+
const resize = Effect.fnUntraced(function* (file, length, operation, publish = true) {
|
|
592
1098
|
if (!Predicate.isBigInt(length) || length < 0n) {
|
|
593
1099
|
return yield* new FsError({ code: "InvalidArgument", operation: operation });
|
|
594
1100
|
}
|
|
@@ -596,19 +1102,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
596
1102
|
return yield* new FsError({ code: "FileTooLarge", operation: operation });
|
|
597
1103
|
const size = Number(length);
|
|
598
1104
|
if (settings.maxBytes !== undefined &&
|
|
599
|
-
BigInt(size - file.data.bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes) {
|
|
1105
|
+
BigInt(size - file.data.bytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes) {
|
|
600
1106
|
return yield* new FsError({ code: "NoSpace", operation: operation });
|
|
601
1107
|
}
|
|
602
1108
|
const data = new Uint8Array(size);
|
|
603
1109
|
data.set(file.data.bytes.subarray(0, size));
|
|
604
|
-
replaceContent(file, data, yield* timestamp(operation));
|
|
1110
|
+
replaceContent(file, data, yield* timestamp(operation), publish);
|
|
605
1111
|
});
|
|
606
1112
|
const fileHandle = (ref) => {
|
|
607
1113
|
const get = (operation, access) => ref.file === undefined || (access === "read" && ref.access === "write") ||
|
|
608
1114
|
(access === "write" && ref.access === "read")
|
|
609
1115
|
? Effect.fail(new FsError({ code: "InvalidHandle", operation: operation }))
|
|
610
1116
|
: Effect.succeed(ref.file);
|
|
611
|
-
const read = Effect.
|
|
1117
|
+
const read = (maximum, position) => coordinated(position === undefined ? "read" : "pread", Effect.gen(function* () {
|
|
612
1118
|
const file = yield* get(position === undefined ? "read" : "pread", "read");
|
|
613
1119
|
if (!isNatural(maximum))
|
|
614
1120
|
return yield* new FsError({ code: "InvalidArgument", operation: "read" });
|
|
@@ -624,12 +1130,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
624
1130
|
if (position === undefined)
|
|
625
1131
|
ref.offset += BigInt(data.length);
|
|
626
1132
|
return data;
|
|
627
|
-
}
|
|
1133
|
+
}));
|
|
628
1134
|
const write = Effect.fnUntraced(function* (input, position) {
|
|
629
1135
|
if (!isAttachedBytes(input))
|
|
630
1136
|
return yield* new FsError({ code: "InvalidArgument", operation: "write" });
|
|
631
1137
|
const bytes = new Uint8Array(input);
|
|
632
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1138
|
+
return yield* coordinated(position === undefined ? "write" : "pwrite", Effect.gen(function* () {
|
|
633
1139
|
const file = yield* get(position === undefined ? "write" : "pwrite", "write");
|
|
634
1140
|
const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
|
|
635
1141
|
if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
|
|
@@ -643,7 +1149,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
643
1149
|
const start = Number(offset);
|
|
644
1150
|
const free = settings.maxBytes === undefined
|
|
645
1151
|
? BigInt(maxFileBytes)
|
|
646
|
-
: ByteSize.toBigInt(settings.maxBytes) - usedBytes;
|
|
1152
|
+
: ByteSize.toBigInt(settings.maxBytes) - state.usedBytes;
|
|
647
1153
|
const maximumEnd = BigInt(file.data.bytes.length) + free;
|
|
648
1154
|
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
649
1155
|
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
@@ -677,12 +1183,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
677
1183
|
return yield* write(bytes, offset);
|
|
678
1184
|
}),
|
|
679
1185
|
seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
|
|
680
|
-
return yield*
|
|
1186
|
+
return yield* coordinatedRead("seek", Effect.uninterruptible(Effect.gen(function* () {
|
|
681
1187
|
const file = yield* get("seek");
|
|
682
1188
|
if (!Predicate.isBigInt(offset) || !isSeekMode(mode)) {
|
|
683
1189
|
return yield* new FsError({ code: "InvalidArgument", operation: "seek" });
|
|
684
1190
|
}
|
|
685
|
-
let next = mode === "current"
|
|
1191
|
+
let next = mode === "current"
|
|
1192
|
+
? ref.offset + offset
|
|
1193
|
+
: mode === "end"
|
|
1194
|
+
? file.metadata.size + offset
|
|
1195
|
+
: offset;
|
|
686
1196
|
if (next < 0n || next > MAX_FILE_OFFSET) {
|
|
687
1197
|
return yield* new FsError({ code: "InvalidArgument", operation: "seek" });
|
|
688
1198
|
}
|
|
@@ -694,21 +1204,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
694
1204
|
}
|
|
695
1205
|
ref.offset = next;
|
|
696
1206
|
return next;
|
|
697
|
-
}));
|
|
1207
|
+
})));
|
|
698
1208
|
}),
|
|
699
1209
|
truncate: Effect.fn("FileHandle.truncate")(function* (length) {
|
|
700
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1210
|
+
return yield* coordinated("truncate", Effect.gen(function* () {
|
|
701
1211
|
yield* resize(yield* get("truncate", "write"), length, "truncate");
|
|
702
1212
|
}));
|
|
703
1213
|
}),
|
|
704
|
-
stat:
|
|
1214
|
+
stat: coordinatedRead("stat", Effect.gen(function* () {
|
|
705
1215
|
return { ...(yield* get("stat")).metadata };
|
|
706
1216
|
})).pipe(Effect.withSpan("FileHandle.stat")),
|
|
707
|
-
sync:
|
|
708
|
-
close: coordinated(Effect.gen(function* () {
|
|
1217
|
+
sync: coordinatedRead("sync", Effect.suspend(() => Effect.asVoid(get("sync")))).pipe(Effect.withSpan("FileHandle.sync")),
|
|
1218
|
+
close: coordinated("close", Effect.gen(function* () {
|
|
709
1219
|
yield* get("close");
|
|
710
1220
|
releaseFile(ref);
|
|
711
|
-
})).pipe(Effect.
|
|
1221
|
+
}), () => releaseFile(ref)).pipe(Effect.catch((error) => coordinatedCleanup(Effect.sync(() => {
|
|
1222
|
+
if (!ref.closed)
|
|
1223
|
+
releaseFile(ref);
|
|
1224
|
+
})).pipe(Effect.andThen(Effect.fail(error)))), Effect.withSpan("FileHandle.close"))
|
|
712
1225
|
});
|
|
713
1226
|
files.set(handle, ref);
|
|
714
1227
|
return handle;
|
|
@@ -725,17 +1238,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
725
1238
|
if (state.volume !== volumeIdentity) {
|
|
726
1239
|
return yield* new FsError({ code: "ForeignReference", operation: operation });
|
|
727
1240
|
}
|
|
728
|
-
if (state.
|
|
1241
|
+
if (!state.active || activeStage?.invalidated.has(state)) {
|
|
729
1242
|
return yield* new FsError({ code: "StaleReference", operation: operation });
|
|
730
|
-
|
|
1243
|
+
}
|
|
1244
|
+
const node = state.cell?.node;
|
|
1245
|
+
if (node === undefined)
|
|
1246
|
+
return yield* new FsError({ code: "StaleReference", operation });
|
|
1247
|
+
return node;
|
|
1248
|
+
});
|
|
1249
|
+
const referencedName = Effect.fnUntraced(function* (input, operation) {
|
|
1250
|
+
if (!isAttachedBytes(input) || input.length === 0 || input.length > MAX_NAME_BYTES || input.includes(0) ||
|
|
1251
|
+
input.includes(SLASH_BYTE))
|
|
1252
|
+
return yield* new FsError({ code: "InvalidArgument", operation });
|
|
1253
|
+
const name = Encoding.encodeHex(new Uint8Array(input));
|
|
1254
|
+
if (isDotComponent(name))
|
|
1255
|
+
return yield* new FsError({ code: "InvalidArgument", operation });
|
|
1256
|
+
return name;
|
|
1257
|
+
});
|
|
1258
|
+
const referencedDirectory = Effect.fnUntraced(function* (target, operation) {
|
|
1259
|
+
const node = yield* referencedNode(target, operation);
|
|
1260
|
+
if (node.kind !== "directory")
|
|
1261
|
+
return yield* new FsError({ code: "NotDirectory", operation });
|
|
1262
|
+
return node;
|
|
1263
|
+
});
|
|
1264
|
+
const creationTimes = (times, now) => ({
|
|
1265
|
+
atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
|
|
1266
|
+
mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
|
|
731
1267
|
});
|
|
732
|
-
const lookup = Effect.fnUntraced(function* (path, base, operation, options = {}) {
|
|
1268
|
+
const lookup = Effect.fnUntraced(function* (path, base, operation, options = {}, referencedBase) {
|
|
733
1269
|
const { followFinalSymlink = true, allowMissing = false, parentOnly = false } = options;
|
|
734
1270
|
if (reference.directory === undefined) {
|
|
735
1271
|
return yield* new FsError({ code: "ClosedCaller", operation: operation, path: path.input });
|
|
736
1272
|
}
|
|
737
|
-
let current = path.absolute ? root : reference.directory;
|
|
738
|
-
if (!path.absolute &&
|
|
1273
|
+
let current = path.absolute ? state.root : reference.directory;
|
|
1274
|
+
if (!path.absolute && referencedBase !== undefined) {
|
|
1275
|
+
current = referencedBase;
|
|
1276
|
+
yield* authorize(current, identity, EXECUTE, operation);
|
|
1277
|
+
}
|
|
1278
|
+
else if (!path.absolute && base !== undefined) {
|
|
739
1279
|
const target = handles.get(base);
|
|
740
1280
|
if (target === undefined) {
|
|
741
1281
|
return yield* new FsError({ code: "InvalidHandle", operation: operation, path: path.input });
|
|
@@ -804,7 +1344,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
804
1344
|
}
|
|
805
1345
|
work = expanded.success;
|
|
806
1346
|
if (work.absolute)
|
|
807
|
-
current = root;
|
|
1347
|
+
current = state.root;
|
|
808
1348
|
index = -1;
|
|
809
1349
|
}
|
|
810
1350
|
else
|
|
@@ -836,11 +1376,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
836
1376
|
const acquireDirectory = Effect.fnUntraced(function* (input, options, operation) {
|
|
837
1377
|
const prepared = preparePath(input, operation, settings.maxPathBytes);
|
|
838
1378
|
const base = options?.relativeTo;
|
|
839
|
-
const acquired =
|
|
1379
|
+
const acquired = makeDirectoryReference();
|
|
840
1380
|
// Register before retaining a directory. Closed scopes can run this immediately,
|
|
841
1381
|
// so registration must not happen while holding the volume permit.
|
|
842
1382
|
yield* Effect.addFinalizer(() => release(acquired));
|
|
843
|
-
return yield*
|
|
1383
|
+
return yield* coordinatedRead(operation, Effect.uninterruptible(Effect.gen(function* () {
|
|
844
1384
|
if (acquired.closed)
|
|
845
1385
|
return yield* Effect.interrupt;
|
|
846
1386
|
const path = yield* Effect.fromResult(prepared);
|
|
@@ -848,12 +1388,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
848
1388
|
yield* authorize(directory, identity, EXECUTE, operation, input);
|
|
849
1389
|
acquired.directory = directory;
|
|
850
1390
|
return acquired;
|
|
851
|
-
}));
|
|
1391
|
+
})));
|
|
852
1392
|
});
|
|
853
1393
|
const list = Effect.fnUntraced(function* (input, options) {
|
|
854
1394
|
const prepared = preparePath(input, "readDirectory", settings.maxPathBytes);
|
|
855
1395
|
const base = options?.relativeTo;
|
|
856
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1396
|
+
return yield* coordinated("readDirectory", Effect.gen(function* () {
|
|
857
1397
|
const directory = yield* locate(yield* Effect.fromResult(prepared), base, "readDirectory");
|
|
858
1398
|
yield* authorize(directory, identity, READ, "readDirectory", input);
|
|
859
1399
|
const result = [...directory.entries.keys()].map(nameBytes);
|
|
@@ -864,7 +1404,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
864
1404
|
const readTarget = Effect.fnUntraced(function* (input, options) {
|
|
865
1405
|
const prepared = preparePath(input, "readLink", settings.maxPathBytes);
|
|
866
1406
|
const base = options?.relativeTo;
|
|
867
|
-
return yield*
|
|
1407
|
+
return yield* coordinatedRead("readLink", Effect.gen(function* () {
|
|
868
1408
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readLink", {
|
|
869
1409
|
followFinalSymlink: false
|
|
870
1410
|
});
|
|
@@ -877,7 +1417,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
877
1417
|
const canonical = Effect.fnUntraced(function* (input, options) {
|
|
878
1418
|
const prepared = preparePath(input, "realPath", settings.maxPathBytes);
|
|
879
1419
|
const base = options?.relativeTo;
|
|
880
|
-
return yield*
|
|
1420
|
+
return yield* coordinatedRead("realPath", Effect.gen(function* () {
|
|
881
1421
|
const result = yield* lookup(yield* Effect.fromResult(prepared), base, "realPath");
|
|
882
1422
|
const components = [];
|
|
883
1423
|
if (result.node?.kind !== "directory" && result.name !== undefined)
|
|
@@ -886,8 +1426,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
886
1426
|
while (directory !== undefined && directory.parent !== undefined) {
|
|
887
1427
|
const parent = directory.parent;
|
|
888
1428
|
const entry = [...parent.entries].find(([, child]) => child === directory);
|
|
889
|
-
if (entry === undefined)
|
|
1429
|
+
if (entry === undefined) {
|
|
890
1430
|
return yield* new FsError({ code: "NotFound", operation: "realPath", path: input });
|
|
1431
|
+
}
|
|
891
1432
|
components.push(entry[0]);
|
|
892
1433
|
directory = parent;
|
|
893
1434
|
}
|
|
@@ -928,7 +1469,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
928
1469
|
if (!isMode(mode))
|
|
929
1470
|
return yield* new FsError({ code: "InvalidArgument", operation: "chmod" });
|
|
930
1471
|
const chosen = options === undefined ? undefined : { ...options };
|
|
931
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1472
|
+
return yield* coordinated("chmod", Effect.gen(function* () {
|
|
932
1473
|
const node = yield* metadataNode(target, chosen, "chmod");
|
|
933
1474
|
const permitted = yield* permittedMode(node.metadata, mode, "chmod");
|
|
934
1475
|
node.metadata = {
|
|
@@ -946,7 +1487,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
946
1487
|
return yield* new FsError({ code: "InvalidArgument", operation: "chown" });
|
|
947
1488
|
const update = { ...decoded.success };
|
|
948
1489
|
const chosen = options === undefined ? undefined : { ...options };
|
|
949
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1490
|
+
return yield* coordinated("chown", Effect.gen(function* () {
|
|
950
1491
|
const node = yield* metadataNode(target, chosen, "chown");
|
|
951
1492
|
if (!identity.privileged && (identity.uid !== node.metadata.uid ||
|
|
952
1493
|
(update.uid !== undefined && update.uid !== node.metadata.uid) ||
|
|
@@ -973,7 +1514,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
973
1514
|
const access = { ...decoded.success.access };
|
|
974
1515
|
const modification = { ...decoded.success.modification };
|
|
975
1516
|
const chosen = options === undefined ? undefined : { ...options };
|
|
976
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1517
|
+
return yield* coordinated("utimes", Effect.gen(function* () {
|
|
977
1518
|
const node = yield* metadataNode(target, chosen, "utimes");
|
|
978
1519
|
if (access.kind === "omit" && modification.kind === "omit")
|
|
979
1520
|
return;
|
|
@@ -1009,15 +1550,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1009
1550
|
});
|
|
1010
1551
|
const authorizeRemoval = (parent, child, operation, input) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
|
|
1011
1552
|
identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
|
|
1012
|
-
? Effect.fail(
|
|
1553
|
+
? Effect.fail(input === undefined
|
|
1554
|
+
? new FsError({ code: "AccessDenied", operation })
|
|
1555
|
+
: new FsError({ code: "AccessDenied", operation, path: input }))
|
|
1013
1556
|
: Effect.void;
|
|
1014
1557
|
return Object.freeze({
|
|
1015
1558
|
[CallerId]: true,
|
|
1016
|
-
rootReference:
|
|
1559
|
+
rootReference: coordinatedRead("rootReference", Effect.gen(function* () {
|
|
1017
1560
|
if (reference.directory === undefined) {
|
|
1018
1561
|
return yield* new FsError({ code: "ClosedCaller", operation: "rootReference" });
|
|
1019
1562
|
}
|
|
1020
|
-
return referenceFor(root);
|
|
1563
|
+
return referenceFor(state.root);
|
|
1021
1564
|
})).pipe(Effect.withSpan("Caller.rootReference")),
|
|
1022
1565
|
lookupReference: Effect.fn("Caller.lookupReference")(function* (directoryReference, name) {
|
|
1023
1566
|
if (!isAttachedBytes(name) || name.length === 0 || name.length > MAX_NAME_BYTES || name.includes(0) ||
|
|
@@ -1026,7 +1569,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1026
1569
|
const key = Encoding.encodeHex(new Uint8Array(name));
|
|
1027
1570
|
if (isDotComponent(key))
|
|
1028
1571
|
return yield* new FsError({ code: "InvalidArgument", operation: "lookupReference" });
|
|
1029
|
-
return yield*
|
|
1572
|
+
return yield* coordinatedRead("lookupReference", Effect.gen(function* () {
|
|
1030
1573
|
const directory = yield* referencedNode(directoryReference, "lookupReference");
|
|
1031
1574
|
if (directory.kind !== "directory") {
|
|
1032
1575
|
return yield* new FsError({ code: "NotDirectory", operation: "lookupReference" });
|
|
@@ -1039,7 +1582,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1039
1582
|
}));
|
|
1040
1583
|
}),
|
|
1041
1584
|
parentReference: Effect.fn("Caller.parentReference")(function* (directoryReference) {
|
|
1042
|
-
return yield*
|
|
1585
|
+
return yield* coordinatedRead("parentReference", Effect.gen(function* () {
|
|
1043
1586
|
const directory = yield* referencedNode(directoryReference, "parentReference");
|
|
1044
1587
|
if (directory.kind !== "directory") {
|
|
1045
1588
|
return yield* new FsError({ code: "NotDirectory", operation: "parentReference" });
|
|
@@ -1049,13 +1592,25 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1049
1592
|
}));
|
|
1050
1593
|
}),
|
|
1051
1594
|
observeMetadata: Effect.fn("Caller.observeMetadata")(function* (objectReference) {
|
|
1052
|
-
return yield*
|
|
1595
|
+
return yield* coordinatedRead("observeMetadata", Effect.gen(function* () {
|
|
1053
1596
|
const node = yield* referencedNode(objectReference, "observeMetadata");
|
|
1054
1597
|
return Object.freeze({ value: Object.freeze({ ...node.metadata }), revision: node.revision });
|
|
1055
1598
|
}));
|
|
1056
1599
|
}),
|
|
1600
|
+
accessReference: Effect.fn("Caller.accessReference")(function* (objectReference, bits = 0) {
|
|
1601
|
+
if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
|
|
1602
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "accessReference" });
|
|
1603
|
+
}
|
|
1604
|
+
return yield* coordinatedRead("accessReference", Effect.gen(function* () {
|
|
1605
|
+
const node = yield* referencedNode(objectReference, "accessReference");
|
|
1606
|
+
if (node.kind === "file" && (bits & EXECUTE) !== 0 && (node.metadata.mode & ANY_EXECUTE) === 0) {
|
|
1607
|
+
return yield* new FsError({ code: "AccessDenied", operation: "accessReference" });
|
|
1608
|
+
}
|
|
1609
|
+
yield* authorize(node, identity, bits, "accessReference");
|
|
1610
|
+
}));
|
|
1611
|
+
}),
|
|
1057
1612
|
observeDirectory: Effect.fn("Caller.observeDirectory")(function* (directoryReference) {
|
|
1058
|
-
return yield*
|
|
1613
|
+
return yield* coordinatedRead("observeDirectory", Effect.gen(function* () {
|
|
1059
1614
|
const directory = yield* referencedNode(directoryReference, "observeDirectory");
|
|
1060
1615
|
if (directory.kind !== "directory") {
|
|
1061
1616
|
return yield* new FsError({ code: "NotDirectory", operation: "observeDirectory" });
|
|
@@ -1066,7 +1621,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1066
1621
|
}));
|
|
1067
1622
|
}),
|
|
1068
1623
|
readLinkReference: Effect.fn("Caller.readLinkReference")(function* (objectReference) {
|
|
1069
|
-
return yield*
|
|
1624
|
+
return yield* coordinatedRead("readLinkReference", Effect.gen(function* () {
|
|
1070
1625
|
const node = yield* referencedNode(objectReference, "readLinkReference");
|
|
1071
1626
|
if (node.kind !== "symlink") {
|
|
1072
1627
|
return yield* new FsError({ code: "InvalidArgument", operation: "readLinkReference" });
|
|
@@ -1074,17 +1629,394 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1074
1629
|
return new Uint8Array(node.target);
|
|
1075
1630
|
}));
|
|
1076
1631
|
}),
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1632
|
+
mkdirReference: Effect.fn("Caller.mkdirReference")(function* (directoryReference, input, raw = {}) {
|
|
1633
|
+
const name = yield* referencedName(input, "mkdirReference");
|
|
1634
|
+
const decoded = decodeMkdirReferenceSettings(raw);
|
|
1635
|
+
if (Result.isFailure(decoded)) {
|
|
1636
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "mkdirReference" });
|
|
1637
|
+
}
|
|
1638
|
+
const chosen = { ...decoded.success };
|
|
1639
|
+
if (chosen.exactMode && chosen.mode === undefined) {
|
|
1640
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "mkdirReference" });
|
|
1641
|
+
}
|
|
1642
|
+
const mode = chosen.mode ?? 0o777;
|
|
1643
|
+
return yield* coordinated("mkdirReference", Effect.gen(function* () {
|
|
1644
|
+
const parent = yield* referencedDirectory(directoryReference, "mkdirReference");
|
|
1645
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "mkdirReference");
|
|
1646
|
+
if (parent.entries.has(name)) {
|
|
1647
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "mkdirReference" });
|
|
1648
|
+
}
|
|
1649
|
+
if (atEntryLimit())
|
|
1650
|
+
return yield* new FsError({ code: "NoSpace", operation: "mkdirReference" });
|
|
1651
|
+
const before = parent.revision;
|
|
1652
|
+
const now = yield* timestamp("mkdirReference");
|
|
1653
|
+
const initial = creationTimes(chosen.times, now);
|
|
1654
|
+
const creationMode = chosen.exactMode
|
|
1655
|
+
? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, mode, "mkdirReference")
|
|
1656
|
+
: (mode & 0o777 & ~umask) | (mode & STICKY_BIT);
|
|
1657
|
+
const child = {
|
|
1658
|
+
kind: "directory",
|
|
1659
|
+
lineage: undefined,
|
|
1660
|
+
parent,
|
|
1661
|
+
entries: new Map(),
|
|
1662
|
+
metadata: {
|
|
1663
|
+
...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, creationMode, now),
|
|
1664
|
+
...initial
|
|
1665
|
+
},
|
|
1666
|
+
revision: nextRevision(),
|
|
1667
|
+
objectReference: undefined
|
|
1668
|
+
};
|
|
1669
|
+
attach(parent, name, child, now);
|
|
1670
|
+
state.nextInode += 1n;
|
|
1671
|
+
state.entries += 1;
|
|
1672
|
+
publishEntry("Create", parent, name);
|
|
1673
|
+
return { reference: referenceFor(child), directory: { before, after: parent.revision } };
|
|
1674
|
+
}));
|
|
1675
|
+
}),
|
|
1676
|
+
symlinkReference: Effect.fn("Caller.symlinkReference")(function* (target, directoryReference, input, raw = {}) {
|
|
1677
|
+
const name = yield* referencedName(input, "symlinkReference");
|
|
1678
|
+
const decoded = decodeSymlinkReferenceSettings(raw);
|
|
1679
|
+
if (Result.isFailure(decoded)) {
|
|
1680
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "symlinkReference" });
|
|
1681
|
+
}
|
|
1682
|
+
const rawTarget = inputBytes(target);
|
|
1683
|
+
if (Result.isFailure(rawTarget)) {
|
|
1684
|
+
return yield* new FsError({ code: rawTarget.failure, operation: "symlinkReference" });
|
|
1685
|
+
}
|
|
1686
|
+
if (rawTarget.success.includes(0)) {
|
|
1687
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "symlinkReference" });
|
|
1688
|
+
}
|
|
1689
|
+
const targetBytes = new Uint8Array(rawTarget.success);
|
|
1690
|
+
const chosen = { ...decoded.success };
|
|
1691
|
+
return yield* coordinated("symlinkReference", Effect.gen(function* () {
|
|
1692
|
+
const parent = yield* referencedDirectory(directoryReference, "symlinkReference");
|
|
1693
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "symlinkReference");
|
|
1694
|
+
if (parent.entries.has(name)) {
|
|
1695
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "symlinkReference" });
|
|
1696
|
+
}
|
|
1697
|
+
if (atEntryLimit() ||
|
|
1698
|
+
(settings.maxBytes !== undefined &&
|
|
1699
|
+
BigInt(targetBytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes))
|
|
1700
|
+
return yield* new FsError({ code: "NoSpace", operation: "symlinkReference" });
|
|
1701
|
+
const before = parent.revision;
|
|
1702
|
+
const now = yield* timestamp("symlinkReference");
|
|
1703
|
+
const initial = creationTimes(chosen.times, now);
|
|
1704
|
+
const node = {
|
|
1705
|
+
kind: "symlink",
|
|
1706
|
+
lineage: undefined,
|
|
1707
|
+
target: targetBytes,
|
|
1708
|
+
metadata: {
|
|
1709
|
+
...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, 0o777, now),
|
|
1710
|
+
...initial,
|
|
1711
|
+
kind: "symlink",
|
|
1712
|
+
nlink: 1,
|
|
1713
|
+
size: BigInt(targetBytes.length)
|
|
1714
|
+
},
|
|
1715
|
+
revision: nextRevision(),
|
|
1716
|
+
objectReference: undefined
|
|
1717
|
+
};
|
|
1718
|
+
attach(parent, name, node, now);
|
|
1719
|
+
state.nextInode += 1n;
|
|
1720
|
+
state.entries += 1;
|
|
1721
|
+
state.usedBytes += BigInt(targetBytes.length);
|
|
1722
|
+
publishEntry("Create", parent, name);
|
|
1723
|
+
return { reference: referenceFor(node), directory: { before, after: parent.revision } };
|
|
1724
|
+
}));
|
|
1725
|
+
}),
|
|
1726
|
+
linkReference: Effect.fn("Caller.linkReference")(function* (sourceReference, directoryReference, input) {
|
|
1727
|
+
const name = yield* referencedName(input, "linkReference");
|
|
1728
|
+
return yield* coordinated("linkReference", Effect.gen(function* () {
|
|
1729
|
+
const node = yield* referencedNode(sourceReference, "linkReference");
|
|
1730
|
+
if (node.kind === "directory") {
|
|
1731
|
+
return yield* new FsError({ code: "IsDirectory", operation: "linkReference" });
|
|
1732
|
+
}
|
|
1733
|
+
if (node.metadata.nlink === 0) {
|
|
1734
|
+
return yield* new FsError({ code: "StaleReference", operation: "linkReference" });
|
|
1735
|
+
}
|
|
1736
|
+
const parent = yield* referencedDirectory(directoryReference, "linkReference");
|
|
1737
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "linkReference");
|
|
1738
|
+
if (parent.entries.has(name)) {
|
|
1739
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "linkReference" });
|
|
1740
|
+
}
|
|
1741
|
+
if (atEntryLimit())
|
|
1742
|
+
return yield* new FsError({ code: "NoSpace", operation: "linkReference" });
|
|
1743
|
+
const before = parent.revision;
|
|
1744
|
+
const now = yield* timestamp("linkReference");
|
|
1745
|
+
attach(parent, name, node, now);
|
|
1746
|
+
node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1, ctimeNs: now };
|
|
1747
|
+
advanceRevision(node);
|
|
1748
|
+
state.entries += 1;
|
|
1749
|
+
publishEntry("Create", parent, name);
|
|
1750
|
+
return { reference: referenceFor(node), directory: { before, after: parent.revision } };
|
|
1751
|
+
}));
|
|
1752
|
+
}),
|
|
1753
|
+
unlinkReference: Effect.fn("Caller.unlinkReference")(function* (directoryReference, input) {
|
|
1754
|
+
const name = yield* referencedName(input, "unlinkReference");
|
|
1755
|
+
return yield* coordinated("unlinkReference", Effect.gen(function* () {
|
|
1756
|
+
const parent = yield* referencedDirectory(directoryReference, "unlinkReference");
|
|
1757
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "unlinkReference");
|
|
1758
|
+
const child = parent.entries.get(name);
|
|
1759
|
+
if (child === undefined)
|
|
1760
|
+
return yield* new FsError({ code: "NotFound", operation: "unlinkReference" });
|
|
1761
|
+
if (child.kind === "directory") {
|
|
1762
|
+
return yield* new FsError({ code: "IsDirectory", operation: "unlinkReference" });
|
|
1763
|
+
}
|
|
1764
|
+
yield* authorizeRemoval(parent, child, "unlinkReference");
|
|
1765
|
+
const before = parent.revision;
|
|
1766
|
+
const now = yield* timestamp("unlinkReference");
|
|
1767
|
+
parent.entries.delete(name);
|
|
1768
|
+
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1769
|
+
advanceRevision(parent);
|
|
1770
|
+
detach(child, now);
|
|
1771
|
+
state.entries -= 1;
|
|
1772
|
+
publishEntry("Remove", parent, name);
|
|
1773
|
+
return { before, after: parent.revision };
|
|
1774
|
+
}));
|
|
1775
|
+
}),
|
|
1776
|
+
rmdirReference: Effect.fn("Caller.rmdirReference")(function* (directoryReference, input) {
|
|
1777
|
+
const name = yield* referencedName(input, "rmdirReference");
|
|
1778
|
+
return yield* coordinated("rmdirReference", Effect.gen(function* () {
|
|
1779
|
+
const parent = yield* referencedDirectory(directoryReference, "rmdirReference");
|
|
1780
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "rmdirReference");
|
|
1781
|
+
const child = parent.entries.get(name);
|
|
1782
|
+
if (child === undefined)
|
|
1783
|
+
return yield* new FsError({ code: "NotFound", operation: "rmdirReference" });
|
|
1784
|
+
yield* authorizeRemoval(parent, child, "rmdirReference");
|
|
1785
|
+
if (child.kind !== "directory") {
|
|
1786
|
+
return yield* new FsError({ code: "NotDirectory", operation: "rmdirReference" });
|
|
1787
|
+
}
|
|
1788
|
+
if (child.entries.size > 0) {
|
|
1789
|
+
return yield* new FsError({ code: "NotEmpty", operation: "rmdirReference" });
|
|
1790
|
+
}
|
|
1791
|
+
const before = parent.revision;
|
|
1792
|
+
const now = yield* timestamp("rmdirReference");
|
|
1793
|
+
parent.entries.delete(name);
|
|
1794
|
+
parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink - 1, mtimeNs: now, ctimeNs: now };
|
|
1795
|
+
child.parent = undefined;
|
|
1796
|
+
child.metadata = { ...child.metadata, nlink: 0, ctimeNs: now };
|
|
1797
|
+
advanceRevision(parent);
|
|
1798
|
+
advanceRevision(child);
|
|
1799
|
+
invalidateReference(child);
|
|
1800
|
+
state.entries -= 1;
|
|
1801
|
+
publishEntry("Remove", parent, name);
|
|
1802
|
+
return { before, after: parent.revision };
|
|
1803
|
+
}));
|
|
1804
|
+
}),
|
|
1805
|
+
removeReference: Effect.fn("Caller.removeReference")(function* (directoryReference, input) {
|
|
1806
|
+
const name = yield* referencedName(input, "removeReference");
|
|
1807
|
+
return yield* coordinated("removeReference", Effect.gen(function* () {
|
|
1808
|
+
const parent = yield* referencedDirectory(directoryReference, "removeReference");
|
|
1809
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "removeReference");
|
|
1810
|
+
const child = parent.entries.get(name);
|
|
1811
|
+
if (child === undefined)
|
|
1812
|
+
return yield* new FsError({ code: "NotFound", operation: "removeReference" });
|
|
1813
|
+
yield* authorizeRemoval(parent, child, "removeReference");
|
|
1814
|
+
if (child.kind === "directory" && child.entries.size > 0) {
|
|
1815
|
+
return yield* new FsError({ code: "NotEmpty", operation: "removeReference" });
|
|
1816
|
+
}
|
|
1817
|
+
const before = parent.revision;
|
|
1818
|
+
const now = yield* timestamp("removeReference");
|
|
1819
|
+
parent.entries.delete(name);
|
|
1820
|
+
parent.metadata = {
|
|
1821
|
+
...parent.metadata,
|
|
1822
|
+
nlink: parent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
|
|
1823
|
+
mtimeNs: now,
|
|
1824
|
+
ctimeNs: now
|
|
1825
|
+
};
|
|
1826
|
+
if (child.kind === "directory") {
|
|
1827
|
+
child.parent = undefined;
|
|
1828
|
+
child.metadata = { ...child.metadata, nlink: 0, ctimeNs: now };
|
|
1829
|
+
advanceRevision(parent);
|
|
1830
|
+
advanceRevision(child);
|
|
1831
|
+
invalidateReference(child);
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
advanceRevision(parent);
|
|
1835
|
+
detach(child, now);
|
|
1836
|
+
}
|
|
1837
|
+
state.entries -= 1;
|
|
1838
|
+
publishEntry("Remove", parent, name);
|
|
1839
|
+
return { before, after: parent.revision };
|
|
1840
|
+
}));
|
|
1841
|
+
}),
|
|
1842
|
+
renameReference: Effect.fn("Caller.renameReference")(function* (sourceDirectoryReference, sourceInput, destinationDirectoryReference, destinationInput) {
|
|
1843
|
+
const sourceName = yield* referencedName(sourceInput, "renameReference");
|
|
1844
|
+
const destinationName = yield* referencedName(destinationInput, "renameReference");
|
|
1845
|
+
return yield* coordinated("renameReference", Effect.gen(function* () {
|
|
1846
|
+
const sourceDirectory = yield* referencedDirectory(sourceDirectoryReference, "renameReference");
|
|
1847
|
+
const destinationDirectory = yield* referencedDirectory(destinationDirectoryReference, "renameReference");
|
|
1848
|
+
yield* authorize(sourceDirectory, identity, WRITE | EXECUTE, "renameReference");
|
|
1849
|
+
yield* authorize(destinationDirectory, identity, WRITE | EXECUTE, "renameReference");
|
|
1850
|
+
const sourceBefore = sourceDirectory.revision;
|
|
1851
|
+
const destinationBefore = destinationDirectory.revision;
|
|
1852
|
+
const child = sourceDirectory.entries.get(sourceName);
|
|
1853
|
+
if (child === undefined) {
|
|
1854
|
+
return yield* new FsError({ code: "NotFound", operation: "renameReference" });
|
|
1855
|
+
}
|
|
1856
|
+
const replaced = destinationDirectory.entries.get(destinationName);
|
|
1857
|
+
if (child === replaced) {
|
|
1858
|
+
return sourceDirectory === destinationDirectory
|
|
1859
|
+
? { _tag: "SameDirectory", directory: { before: sourceBefore, after: sourceBefore } }
|
|
1860
|
+
: {
|
|
1861
|
+
_tag: "DifferentDirectories",
|
|
1862
|
+
sourceDirectory: { before: sourceBefore, after: sourceBefore },
|
|
1863
|
+
destinationDirectory: { before: destinationBefore, after: destinationBefore }
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
yield* authorizeRemoval(sourceDirectory, child, "renameReference");
|
|
1867
|
+
if (replaced !== undefined) {
|
|
1868
|
+
yield* authorizeRemoval(destinationDirectory, replaced, "renameReference");
|
|
1869
|
+
if (child.kind === "directory" && replaced.kind !== "directory") {
|
|
1870
|
+
return yield* new FsError({ code: "NotDirectory", operation: "renameReference" });
|
|
1871
|
+
}
|
|
1872
|
+
if (child.kind !== "directory" && replaced.kind === "directory") {
|
|
1873
|
+
return yield* new FsError({ code: "IsDirectory", operation: "renameReference" });
|
|
1874
|
+
}
|
|
1875
|
+
if (replaced.kind === "directory" && replaced.entries.size > 0) {
|
|
1876
|
+
return yield* new FsError({ code: "NotEmpty", operation: "renameReference" });
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
for (let ancestor = destinationDirectory; ancestor !== undefined; ancestor = ancestor.parent) {
|
|
1880
|
+
if (ancestor === child) {
|
|
1881
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "renameReference" });
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const now = yield* timestamp("renameReference");
|
|
1885
|
+
const oldEvent = () => ownedPath(nameBytes(directoryHex(sourceDirectory) + (sourceDirectory === state.root ? "" : SLASH_HEX) + sourceName));
|
|
1886
|
+
sourceDirectory.entries.delete(sourceName);
|
|
1887
|
+
destinationDirectory.entries.set(destinationName, child);
|
|
1888
|
+
if (child.kind === "directory")
|
|
1889
|
+
child.parent = destinationDirectory;
|
|
1890
|
+
sourceDirectory.metadata = {
|
|
1891
|
+
...sourceDirectory.metadata,
|
|
1892
|
+
nlink: sourceDirectory.metadata.nlink - (child.kind === "directory" ? 1 : 0),
|
|
1893
|
+
mtimeNs: now,
|
|
1894
|
+
ctimeNs: now
|
|
1895
|
+
};
|
|
1896
|
+
destinationDirectory.metadata = {
|
|
1897
|
+
...destinationDirectory.metadata,
|
|
1898
|
+
nlink: destinationDirectory.metadata.nlink +
|
|
1899
|
+
(child.kind === "directory" && replaced === undefined ? 1 : 0),
|
|
1900
|
+
mtimeNs: now,
|
|
1901
|
+
ctimeNs: now
|
|
1902
|
+
};
|
|
1903
|
+
child.metadata = { ...child.metadata, ctimeNs: now };
|
|
1904
|
+
advanceRevision(sourceDirectory);
|
|
1905
|
+
if (destinationDirectory !== sourceDirectory)
|
|
1906
|
+
advanceRevision(destinationDirectory);
|
|
1907
|
+
advanceRevision(child);
|
|
1908
|
+
if (replaced !== undefined) {
|
|
1909
|
+
detach(replaced, now);
|
|
1910
|
+
state.entries -= 1;
|
|
1911
|
+
}
|
|
1912
|
+
queuePublish(() => watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() })));
|
|
1913
|
+
publishEntry("Create", destinationDirectory, destinationName);
|
|
1914
|
+
return sourceDirectory === destinationDirectory
|
|
1915
|
+
? {
|
|
1916
|
+
_tag: "SameDirectory",
|
|
1917
|
+
directory: { before: sourceBefore, after: sourceDirectory.revision }
|
|
1918
|
+
}
|
|
1919
|
+
: {
|
|
1920
|
+
_tag: "DifferentDirectories",
|
|
1921
|
+
sourceDirectory: { before: sourceBefore, after: sourceDirectory.revision },
|
|
1922
|
+
destinationDirectory: { before: destinationBefore, after: destinationDirectory.revision }
|
|
1923
|
+
};
|
|
1924
|
+
}));
|
|
1925
|
+
}),
|
|
1926
|
+
chmodReference: Effect.fn("Caller.chmodReference")(function* (objectReference, mode) {
|
|
1927
|
+
if (!isMode(mode))
|
|
1928
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "chmodReference" });
|
|
1929
|
+
return yield* coordinated("chmodReference", Effect.gen(function* () {
|
|
1930
|
+
const node = yield* referencedNode(objectReference, "chmodReference");
|
|
1931
|
+
const permitted = yield* permittedMode(node.metadata, mode, "chmodReference");
|
|
1932
|
+
node.metadata = { ...node.metadata, mode: permitted, ctimeNs: (yield* timestamp("chmodReference")) };
|
|
1933
|
+
advanceRevision(node);
|
|
1934
|
+
publishNode(node);
|
|
1935
|
+
}));
|
|
1936
|
+
}),
|
|
1937
|
+
chownReference: Effect.fn("Caller.chownReference")(function* (objectReference, owner) {
|
|
1938
|
+
const decoded = decodeOwnerUpdate(owner);
|
|
1939
|
+
if (Result.isFailure(decoded)) {
|
|
1940
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "chownReference" });
|
|
1941
|
+
}
|
|
1942
|
+
const update = { ...decoded.success };
|
|
1943
|
+
return yield* coordinated("chownReference", Effect.gen(function* () {
|
|
1944
|
+
const node = yield* referencedNode(objectReference, "chownReference");
|
|
1945
|
+
if (!identity.privileged && (identity.uid !== node.metadata.uid ||
|
|
1946
|
+
(update.uid !== undefined && update.uid !== node.metadata.uid) ||
|
|
1947
|
+
(update.gid !== undefined && update.gid !== identity.gid && !identity.groups.includes(update.gid))))
|
|
1948
|
+
return yield* new FsError({ code: "AccessDenied", operation: "chownReference" });
|
|
1949
|
+
if (update.uid === undefined && update.gid === undefined)
|
|
1950
|
+
return;
|
|
1951
|
+
node.metadata = {
|
|
1952
|
+
...node.metadata,
|
|
1953
|
+
uid: update.uid ?? node.metadata.uid,
|
|
1954
|
+
gid: update.gid ?? node.metadata.gid,
|
|
1955
|
+
mode: node.kind === "file" ? node.metadata.mode & ~SET_ID_BITS : node.metadata.mode,
|
|
1956
|
+
ctimeNs: (yield* timestamp("chownReference"))
|
|
1957
|
+
};
|
|
1958
|
+
advanceRevision(node);
|
|
1959
|
+
publishNode(node);
|
|
1960
|
+
}));
|
|
1961
|
+
}),
|
|
1962
|
+
utimesReference: Effect.fn("Caller.utimesReference")(function* (objectReference, times) {
|
|
1963
|
+
const decoded = decodeTimes(times);
|
|
1964
|
+
if (Result.isFailure(decoded)) {
|
|
1965
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "utimesReference" });
|
|
1966
|
+
}
|
|
1967
|
+
const access = { ...decoded.success.access };
|
|
1968
|
+
const modification = { ...decoded.success.modification };
|
|
1969
|
+
return yield* coordinated("utimesReference", Effect.gen(function* () {
|
|
1970
|
+
const node = yield* referencedNode(objectReference, "utimesReference");
|
|
1971
|
+
if (access.kind === "omit" && modification.kind === "omit")
|
|
1972
|
+
return;
|
|
1973
|
+
if (!identity.privileged && identity.uid !== node.metadata.uid) {
|
|
1974
|
+
if (access.kind !== "now" || modification.kind !== "now") {
|
|
1975
|
+
return yield* new FsError({ code: "AccessDenied", operation: "utimesReference" });
|
|
1976
|
+
}
|
|
1977
|
+
yield* authorize(node, identity, WRITE, "utimesReference");
|
|
1978
|
+
}
|
|
1979
|
+
const now = yield* timestamp("utimesReference");
|
|
1980
|
+
node.metadata = {
|
|
1981
|
+
...node.metadata,
|
|
1982
|
+
atimeNs: access.kind === "omit"
|
|
1983
|
+
? node.metadata.atimeNs
|
|
1984
|
+
: access.kind === "now"
|
|
1985
|
+
? now
|
|
1986
|
+
: access.nanoseconds,
|
|
1987
|
+
mtimeNs: modification.kind === "omit"
|
|
1988
|
+
? node.metadata.mtimeNs
|
|
1989
|
+
: modification.kind === "now"
|
|
1990
|
+
? now
|
|
1991
|
+
: modification.nanoseconds,
|
|
1992
|
+
ctimeNs: now
|
|
1993
|
+
};
|
|
1994
|
+
advanceRevision(node);
|
|
1995
|
+
publishNode(node);
|
|
1996
|
+
}));
|
|
1997
|
+
}),
|
|
1998
|
+
truncateReference: Effect.fn("Caller.truncateReference")(function* (objectReference, length) {
|
|
1999
|
+
return yield* coordinated("truncateReference", Effect.gen(function* () {
|
|
2000
|
+
const node = yield* referencedNode(objectReference, "truncateReference");
|
|
2001
|
+
if (node.kind !== "file") {
|
|
2002
|
+
return yield* new FsError({ code: "IsDirectory", operation: "truncateReference" });
|
|
2003
|
+
}
|
|
2004
|
+
yield* authorize(node, identity, WRITE, "truncateReference");
|
|
2005
|
+
yield* resize(node, length, "truncateReference");
|
|
2006
|
+
}));
|
|
2007
|
+
}),
|
|
2008
|
+
openReference: Effect.fn("Caller.openReference")(function* (objectReference, raw = { access: "read" }) {
|
|
2009
|
+
const decoded = decodeOpenReferenceSettings(raw);
|
|
2010
|
+
if (Result.isFailure(decoded)) {
|
|
2011
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "openReference" });
|
|
2012
|
+
}
|
|
2013
|
+
const chosen = { ...decoded.success };
|
|
2014
|
+
if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
|
|
2015
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "openReference" });
|
|
2016
|
+
}
|
|
2017
|
+
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
2018
|
+
yield* Effect.addFinalizer(() => finalizeFile(acquired));
|
|
2019
|
+
return yield* coordinated("openReference", Effect.gen(function* () {
|
|
1088
2020
|
if (acquired.closed)
|
|
1089
2021
|
return yield* Effect.interrupt;
|
|
1090
2022
|
const node = yield* referencedNode(objectReference, "openReference");
|
|
@@ -1093,16 +2025,170 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1093
2025
|
if (node.metadata.nlink === 0) {
|
|
1094
2026
|
return yield* new FsError({ code: "StaleReference", operation: "openReference" });
|
|
1095
2027
|
}
|
|
1096
|
-
yield* authorize(node, identity, READ
|
|
2028
|
+
yield* authorize(node, identity, chosen.access === "read" ? READ : chosen.access === "write" ? WRITE : READ | WRITE, "openReference");
|
|
2029
|
+
if (chosen.truncate)
|
|
2030
|
+
yield* resize(node, 0n, "openReference");
|
|
1097
2031
|
node.openCount += 1;
|
|
1098
2032
|
acquired.file = node;
|
|
1099
2033
|
return fileHandle(acquired);
|
|
1100
2034
|
}));
|
|
1101
2035
|
}),
|
|
2036
|
+
openChildReference: Effect.fn("Caller.openChildReference")(function* (directoryReference, input, raw, expected) {
|
|
2037
|
+
const name = yield* referencedName(input, "openChildReference");
|
|
2038
|
+
const decoded = decodeOpenChildReferenceSettings(raw);
|
|
2039
|
+
if (Result.isFailure(decoded)) {
|
|
2040
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
|
|
2041
|
+
}
|
|
2042
|
+
const chosen = { ...decoded.success };
|
|
2043
|
+
if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
|
|
2044
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
|
|
2045
|
+
}
|
|
2046
|
+
if ((chosen.mode !== undefined || chosen.times !== undefined || chosen.initialSize !== undefined ||
|
|
2047
|
+
chosen.exactMode !== undefined || chosen.owner !== undefined) &&
|
|
2048
|
+
(chosen.create === undefined || chosen.create === "never")) {
|
|
2049
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
|
|
2050
|
+
}
|
|
2051
|
+
if (chosen.exactMode && chosen.mode === undefined) {
|
|
2052
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
|
|
2053
|
+
}
|
|
2054
|
+
const relativePath = preparePath(ownedPath(nameBytes(name)), "openChildReference", undefined);
|
|
2055
|
+
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
2056
|
+
yield* Effect.addFinalizer(() => finalizeFile(acquired));
|
|
2057
|
+
return yield* coordinated("openChildReference", Effect.gen(function* () {
|
|
2058
|
+
if (acquired.closed)
|
|
2059
|
+
return yield* Effect.interrupt;
|
|
2060
|
+
const parent = yield* referencedDirectory(directoryReference, "openChildReference");
|
|
2061
|
+
yield* authorize(parent, identity, EXECUTE, "openChildReference");
|
|
2062
|
+
const direct = parent.entries.get(name);
|
|
2063
|
+
if (expected !== undefined) {
|
|
2064
|
+
let expectedNode;
|
|
2065
|
+
if (expected !== null) {
|
|
2066
|
+
const observed = yield* Effect.result(referencedNode(expected, "openChildReference"));
|
|
2067
|
+
if (Result.isFailure(observed)) {
|
|
2068
|
+
return yield* new FsError({ code: "VolumeBusy", operation: "openChildReference" });
|
|
2069
|
+
}
|
|
2070
|
+
expectedNode = observed.success;
|
|
2071
|
+
}
|
|
2072
|
+
if (direct !== expectedNode) {
|
|
2073
|
+
return yield* new FsError({ code: "VolumeBusy", operation: "openChildReference" });
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (chosen.expectedChild === null) {
|
|
2077
|
+
if (direct !== undefined) {
|
|
2078
|
+
return yield* new FsError({ code: "StaleReference", operation: "openChildReference" });
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
else if (chosen.expectedChild !== undefined) {
|
|
2082
|
+
const expected = chosen.expectedChild;
|
|
2083
|
+
const observed = yield* referencedNode(expected.reference, "openChildReference");
|
|
2084
|
+
if (direct !== observed || observed.revision !== expected.revision ||
|
|
2085
|
+
observed.metadata.atimeNs !== expected.atimeNs || observed.metadata.mtimeNs !== expected.mtimeNs) {
|
|
2086
|
+
return yield* new FsError({ code: "StaleReference", operation: "openChildReference" });
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
if (direct !== undefined && chosen.create === "exclusive") {
|
|
2090
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "openChildReference" });
|
|
2091
|
+
}
|
|
2092
|
+
let file = direct;
|
|
2093
|
+
let mutationParent = parent;
|
|
2094
|
+
let mutationName = name;
|
|
2095
|
+
if (file?.kind === "symlink" && chosen.followFinalSymlink !== false) {
|
|
2096
|
+
const resolved = yield* lookup(yield* Effect.fromResult(relativePath), undefined, "openChildReference", {
|
|
2097
|
+
followFinalSymlink: true,
|
|
2098
|
+
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
2099
|
+
}, parent);
|
|
2100
|
+
file = resolved.node;
|
|
2101
|
+
if (file === undefined) {
|
|
2102
|
+
if (resolved.parent === undefined || resolved.name === undefined) {
|
|
2103
|
+
return yield* new FsError({ code: "IsDirectory", operation: "openChildReference" });
|
|
2104
|
+
}
|
|
2105
|
+
mutationParent = resolved.parent;
|
|
2106
|
+
mutationName = resolved.name;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const before = mutationParent.revision;
|
|
2110
|
+
let created = false;
|
|
2111
|
+
if (file === undefined) {
|
|
2112
|
+
if (chosen.create === undefined || chosen.create === "never") {
|
|
2113
|
+
return yield* new FsError({ code: "NotFound", operation: "openChildReference" });
|
|
2114
|
+
}
|
|
2115
|
+
yield* authorize(mutationParent, identity, WRITE | EXECUTE, "openChildReference");
|
|
2116
|
+
if (atEntryLimit()) {
|
|
2117
|
+
return yield* new FsError({ code: "NoSpace", operation: "openChildReference" });
|
|
2118
|
+
}
|
|
2119
|
+
const size = chosen.initialSize ?? 0n;
|
|
2120
|
+
if (size < 0n) {
|
|
2121
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
|
|
2122
|
+
}
|
|
2123
|
+
if (size > BigInt(maxFileBytes)) {
|
|
2124
|
+
return yield* new FsError({ code: "FileTooLarge", operation: "openChildReference" });
|
|
2125
|
+
}
|
|
2126
|
+
if (settings.maxBytes !== undefined && size > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes) {
|
|
2127
|
+
return yield* new FsError({ code: "NoSpace", operation: "openChildReference" });
|
|
2128
|
+
}
|
|
2129
|
+
if (!identity.privileged &&
|
|
2130
|
+
((chosen.owner?.uid !== undefined && chosen.owner.uid !== identity.uid) ||
|
|
2131
|
+
(chosen.owner?.gid !== undefined && chosen.owner.gid !== identity.gid &&
|
|
2132
|
+
!identity.groups.includes(chosen.owner.gid)))) {
|
|
2133
|
+
return yield* new FsError({ code: "AccessDenied", operation: "openChildReference" });
|
|
2134
|
+
}
|
|
2135
|
+
const now = yield* timestamp("openChildReference");
|
|
2136
|
+
const initial = creationTimes(chosen.times, now);
|
|
2137
|
+
const creationMode = chosen.exactMode
|
|
2138
|
+
? yield* permittedMode({
|
|
2139
|
+
kind: "file",
|
|
2140
|
+
uid: chosen.owner?.uid ?? identity.uid,
|
|
2141
|
+
gid: chosen.owner?.gid ?? mutationParent.metadata.gid
|
|
2142
|
+
}, chosen.mode, "openChildReference")
|
|
2143
|
+
: (chosen.mode ?? 0o666) & 0o777 & ~umask;
|
|
2144
|
+
const createdFile = {
|
|
2145
|
+
kind: "file",
|
|
2146
|
+
lineage: undefined,
|
|
2147
|
+
data: Content.make(new Uint8Array(Number(size))),
|
|
2148
|
+
openCount: 0,
|
|
2149
|
+
metadata: {
|
|
2150
|
+
...directoryMetadata(state.nextInode, chosen.owner?.uid ?? identity.uid, chosen.owner?.gid ?? mutationParent.metadata.gid, creationMode, now),
|
|
2151
|
+
...initial,
|
|
2152
|
+
kind: "file",
|
|
2153
|
+
size,
|
|
2154
|
+
nlink: 1
|
|
2155
|
+
},
|
|
2156
|
+
revision: nextRevision(),
|
|
2157
|
+
objectReference: undefined
|
|
2158
|
+
};
|
|
2159
|
+
file = createdFile;
|
|
2160
|
+
attach(mutationParent, mutationName, createdFile, now);
|
|
2161
|
+
state.entries += 1;
|
|
2162
|
+
state.usedBytes += size;
|
|
2163
|
+
state.nextInode += 1n;
|
|
2164
|
+
created = true;
|
|
2165
|
+
publishEntry("Create", mutationParent, mutationName);
|
|
2166
|
+
}
|
|
2167
|
+
else {
|
|
2168
|
+
if (file.kind === "symlink") {
|
|
2169
|
+
return yield* new FsError({ code: "SymlinkLoop", operation: "openChildReference" });
|
|
2170
|
+
}
|
|
2171
|
+
if (file.kind !== "file") {
|
|
2172
|
+
return yield* new FsError({ code: "IsDirectory", operation: "openChildReference" });
|
|
2173
|
+
}
|
|
2174
|
+
yield* authorize(file, identity, chosen.access === "read" ? READ : chosen.access === "write" ? WRITE : READ | WRITE, "openChildReference");
|
|
2175
|
+
if (chosen.truncate)
|
|
2176
|
+
yield* resize(file, 0n, "openChildReference");
|
|
2177
|
+
}
|
|
2178
|
+
file.openCount += 1;
|
|
2179
|
+
acquired.file = file;
|
|
2180
|
+
return {
|
|
2181
|
+
handle: fileHandle(acquired),
|
|
2182
|
+
reference: referenceFor(file),
|
|
2183
|
+
created,
|
|
2184
|
+
directory: { before, after: mutationParent.revision }
|
|
2185
|
+
};
|
|
2186
|
+
}));
|
|
2187
|
+
}),
|
|
1102
2188
|
readFile: Effect.fn("Caller.readFile")(function* (input, options) {
|
|
1103
2189
|
const prepared = preparePath(input, "readFile", settings.maxPathBytes);
|
|
1104
2190
|
const base = options?.relativeTo;
|
|
1105
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2191
|
+
return yield* coordinated("readFile", Effect.gen(function* () {
|
|
1106
2192
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readFile");
|
|
1107
2193
|
if (node.kind !== "file") {
|
|
1108
2194
|
return yield* new FsError({ code: "IsDirectory", operation: "readFile", path: input });
|
|
@@ -1125,7 +2211,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1125
2211
|
return yield* new FsError({ code: "InvalidArgument", operation: "writeFile", path: input });
|
|
1126
2212
|
}
|
|
1127
2213
|
const chosen = decoded.success;
|
|
1128
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2214
|
+
return yield* coordinated("writeFile", Effect.gen(function* () {
|
|
1129
2215
|
const path = yield* Effect.fromResult(prepared);
|
|
1130
2216
|
if (chosen.create === "exclusive") {
|
|
1131
2217
|
const exists = yield* Effect.result(lookup(path, base, "writeFile", { followFinalSymlink: false }));
|
|
@@ -1172,7 +2258,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1172
2258
|
}
|
|
1173
2259
|
const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
|
|
1174
2260
|
if (settings.maxBytes !== undefined &&
|
|
1175
|
-
BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - usedBytes + BigInt(reclaimed)) {
|
|
2261
|
+
BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes + BigInt(reclaimed)) {
|
|
1176
2262
|
return yield* new FsError({ code: "NoSpace", operation: "writeFile", path: input });
|
|
1177
2263
|
}
|
|
1178
2264
|
if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
|
|
@@ -1194,7 +2280,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1194
2280
|
data: Content.empty(),
|
|
1195
2281
|
openCount: 0,
|
|
1196
2282
|
metadata: {
|
|
1197
|
-
...directoryMetadata(nextInode++, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
2283
|
+
...directoryMetadata(state.nextInode++, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
1198
2284
|
kind: "file",
|
|
1199
2285
|
nlink: 1
|
|
1200
2286
|
},
|
|
@@ -1210,13 +2296,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1210
2296
|
ctimeNs: now
|
|
1211
2297
|
};
|
|
1212
2298
|
advanceRevision(node);
|
|
1213
|
-
usedBytes += BigInt(size - previous);
|
|
2299
|
+
state.usedBytes += BigInt(size - previous);
|
|
1214
2300
|
if (file === undefined) {
|
|
1215
2301
|
if (replaced !== undefined)
|
|
1216
2302
|
detach(replaced, now);
|
|
1217
2303
|
attach(parent, name, node, now);
|
|
1218
2304
|
if (replaced === undefined)
|
|
1219
|
-
entries += 1;
|
|
2305
|
+
state.entries += 1;
|
|
1220
2306
|
publishEntry(replaced === undefined ? "Create" : "Update", parent, name);
|
|
1221
2307
|
}
|
|
1222
2308
|
else
|
|
@@ -1247,7 +2333,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1247
2333
|
if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
|
|
1248
2334
|
return yield* new FsError({ code: "InvalidArgument", operation: "access", path: input });
|
|
1249
2335
|
}
|
|
1250
|
-
return yield*
|
|
2336
|
+
return yield* coordinatedRead("access", Effect.gen(function* () {
|
|
1251
2337
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "access");
|
|
1252
2338
|
if (node.kind === "file" && (bits & EXECUTE) !== 0 && (node.metadata.mode & ANY_EXECUTE) === 0) {
|
|
1253
2339
|
return yield* new FsError({ code: "AccessDenied", operation: "access", path: input });
|
|
@@ -1258,7 +2344,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1258
2344
|
truncate: Effect.fn("Caller.truncate")(function* (input, length, options) {
|
|
1259
2345
|
const prepared = preparePath(input, "truncate", settings.maxPathBytes);
|
|
1260
2346
|
const base = options?.relativeTo;
|
|
1261
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2347
|
+
return yield* coordinated("truncate", Effect.gen(function* () {
|
|
1262
2348
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "truncate");
|
|
1263
2349
|
if (node.kind !== "file") {
|
|
1264
2350
|
return yield* new FsError({ code: "IsDirectory", operation: "truncate", path: input });
|
|
@@ -1270,7 +2356,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1270
2356
|
lstat: Effect.fn("Caller.lstat")(function* (input, options) {
|
|
1271
2357
|
const prepared = preparePath(input, "lstat", settings.maxPathBytes);
|
|
1272
2358
|
const base = options?.relativeTo;
|
|
1273
|
-
return yield*
|
|
2359
|
+
return yield* coordinatedRead("lstat", Effect.gen(function* () {
|
|
1274
2360
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "lstat", {
|
|
1275
2361
|
followFinalSymlink: false
|
|
1276
2362
|
});
|
|
@@ -1283,7 +2369,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1283
2369
|
const sourceBase = options?.sourceRelativeTo;
|
|
1284
2370
|
const destinationBase = options?.destinationRelativeTo;
|
|
1285
2371
|
const follow = options?.followSourceSymlink ?? false;
|
|
1286
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2372
|
+
return yield* coordinated("link", Effect.gen(function* () {
|
|
1287
2373
|
const node = yield* resolveNode(yield* Effect.fromResult(a), sourceBase, "link", {
|
|
1288
2374
|
followFinalSymlink: follow
|
|
1289
2375
|
});
|
|
@@ -1307,7 +2393,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1307
2393
|
attach(parent, name, node, now);
|
|
1308
2394
|
node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1, ctimeNs: now };
|
|
1309
2395
|
advanceRevision(node);
|
|
1310
|
-
entries += 1;
|
|
2396
|
+
state.entries += 1;
|
|
1311
2397
|
publishEntry("Create", parent, name);
|
|
1312
2398
|
}));
|
|
1313
2399
|
}),
|
|
@@ -1322,7 +2408,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1322
2408
|
}
|
|
1323
2409
|
const targetBytes = new Uint8Array(rawTarget.success);
|
|
1324
2410
|
const base = options?.relativeTo;
|
|
1325
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2411
|
+
return yield* coordinated("symlink", Effect.gen(function* () {
|
|
1326
2412
|
const path = yield* Effect.fromResult(prepared);
|
|
1327
2413
|
const bytes = targetBytes;
|
|
1328
2414
|
const parent = yield* locate(path, base, "symlink", { parentOnly: true });
|
|
@@ -1336,7 +2422,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1336
2422
|
}
|
|
1337
2423
|
if (atEntryLimit() ||
|
|
1338
2424
|
(settings.maxBytes !== undefined &&
|
|
1339
|
-
BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes))
|
|
2425
|
+
BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes))
|
|
1340
2426
|
return yield* new FsError({ code: "NoSpace", operation: "symlink", path: input });
|
|
1341
2427
|
const now = yield* timestamp("symlink");
|
|
1342
2428
|
const node = {
|
|
@@ -1344,7 +2430,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1344
2430
|
lineage: undefined,
|
|
1345
2431
|
target: new Uint8Array(bytes),
|
|
1346
2432
|
metadata: {
|
|
1347
|
-
...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, 0o777, now),
|
|
2433
|
+
...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, 0o777, now),
|
|
1348
2434
|
kind: "symlink",
|
|
1349
2435
|
nlink: 1,
|
|
1350
2436
|
size: BigInt(bytes.length)
|
|
@@ -1353,9 +2439,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1353
2439
|
objectReference: undefined
|
|
1354
2440
|
};
|
|
1355
2441
|
attach(parent, name, node, now);
|
|
1356
|
-
nextInode += 1n;
|
|
1357
|
-
entries += 1;
|
|
1358
|
-
usedBytes += BigInt(bytes.length);
|
|
2442
|
+
state.nextInode += 1n;
|
|
2443
|
+
state.entries += 1;
|
|
2444
|
+
state.usedBytes += BigInt(bytes.length);
|
|
1359
2445
|
publishEntry("Create", parent, name);
|
|
1360
2446
|
}));
|
|
1361
2447
|
}),
|
|
@@ -1391,16 +2477,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1391
2477
|
if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
1392
2478
|
return yield* new FsError({ code: "InvalidArgument", operation: "open", path: input });
|
|
1393
2479
|
}
|
|
1394
|
-
const acquired =
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
closed: false,
|
|
1398
|
-
offset: 0n,
|
|
1399
|
-
access: chosen.access,
|
|
1400
|
-
append: chosen.append ?? false
|
|
1401
|
-
};
|
|
1402
|
-
yield* Effect.addFinalizer(() => coordinated(Effect.sync(() => releaseFile(acquired))));
|
|
1403
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2480
|
+
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
2481
|
+
yield* Effect.addFinalizer(() => finalizeFile(acquired));
|
|
2482
|
+
return yield* coordinated("open", Effect.gen(function* () {
|
|
1404
2483
|
if (acquired.closed)
|
|
1405
2484
|
return yield* Effect.interrupt;
|
|
1406
2485
|
const path = yield* Effect.fromResult(prepared);
|
|
@@ -1417,8 +2496,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1417
2496
|
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1418
2497
|
});
|
|
1419
2498
|
const parent = resolved.parent;
|
|
1420
|
-
if (parent === undefined)
|
|
2499
|
+
if (parent === undefined) {
|
|
1421
2500
|
return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
|
|
2501
|
+
}
|
|
1422
2502
|
const name = resolved.name;
|
|
1423
2503
|
if (isDotComponent(name)) {
|
|
1424
2504
|
return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
|
|
@@ -1443,7 +2523,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1443
2523
|
data: Content.empty(),
|
|
1444
2524
|
openCount: 0,
|
|
1445
2525
|
metadata: {
|
|
1446
|
-
...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
2526
|
+
...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
1447
2527
|
kind: "file",
|
|
1448
2528
|
nlink: 1
|
|
1449
2529
|
},
|
|
@@ -1451,8 +2531,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1451
2531
|
objectReference: undefined
|
|
1452
2532
|
};
|
|
1453
2533
|
attach(parent, name, file, now);
|
|
1454
|
-
entries += 1;
|
|
1455
|
-
nextInode += 1n;
|
|
2534
|
+
state.entries += 1;
|
|
2535
|
+
state.nextInode += 1n;
|
|
1456
2536
|
publishEntry("Create", parent, name);
|
|
1457
2537
|
}
|
|
1458
2538
|
else {
|
|
@@ -1477,7 +2557,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1477
2557
|
unlink: Effect.fn("Caller.unlink")(function* (input, options) {
|
|
1478
2558
|
const prepared = preparePath(input, "unlink", settings.maxPathBytes);
|
|
1479
2559
|
const base = options?.relativeTo;
|
|
1480
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2560
|
+
return yield* coordinated("unlink", Effect.gen(function* () {
|
|
1481
2561
|
const path = yield* Effect.fromResult(prepared);
|
|
1482
2562
|
const parent = yield* locate(path, base, "unlink", { parentOnly: true });
|
|
1483
2563
|
yield* authorize(parent, identity, WRITE | EXECUTE, "unlink", input);
|
|
@@ -1500,7 +2580,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1500
2580
|
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1501
2581
|
advanceRevision(parent);
|
|
1502
2582
|
detach(child, now);
|
|
1503
|
-
entries -= 1;
|
|
2583
|
+
state.entries -= 1;
|
|
1504
2584
|
publishEntry("Remove", parent, name);
|
|
1505
2585
|
}));
|
|
1506
2586
|
}),
|
|
@@ -1509,7 +2589,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1509
2589
|
const newPrepared = preparePath(destination, "rename", settings.maxPathBytes);
|
|
1510
2590
|
const oldBase = options?.sourceRelativeTo;
|
|
1511
2591
|
const newBase = options?.destinationRelativeTo;
|
|
1512
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2592
|
+
return yield* coordinated("rename", Effect.gen(function* () {
|
|
1513
2593
|
const oldPath = yield* Effect.fromResult(oldPrepared);
|
|
1514
2594
|
const newPath = yield* Effect.fromResult(newPrepared);
|
|
1515
2595
|
const oldParent = yield* locate(oldPath, oldBase, "rename", { parentOnly: true });
|
|
@@ -1522,8 +2602,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1522
2602
|
return yield* new FsError({ code: "InvalidArgument", operation: "rename", path: source });
|
|
1523
2603
|
}
|
|
1524
2604
|
const child = oldParent.entries.get(oldName);
|
|
1525
|
-
if (child === undefined)
|
|
2605
|
+
if (child === undefined) {
|
|
1526
2606
|
return yield* new FsError({ code: "NotFound", operation: "rename", path: source });
|
|
2607
|
+
}
|
|
1527
2608
|
const replaced = newParent.entries.get(newName);
|
|
1528
2609
|
if (newPath.trailingSlash && replaced === undefined) {
|
|
1529
2610
|
return yield* new FsError({ code: "NotFound", operation: "rename", path: destination });
|
|
@@ -1556,7 +2637,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1556
2637
|
}
|
|
1557
2638
|
const now = yield* timestamp("rename");
|
|
1558
2639
|
// All rejection checks precede namespace, ancestry, quota, and metadata publication.
|
|
1559
|
-
const oldEvent = () => ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === root ? "" : SLASH_HEX) + oldName));
|
|
2640
|
+
const oldEvent = () => ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === state.root ? "" : SLASH_HEX) + oldName));
|
|
1560
2641
|
oldParent.entries.delete(oldName);
|
|
1561
2642
|
newParent.entries.set(newName, child);
|
|
1562
2643
|
if (child.kind === "directory")
|
|
@@ -1580,16 +2661,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1580
2661
|
advanceRevision(child);
|
|
1581
2662
|
if (replaced !== undefined) {
|
|
1582
2663
|
detach(replaced, now);
|
|
1583
|
-
entries -= 1;
|
|
2664
|
+
state.entries -= 1;
|
|
1584
2665
|
}
|
|
1585
|
-
watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() }));
|
|
2666
|
+
queuePublish(() => watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() })));
|
|
1586
2667
|
publishEntry("Create", newParent, newName);
|
|
1587
2668
|
}));
|
|
1588
2669
|
}),
|
|
1589
2670
|
rmdir: Effect.fn("Caller.rmdir")(function* (input, options) {
|
|
1590
2671
|
const prepared = preparePath(input, "rmdir", settings.maxPathBytes);
|
|
1591
2672
|
const base = options?.relativeTo;
|
|
1592
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2673
|
+
return yield* coordinated("rmdir", Effect.gen(function* () {
|
|
1593
2674
|
const path = yield* Effect.fromResult(prepared);
|
|
1594
2675
|
const parent = yield* locate(path, base, "rmdir", { parentOnly: true });
|
|
1595
2676
|
yield* authorize(parent, identity, WRITE | EXECUTE, "rmdir", input);
|
|
@@ -1604,8 +2685,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1604
2685
|
if (child.kind !== "directory") {
|
|
1605
2686
|
return yield* new FsError({ code: "NotDirectory", operation: "rmdir", path: input });
|
|
1606
2687
|
}
|
|
1607
|
-
if (child.entries.size > 0)
|
|
2688
|
+
if (child.entries.size > 0) {
|
|
1608
2689
|
return yield* new FsError({ code: "NotEmpty", operation: "rmdir", path: input });
|
|
2690
|
+
}
|
|
1609
2691
|
const now = yield* timestamp("rmdir");
|
|
1610
2692
|
parent.entries.delete(name);
|
|
1611
2693
|
parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink - 1, mtimeNs: now, ctimeNs: now };
|
|
@@ -1614,14 +2696,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1614
2696
|
advanceRevision(parent);
|
|
1615
2697
|
advanceRevision(child);
|
|
1616
2698
|
invalidateReference(child);
|
|
1617
|
-
entries -= 1;
|
|
2699
|
+
state.entries -= 1;
|
|
1618
2700
|
publishEntry("Remove", parent, name);
|
|
1619
2701
|
}));
|
|
1620
2702
|
}),
|
|
1621
2703
|
stat: Effect.fn("Caller.stat")(function* (input, options) {
|
|
1622
2704
|
const prepared = preparePath(input, "stat", settings.maxPathBytes);
|
|
1623
2705
|
const base = options?.relativeTo;
|
|
1624
|
-
return yield*
|
|
2706
|
+
return yield* coordinatedRead("stat", Effect.gen(function* () {
|
|
1625
2707
|
const path = yield* Effect.fromResult(prepared);
|
|
1626
2708
|
const directory = yield* resolveNode(path, base, "stat");
|
|
1627
2709
|
return { ...directory.metadata };
|
|
@@ -1633,7 +2715,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1633
2715
|
const mode = options?.mode === undefined ? 0o777 : options.mode;
|
|
1634
2716
|
if (!isMode(mode))
|
|
1635
2717
|
return yield* new FsError({ code: "InvalidArgument", operation: "mkdir", path: input });
|
|
1636
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2718
|
+
return yield* coordinated("mkdir", Effect.gen(function* () {
|
|
1637
2719
|
const path = yield* Effect.fromResult(prepared);
|
|
1638
2720
|
const parent = yield* locate(path, base, "mkdir", { parentOnly: true });
|
|
1639
2721
|
yield* authorize(parent, identity, WRITE | EXECUTE, "mkdir", input);
|
|
@@ -1650,14 +2732,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1650
2732
|
lineage: undefined,
|
|
1651
2733
|
parent,
|
|
1652
2734
|
entries: new Map(),
|
|
1653
|
-
metadata: directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & STICKY_BIT), now),
|
|
2735
|
+
metadata: directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & STICKY_BIT), now),
|
|
1654
2736
|
revision: nextRevision(),
|
|
1655
2737
|
objectReference: undefined
|
|
1656
2738
|
};
|
|
1657
2739
|
// No Effect yield or expected failure between these publication writes.
|
|
1658
2740
|
attach(parent, name, child, now);
|
|
1659
|
-
nextInode += 1n;
|
|
1660
|
-
entries += 1;
|
|
2741
|
+
state.nextInode += 1n;
|
|
2742
|
+
state.entries += 1;
|
|
1661
2743
|
publishEntry("Create", parent, name);
|
|
1662
2744
|
}));
|
|
1663
2745
|
}),
|
|
@@ -1669,10 +2751,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1669
2751
|
const acquired = yield* acquireDirectory(input, options, "openDirectory");
|
|
1670
2752
|
const handle = Object.freeze({
|
|
1671
2753
|
[DirectoryHandleId]: true,
|
|
1672
|
-
stat:
|
|
2754
|
+
stat: coordinatedRead("stat", Effect.suspend(() => acquired.directory === undefined
|
|
1673
2755
|
? Effect.fail(new FsError({ code: "InvalidHandle", operation: "stat" }))
|
|
1674
2756
|
: Effect.succeed({ ...acquired.directory.metadata }))).pipe(Effect.withSpan("DirectoryHandle.stat")),
|
|
1675
|
-
close:
|
|
2757
|
+
close: coordinatedCleanup(Effect.suspend(() => {
|
|
1676
2758
|
if (acquired.directory === undefined) {
|
|
1677
2759
|
return Effect.fail(new FsError({ code: "InvalidHandle", operation: "close" }));
|
|
1678
2760
|
}
|
|
@@ -1700,18 +2782,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1700
2782
|
let surface;
|
|
1701
2783
|
const volume = Object.freeze({
|
|
1702
2784
|
[VolumeId]: true,
|
|
2785
|
+
durability,
|
|
2786
|
+
identity,
|
|
2787
|
+
incarnation,
|
|
2788
|
+
limits,
|
|
2789
|
+
usage: coordinatedRead("usage", Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries })))
|
|
2790
|
+
.pipe(Effect.withSpan("Volume.usage")),
|
|
1703
2791
|
watch: Effect.gen(function* () {
|
|
1704
2792
|
const hook = TestHooks.getRegistrationHook(surface);
|
|
1705
|
-
return yield* watchHub.subscribe(hook?.afterSubscribe);
|
|
2793
|
+
return yield* admit("watch", watchHub.subscribe(hook?.afterSubscribe));
|
|
1706
2794
|
}).pipe(Effect.withSpan("Volume.watch")),
|
|
1707
|
-
snapshot: coordinatedRead(captureSnapshot()).pipe(Effect.withSpan("Volume.snapshot")),
|
|
2795
|
+
snapshot: coordinatedRead("snapshot", captureSnapshot()).pipe(Effect.withSpan("Volume.snapshot")),
|
|
1708
2796
|
caller: Effect.fn("Volume.caller")(function* (options) {
|
|
1709
2797
|
const decoded = decodeConfiguration(RootCallerOptions, options === undefined ? {} : options);
|
|
1710
2798
|
if (Result.isFailure(decoded))
|
|
1711
2799
|
return yield* decoded.failure;
|
|
1712
2800
|
const chosen = decoded.success.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
|
|
1713
2801
|
const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
|
|
1714
|
-
return
|
|
2802
|
+
return yield* coordinatedRead("caller", Effect.sync(() => createCaller(makeDirectoryReference(state.root), identity, decoded.success.umask ?? 0o022)));
|
|
1715
2803
|
})
|
|
1716
2804
|
});
|
|
1717
2805
|
surface = volume;
|
|
@@ -1724,12 +2812,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1724
2812
|
...volume,
|
|
1725
2813
|
changes: Effect.fn("OverlayVolume.changes")(function* (options) {
|
|
1726
2814
|
const selected = yield* changeOptions(options);
|
|
1727
|
-
const current = yield* coordinatedRead(observeChanges());
|
|
2815
|
+
const current = yield* coordinatedRead("changes", observeChanges());
|
|
1728
2816
|
return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
|
|
1729
2817
|
}),
|
|
1730
2818
|
capture: Effect.fn("OverlayVolume.capture")(function* (options) {
|
|
1731
2819
|
const selected = yield* changeOptions(options);
|
|
1732
|
-
const current = yield* coordinatedRead(captureState(hook));
|
|
2820
|
+
const current = yield* coordinatedRead("capture", captureState(hook));
|
|
1733
2821
|
return Object.freeze({
|
|
1734
2822
|
snapshot: current.snapshot,
|
|
1735
2823
|
changes: publicChanges(compareOverlay(baseObservation, current.observation, selected.includeTimestamps ?? false))
|
|
@@ -1744,6 +2832,53 @@ export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
|
|
|
1744
2832
|
return yield* makeVolume(VolumeSource.Empty(), options).pipe(Effect.catchTag("ImageError", Effect.die));
|
|
1745
2833
|
});
|
|
1746
2834
|
/** @internal */
|
|
2835
|
+
export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
|
|
2836
|
+
let bytes;
|
|
2837
|
+
yield* makeVolume(VolumeSource.Empty(), options, undefined, (initial, identity, limits) => captureLiveImage(initial, identity, limits).pipe(Effect.tap((image) => Effect.sync(() => {
|
|
2838
|
+
bytes = image;
|
|
2839
|
+
})), Effect.asVoid));
|
|
2840
|
+
if (bytes === undefined)
|
|
2841
|
+
return yield* new ImageError({ code: "InvalidStructure", field: "liveImage" });
|
|
2842
|
+
return bytes;
|
|
2843
|
+
});
|
|
2844
|
+
/** @internal */
|
|
2845
|
+
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only") {
|
|
2846
|
+
const document = yield* LiveImage.decode(image, maxImageBytes);
|
|
2847
|
+
const prepared = new WeakMap();
|
|
2848
|
+
const identity = VolumeIdentity.make(document.identity);
|
|
2849
|
+
let shutdown;
|
|
2850
|
+
const limits = {
|
|
2851
|
+
maxEntries: document.limits.maxEntries,
|
|
2852
|
+
maxBytes: document.limits.maxBytes === undefined ? undefined : ByteSize.bytes(document.limits.maxBytes),
|
|
2853
|
+
maxFileBytes: document.limits.maxFileBytes === undefined
|
|
2854
|
+
? ByteSize.bytes(0xffffffff)
|
|
2855
|
+
: ByteSize.bytes(document.limits.maxFileBytes),
|
|
2856
|
+
maxPathBytes: document.limits.maxPathBytes === undefined ? undefined : ByteSize.bytes(document.limits.maxPathBytes),
|
|
2857
|
+
maxPendingOperations: 64,
|
|
2858
|
+
maxWatchEvents: 256
|
|
2859
|
+
};
|
|
2860
|
+
const volume = yield* makeVolume(VolumeSource.Live({ document }), undefined, {
|
|
2861
|
+
onReady: (effect) => {
|
|
2862
|
+
shutdown = effect;
|
|
2863
|
+
},
|
|
2864
|
+
prepare: (candidate) => captureLiveImage(candidate, identity, limits).pipe(Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
|
|
2865
|
+
? new FsError({ code: "StorageRejected", operation: "commit" })
|
|
2866
|
+
: Effect.sync(() => {
|
|
2867
|
+
prepared.set(candidate, bytes);
|
|
2868
|
+
})), Effect.mapError(() => new FsError({ code: "StorageRejected", operation: "commit" }))),
|
|
2869
|
+
commit: (candidate) => Effect.suspend(() => {
|
|
2870
|
+
const bytes = prepared.get(candidate);
|
|
2871
|
+
if (bytes === undefined)
|
|
2872
|
+
return Effect.succeed("unknown");
|
|
2873
|
+
prepared.delete(candidate);
|
|
2874
|
+
return commit(bytes);
|
|
2875
|
+
})
|
|
2876
|
+
}, undefined, durability);
|
|
2877
|
+
if (shutdown === undefined)
|
|
2878
|
+
return yield* new ImageError({ code: "InvalidStructure", field: "liveImage" });
|
|
2879
|
+
return Object.freeze({ volume, shutdown });
|
|
2880
|
+
});
|
|
2881
|
+
/** @internal */
|
|
1747
2882
|
export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
|
|
1748
2883
|
const image = yield* Image.inspect(snapshot);
|
|
1749
2884
|
return yield* makeVolume(VolumeSource.Snapshot({ image }), options);
|