@effect-vfs/core 0.3.0 → 0.4.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 +36 -149
- package/dist/BytePath.d.ts +34 -2
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +1 -1
- package/dist/LiveVolume.d.ts +101 -0
- package/dist/LiveVolume.d.ts.map +1 -0
- package/dist/LiveVolume.js +72 -0
- package/dist/Snapshot.d.ts +60 -2
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +59 -1
- package/dist/SnapshotDelta.d.ts +113 -3
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +112 -2
- package/dist/VirtualFileSystem.d.ts +1224 -90
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +678 -20
- 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 +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/internal/bytePath.d.ts.map +1 -1
- package/dist/internal/bytePath.js +5 -10
- package/dist/internal/bytes.d.ts +6 -0
- package/dist/internal/bytes.d.ts.map +1 -0
- package/dist/internal/bytes.js +17 -0
- package/dist/internal/canonicalBase64.d.ts +13 -5
- package/dist/internal/canonicalBase64.d.ts.map +1 -1
- package/dist/internal/canonicalBase64.js +28 -6
- package/dist/internal/image.d.ts +76 -90
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +98 -89
- 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/metadata.d.ts +28 -0
- package/dist/internal/metadata.d.ts.map +1 -0
- package/dist/internal/metadata.js +29 -0
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +346 -300
- package/dist/internal/snapshotDeltaModel.d.ts +2 -2
- package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
- package/dist/internal/snapshotDeltaModel.js +3 -1
- package/dist/internal/virtualFileSystem/errors.d.ts +6 -20
- package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/errors.js +22 -7
- package/dist/internal/virtualFileSystem/fixture.d.ts +4 -1
- package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/fixture.js +80 -74
- package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/overlayContent.js +9 -2
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts +2 -11
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/overlayDiff.js +45 -71
- package/dist/internal/virtualFileSystem/path.d.ts +15 -14
- package/dist/internal/virtualFileSystem/path.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/path.js +57 -54
- package/dist/internal/virtualFileSystem/stagedState.d.ts +22 -0
- package/dist/internal/virtualFileSystem/stagedState.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/stagedState.js +62 -0
- package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/watchHub.d.ts +3 -3
- package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/watchHub.js +45 -24
- package/dist/internal/virtualFileSystem.d.ts +307 -23
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +1738 -525
- package/package.json +23 -5
|
@@ -1,20 +1,27 @@
|
|
|
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
|
+
import * as Data from "effect/Data";
|
|
5
6
|
import * as Effect from "effect/Effect";
|
|
6
7
|
import * as Encoding from "effect/Encoding";
|
|
8
|
+
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as Order from "effect/Order";
|
|
10
|
+
import * as Predicate from "effect/Predicate";
|
|
7
11
|
import * as Result from "effect/Result";
|
|
8
12
|
import * as Schema from "effect/Schema";
|
|
9
13
|
import * as Semaphore from "effect/Semaphore";
|
|
10
14
|
import { BytePath } from "../BytePath.js";
|
|
11
15
|
import { ImageError } from "../Snapshot.js";
|
|
12
|
-
import {
|
|
16
|
+
import { CanonicalBase64 } from "./canonicalBase64.js";
|
|
13
17
|
import * as Image from "./image.js";
|
|
14
|
-
import
|
|
18
|
+
import * as LiveImage from "./liveImage.js";
|
|
19
|
+
import * as MetadataDomain from "./metadata.js";
|
|
20
|
+
import { ConfigurationError, decodeConfiguration, FsCode as FsCodeSchema, FsError } from "./virtualFileSystem/errors.js";
|
|
15
21
|
import * as Content from "./virtualFileSystem/overlayContent.js";
|
|
16
22
|
import { compareOverlay } from "./virtualFileSystem/overlayDiff.js";
|
|
17
|
-
import {
|
|
23
|
+
import { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, MAX_NAME_BYTES, nameBytes, ownedPath, preparePath, SLASH_BYTE, SLASH_HEX, strictString } from "./virtualFileSystem/path.js";
|
|
24
|
+
import { makeStagedState } from "./virtualFileSystem/stagedState.js";
|
|
18
25
|
import * as TestHooks from "./virtualFileSystem/testHooks.js";
|
|
19
26
|
import * as WatchHub from "./virtualFileSystem/watchHub.js";
|
|
20
27
|
/** @internal */
|
|
@@ -29,8 +36,22 @@ export const DirectoryHandleId = Symbol("@effect-vfs/core/DirectoryHandle");
|
|
|
29
36
|
export const ObjectReferenceId = Symbol("@effect-vfs/core/ObjectReference");
|
|
30
37
|
/** @internal */
|
|
31
38
|
export { ConfigurationError, FsCodeSchema as FsCode, FsError };
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
// POSIX permission bits, masked against mode once it is shifted to the caller's class.
|
|
40
|
+
const EXECUTE = 0o1;
|
|
41
|
+
const WRITE = 0o2;
|
|
42
|
+
const READ = 0o4;
|
|
43
|
+
// Execute for every class; a file with none is not executable by anyone.
|
|
44
|
+
const ANY_EXECUTE = 0o111;
|
|
45
|
+
// setuid and setgid, cleared whenever a file's contents or ownership change.
|
|
46
|
+
const SET_ID_BITS = 0o6000;
|
|
47
|
+
// Sticky: only the owner of an entry or of its directory may remove it.
|
|
48
|
+
const STICKY_BIT = 0o1000;
|
|
49
|
+
const WALK_YIELD_INTERVAL = 128;
|
|
50
|
+
// Largest signed 64-bit file offset, as POSIX off_t.
|
|
51
|
+
const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
|
|
52
|
+
const Mode = Schema.Natural.check(Schema.isLessThanOrEqualTo(0o7777));
|
|
53
|
+
const isMode = Schema.is(Mode);
|
|
54
|
+
const isNatural = Schema.is(Schema.Natural);
|
|
34
55
|
/** @internal */
|
|
35
56
|
export const Identity = Schema.Struct({
|
|
36
57
|
uid: Schema.Natural,
|
|
@@ -43,17 +64,39 @@ export const RootCallerOptions = Schema.Struct({
|
|
|
43
64
|
identity: Schema.optionalKey(Identity),
|
|
44
65
|
umask: Schema.optionalKey(Schema.Natural.check(Schema.isLessThanOrEqualTo(0o777)))
|
|
45
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"));
|
|
46
89
|
/** @internal */
|
|
47
90
|
export const VolumeOptions = Schema.Struct({
|
|
91
|
+
identity: Schema.optionalKey(VolumeIdentity),
|
|
48
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))),
|
|
49
95
|
maxBytes: Schema.optionalKey(Schema.ByteSize),
|
|
50
96
|
maxFileBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isLessThanOrEqualTo(size, ByteSize.bytes(0xffffffff)) ? undefined : "must be at most 4294967295 bytes"))),
|
|
51
97
|
maxPathBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isGreaterThanOrEqualTo(size, ByteSize.bytes(1)) ? undefined : "must be at least 1 byte")))
|
|
52
98
|
});
|
|
53
|
-
|
|
54
|
-
const timestampLimit = 10n ** 128n - 1n;
|
|
55
|
-
/** @internal */
|
|
56
|
-
export const Timestamp = Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(-timestampLimit), Schema.isLessThanOrEqualToBigInt(timestampLimit));
|
|
99
|
+
const isTimestamp = Schema.is(MetadataDomain.Timestamp);
|
|
57
100
|
/** @internal */
|
|
58
101
|
export const Metadata = Schema.Struct({
|
|
59
102
|
kind: Schema.Literals(["directory", "file", "symlink"]),
|
|
@@ -63,10 +106,10 @@ export const Metadata = Schema.Struct({
|
|
|
63
106
|
uid: Schema.Natural,
|
|
64
107
|
gid: Schema.Natural,
|
|
65
108
|
mode: Mode,
|
|
66
|
-
atimeNs: Timestamp,
|
|
67
|
-
mtimeNs: Timestamp,
|
|
68
|
-
ctimeNs: Timestamp,
|
|
69
|
-
birthtimeNs: Timestamp
|
|
109
|
+
atimeNs: MetadataDomain.Timestamp,
|
|
110
|
+
mtimeNs: MetadataDomain.Timestamp,
|
|
111
|
+
ctimeNs: MetadataDomain.Timestamp,
|
|
112
|
+
birthtimeNs: MetadataDomain.Timestamp
|
|
70
113
|
});
|
|
71
114
|
/** @internal */
|
|
72
115
|
export const OwnerUpdate = Schema.Struct({
|
|
@@ -77,12 +120,13 @@ export const OwnerUpdate = Schema.Struct({
|
|
|
77
120
|
export const TimeUpdate = Schema.Union([
|
|
78
121
|
Schema.Struct({ kind: Schema.Literal("now") }),
|
|
79
122
|
Schema.Struct({ kind: Schema.Literal("omit") }),
|
|
80
|
-
Schema.Struct({ kind: Schema.Literal("value"), nanoseconds: Timestamp })
|
|
123
|
+
Schema.Struct({ kind: Schema.Literal("value"), nanoseconds: MetadataDomain.Timestamp })
|
|
81
124
|
]);
|
|
82
125
|
/** @internal */
|
|
83
126
|
export const Times = Schema.Struct({ access: TimeUpdate, modification: TimeUpdate });
|
|
84
127
|
/** @internal */
|
|
85
128
|
export const SeekMode = Schema.Literals(["start", "current", "end", "data", "hole"]);
|
|
129
|
+
const isSeekMode = Schema.is(SeekMode);
|
|
86
130
|
/** @internal */
|
|
87
131
|
export const OpenSettings = Schema.Struct({
|
|
88
132
|
access: Schema.Literals(["read", "write", "readWrite"]),
|
|
@@ -93,11 +137,61 @@ export const OpenSettings = Schema.Struct({
|
|
|
93
137
|
followFinalSymlink: Schema.optionalKey(Schema.Boolean)
|
|
94
138
|
});
|
|
95
139
|
/** @internal */
|
|
96
|
-
export const
|
|
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
|
+
});
|
|
182
|
+
const WriteFileSettings = Schema.Struct({
|
|
97
183
|
...OpenSettings.fields,
|
|
98
184
|
replaceFinalSymlink: Schema.optionalKey(Schema.Boolean),
|
|
99
185
|
finalMode: Schema.optionalKey(Mode)
|
|
100
186
|
});
|
|
187
|
+
const decodeOwnerUpdate = Schema.decodeResult(OwnerUpdate, { onExcessProperty: "error" });
|
|
188
|
+
const decodeTimes = Schema.decodeResult(Times, { onExcessProperty: "error" });
|
|
189
|
+
const decodeWriteFileSettings = Schema.decodeResult(WriteFileSettings, { onExcessProperty: "error" });
|
|
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" });
|
|
101
195
|
/** @internal */
|
|
102
196
|
export const OverlayNodeKind = Schema.Literals(["directory", "file", "symlink"]);
|
|
103
197
|
/** @internal */
|
|
@@ -136,47 +230,112 @@ export const OverlayChangesOptions = Schema.Struct({
|
|
|
136
230
|
includeTimestamps: Schema.optionalKey(Schema.Boolean)
|
|
137
231
|
});
|
|
138
232
|
/** @internal */
|
|
139
|
-
export class CurrentFileSystem extends Context.Service()("@effect-vfs/core/CurrentFileSystem") {
|
|
140
|
-
}
|
|
141
|
-
/** @internal */
|
|
142
233
|
export const FixtureMetadata = Schema.Struct({
|
|
143
234
|
uid: Schema.optionalKey(Schema.Natural),
|
|
144
235
|
gid: Schema.optionalKey(Schema.Natural),
|
|
145
236
|
mode: Schema.optionalKey(Mode),
|
|
146
|
-
atimeNs: Schema.optionalKey(Timestamp),
|
|
147
|
-
mtimeNs: Schema.optionalKey(Timestamp),
|
|
148
|
-
ctimeNs: Schema.optionalKey(Timestamp),
|
|
149
|
-
birthtimeNs: Schema.optionalKey(Timestamp)
|
|
237
|
+
atimeNs: Schema.optionalKey(MetadataDomain.Timestamp),
|
|
238
|
+
mtimeNs: Schema.optionalKey(MetadataDomain.Timestamp),
|
|
239
|
+
ctimeNs: Schema.optionalKey(MetadataDomain.Timestamp),
|
|
240
|
+
birthtimeNs: Schema.optionalKey(MetadataDomain.Timestamp)
|
|
150
241
|
});
|
|
151
242
|
const FixturePath = Schema.Union([
|
|
152
243
|
Schema.String,
|
|
153
244
|
BytePath
|
|
154
245
|
]);
|
|
155
246
|
/** @internal */
|
|
247
|
+
export const FixtureEntry = Schema.Union([
|
|
248
|
+
Schema.Struct({
|
|
249
|
+
kind: Schema.Literal("directory"),
|
|
250
|
+
path: FixturePath,
|
|
251
|
+
metadata: Schema.optionalKey(FixtureMetadata)
|
|
252
|
+
}),
|
|
253
|
+
Schema.Struct({
|
|
254
|
+
kind: Schema.Literal("file"),
|
|
255
|
+
path: FixturePath,
|
|
256
|
+
bytes: Schema.Uint8Array,
|
|
257
|
+
metadata: Schema.optionalKey(FixtureMetadata)
|
|
258
|
+
}),
|
|
259
|
+
Schema.Struct({
|
|
260
|
+
kind: Schema.Literal("symlink"),
|
|
261
|
+
path: FixturePath,
|
|
262
|
+
target: FixturePath,
|
|
263
|
+
metadata: Schema.optionalKey(FixtureMetadata)
|
|
264
|
+
}),
|
|
265
|
+
Schema.Struct({ kind: Schema.Literal("hardLink"), path: FixturePath, target: FixturePath })
|
|
266
|
+
]).pipe(Schema.toTaggedUnion("kind"));
|
|
267
|
+
/** @internal */
|
|
156
268
|
export const Fixture = Schema.Struct({
|
|
157
269
|
rootMetadata: Schema.optionalKey(FixtureMetadata),
|
|
158
|
-
entries: Schema.Array(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
270
|
+
entries: Schema.Array(FixtureEntry)
|
|
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);
|
|
178
333
|
});
|
|
179
334
|
const objectReferences = new WeakMap();
|
|
335
|
+
const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
|
|
336
|
+
// Only a PathInput target names a path; a handle-based call has none to report in its error.
|
|
337
|
+
const pathOf = (target) => isFileHandle(target) || isDirectoryHandle(target) ? undefined : target;
|
|
338
|
+
const isDirectoryHandle = (value) => Predicate.hasProperty(DirectoryHandleId)(value);
|
|
180
339
|
const files = new WeakMap();
|
|
181
340
|
const handles = new WeakMap();
|
|
182
341
|
const directoryMetadata = (ino, uid, gid, mode, now) => ({
|
|
@@ -196,58 +355,235 @@ const storedMetadata = (metadata) => ({
|
|
|
196
355
|
uid: metadata.uid,
|
|
197
356
|
gid: metadata.gid,
|
|
198
357
|
mode: metadata.mode,
|
|
199
|
-
atimeNs:
|
|
200
|
-
mtimeNs:
|
|
201
|
-
ctimeNs:
|
|
202
|
-
birthtimeNs:
|
|
358
|
+
atimeNs: metadata.atimeNs,
|
|
359
|
+
mtimeNs: metadata.mtimeNs,
|
|
360
|
+
ctimeNs: metadata.ctimeNs,
|
|
361
|
+
birthtimeNs: metadata.birthtimeNs
|
|
203
362
|
});
|
|
363
|
+
/** @internal */
|
|
364
|
+
export const VolumeSource = Data.taggedEnum();
|
|
365
|
+
const UpdateChange = Schema.TaggedStruct("Update", { path: BytePath });
|
|
366
|
+
const RescanChange = Schema.TaggedStruct("Rescan", { path: BytePath });
|
|
204
367
|
// Each execution constructs a fresh volume and captures its Clock.
|
|
205
368
|
/** @internal */
|
|
206
|
-
export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
207
|
-
const image =
|
|
208
|
-
const
|
|
369
|
+
export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial) {
|
|
370
|
+
const image = "image" in source ? source.image : undefined;
|
|
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);
|
|
209
386
|
if (Result.isFailure(decoded))
|
|
210
387
|
return yield* decoded.failure;
|
|
211
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)));
|
|
212
394
|
const clock = yield* Clock.clockWith(Effect.succeed);
|
|
213
395
|
const initialTime = clock.currentTimeNanosUnsafe();
|
|
214
|
-
if (!
|
|
396
|
+
if (!isTimestamp(initialTime)) {
|
|
215
397
|
return yield* new ConfigurationError({ field: "clock.currentTimeNanos" });
|
|
216
398
|
}
|
|
217
399
|
const timestamp = (operation) => Effect.suspend(() => {
|
|
218
400
|
const now = clock.currentTimeNanosUnsafe();
|
|
219
|
-
return
|
|
401
|
+
return isTimestamp(now)
|
|
402
|
+
? Effect.succeed(now)
|
|
403
|
+
: Effect.fail(new FsError({ code: "InvalidArgument", operation: operation }));
|
|
220
404
|
});
|
|
221
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
|
+
};
|
|
222
538
|
const gate = Semaphore.makeUnsafe(1);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
233
565
|
};
|
|
234
|
-
|
|
235
|
-
let entries = 0;
|
|
236
|
-
let usedBytes = 0n;
|
|
566
|
+
const nextRevision = () => ++state.revisionCounter;
|
|
237
567
|
// The schema caps this value at uint32, so this boundary conversion is exact.
|
|
238
568
|
const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(0xffffffff)));
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
});
|
|
241
577
|
if (image !== undefined) {
|
|
242
578
|
const incoming = new Map();
|
|
243
579
|
let content = 0n;
|
|
244
580
|
let count = 0;
|
|
245
581
|
for (const record of image.records) {
|
|
246
|
-
if (
|
|
582
|
+
if (Image.Record.guards.directory(record))
|
|
247
583
|
count += record.entries.length;
|
|
248
584
|
else {
|
|
249
|
-
const length =
|
|
250
|
-
if (
|
|
585
|
+
const length = CanonicalBase64.decodedLength(Image.Record.guards.file(record) ? record.data : record.target);
|
|
586
|
+
if (Image.Record.guards.file(record) && length > maxFileBytes) {
|
|
251
587
|
return yield* new ImageError({ code: "LimitExceeded", field: "maxFileBytes" });
|
|
252
588
|
}
|
|
253
589
|
content += BigInt(length);
|
|
@@ -257,22 +593,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
257
593
|
(settings.maxBytes !== undefined && content > ByteSize.toBigInt(settings.maxBytes))) {
|
|
258
594
|
return yield* new ImageError({ code: "LimitExceeded", field: "volume" });
|
|
259
595
|
}
|
|
260
|
-
const baseContents =
|
|
596
|
+
const baseContents = Predicate.isTagged("Overlay")(source) ? Content.forOverlay(source.base, image) : undefined;
|
|
261
597
|
for (const record of image.records) {
|
|
262
598
|
const metadata = {
|
|
263
599
|
...record.metadata,
|
|
264
|
-
kind: record.
|
|
265
|
-
ino: record.id === image.root ? 1n : nextInode++,
|
|
266
|
-
nlink:
|
|
600
|
+
kind: record._tag,
|
|
601
|
+
ino: record.id === image.root ? 1n : state.nextInode++,
|
|
602
|
+
nlink: Image.Record.guards.directory(record) ? 2 : 0,
|
|
267
603
|
size: 0n,
|
|
268
|
-
atimeNs:
|
|
269
|
-
mtimeNs:
|
|
270
|
-
ctimeNs:
|
|
271
|
-
birthtimeNs:
|
|
604
|
+
atimeNs: record.metadata.atimeNs,
|
|
605
|
+
mtimeNs: record.metadata.mtimeNs,
|
|
606
|
+
ctimeNs: record.metadata.ctimeNs,
|
|
607
|
+
birthtimeNs: record.metadata.birthtimeNs
|
|
272
608
|
};
|
|
273
|
-
if (
|
|
609
|
+
if (Image.Record.guards.directory(record)) {
|
|
274
610
|
const node = record.id === image.root
|
|
275
|
-
? root
|
|
611
|
+
? state.root
|
|
276
612
|
: {
|
|
277
613
|
kind: "directory",
|
|
278
614
|
lineage: record.id,
|
|
@@ -285,8 +621,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
285
621
|
node.metadata = metadata;
|
|
286
622
|
incoming.set(record.id, node);
|
|
287
623
|
}
|
|
288
|
-
else if (
|
|
289
|
-
const data = baseContents?.get(record.id) ?? Content.make(
|
|
624
|
+
else if (Image.Record.guards.file(record)) {
|
|
625
|
+
const data = baseContents?.get(record.id) ?? Content.make(yield* CanonicalBase64.decode(record.data));
|
|
290
626
|
incoming.set(record.id, {
|
|
291
627
|
kind: "file",
|
|
292
628
|
lineage: record.id,
|
|
@@ -298,7 +634,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
298
634
|
});
|
|
299
635
|
}
|
|
300
636
|
else {
|
|
301
|
-
const target =
|
|
637
|
+
const target = yield* CanonicalBase64.decode(record.target);
|
|
302
638
|
incoming.set(record.id, {
|
|
303
639
|
kind: "symlink",
|
|
304
640
|
lineage: record.id,
|
|
@@ -310,7 +646,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
310
646
|
}
|
|
311
647
|
}
|
|
312
648
|
for (const record of image.records) {
|
|
313
|
-
if (
|
|
649
|
+
if (!Image.Record.guards.directory(record))
|
|
314
650
|
continue;
|
|
315
651
|
const parent = incoming.get(record.id);
|
|
316
652
|
if (parent?.kind !== "directory")
|
|
@@ -319,7 +655,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
319
655
|
const node = incoming.get(entry.target);
|
|
320
656
|
if (node === undefined)
|
|
321
657
|
return yield* new ImageError({ code: "InvalidStructure" });
|
|
322
|
-
parent.entries.set(Encoding.encodeHex(
|
|
658
|
+
parent.entries.set(Encoding.encodeHex(yield* CanonicalBase64.decode(entry.name)), node);
|
|
323
659
|
if (node.kind === "directory") {
|
|
324
660
|
node.parent = parent;
|
|
325
661
|
parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink + 1 };
|
|
@@ -328,10 +664,191 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
328
664
|
node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1 };
|
|
329
665
|
}
|
|
330
666
|
}
|
|
331
|
-
entries = count;
|
|
332
|
-
usedBytes = content;
|
|
667
|
+
state.entries = count;
|
|
668
|
+
state.usedBytes = content;
|
|
333
669
|
}
|
|
334
|
-
|
|
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
|
+
};
|
|
335
852
|
const directoryHex = (directory) => {
|
|
336
853
|
const names = [];
|
|
337
854
|
let current = directory;
|
|
@@ -343,22 +860,30 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
343
860
|
names.push(found[0]);
|
|
344
861
|
current = parent;
|
|
345
862
|
}
|
|
346
|
-
return
|
|
863
|
+
return SLASH_HEX + names.reverse().join(SLASH_HEX);
|
|
347
864
|
};
|
|
348
865
|
const publishEntry = (_tag, parent, name) => {
|
|
349
|
-
watchHub.publishUnsafe(() => {
|
|
866
|
+
queuePublish(() => watchHub.publishUnsafe(() => {
|
|
350
867
|
const prefix = directoryHex(parent);
|
|
351
|
-
return { _tag, path: ownedPath(nameBytes(prefix + (prefix ===
|
|
352
|
-
});
|
|
868
|
+
return { _tag, path: ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name)) };
|
|
869
|
+
}));
|
|
353
870
|
};
|
|
354
871
|
const publishNode = (target) => {
|
|
355
|
-
|
|
872
|
+
// Directories are never hard linked, so one name reaches them and the parent chain resolves it.
|
|
873
|
+
if (target.kind === "directory") {
|
|
874
|
+
// Removal and rename displacement drop the link count while open handles keep reaching the node.
|
|
875
|
+
// No name resolves it any more, so its empty parent chain would otherwise read as the root path.
|
|
876
|
+
// The file scan below stops on the same signal, and `lookup` already reads it as NotFound.
|
|
877
|
+
if (target.metadata.nlink === 0)
|
|
878
|
+
return;
|
|
879
|
+
queuePublish(() => watchHub.publishUnsafe(() => UpdateChange.make({ path: ownedPath(nameBytes(directoryHex(target))) })));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
queuePublish(() => watchHub.publishManyUnsafe(() => {
|
|
356
883
|
const changes = [];
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const pending = [[root, "2f"]];
|
|
361
|
-
while (pending.length > 0) {
|
|
884
|
+
const pending = [[state.root, SLASH_HEX]];
|
|
885
|
+
// nlink counts the names bound to this node, so the scan stops once it has found them all.
|
|
886
|
+
while (pending.length > 0 && changes.length < target.metadata.nlink) {
|
|
362
887
|
const next = pending.pop();
|
|
363
888
|
if (next === undefined)
|
|
364
889
|
break;
|
|
@@ -366,20 +891,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
366
891
|
for (const [name, node] of directory.entries) {
|
|
367
892
|
const path = prefix + name;
|
|
368
893
|
if (node === target) {
|
|
369
|
-
changes.push({
|
|
894
|
+
changes.push(UpdateChange.make({ path: ownedPath(nameBytes(path)) }));
|
|
370
895
|
}
|
|
371
896
|
if (node.kind === "directory")
|
|
372
|
-
pending.push([node, path +
|
|
897
|
+
pending.push([node, path + SLASH_HEX]);
|
|
373
898
|
}
|
|
374
899
|
}
|
|
375
900
|
return changes;
|
|
376
|
-
});
|
|
901
|
+
}));
|
|
377
902
|
};
|
|
378
903
|
const captureSnapshot = Effect.fnUntraced(function* () {
|
|
379
|
-
const ids = new Map([[root, "0"]]);
|
|
380
|
-
const pending = [root];
|
|
904
|
+
const ids = new Map([[state.root, "0"]]);
|
|
905
|
+
const pending = [state.root];
|
|
381
906
|
const records = [];
|
|
382
907
|
for (let index = 0; index < pending.length; index++) {
|
|
908
|
+
if (index % WALK_YIELD_INTERVAL === 0)
|
|
909
|
+
yield* Effect.yieldNow;
|
|
383
910
|
const node = pending[index];
|
|
384
911
|
if (node === undefined)
|
|
385
912
|
continue;
|
|
@@ -396,22 +923,25 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
396
923
|
ids.set(child, target);
|
|
397
924
|
pending.push(child);
|
|
398
925
|
}
|
|
399
|
-
children.push({ name:
|
|
926
|
+
children.push({ name: CanonicalBase64.encode(nameBytes(name)), target });
|
|
400
927
|
}
|
|
401
|
-
records.push({ id,
|
|
928
|
+
records.push(Image.Record.cases.directory.make({ id, metadata, entries: children }));
|
|
402
929
|
}
|
|
403
930
|
else if (node.kind === "file") {
|
|
404
|
-
records.push({ id,
|
|
931
|
+
records.push(Image.Record.cases.file.make({ id, metadata, data: CanonicalBase64.encode(node.data.bytes) }));
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
records.push(Image.Record.cases.symlink.make({ id, metadata, target: CanonicalBase64.encode(node.target) }));
|
|
405
935
|
}
|
|
406
|
-
else
|
|
407
|
-
records.push({ id, kind: "symlink", metadata, target: Image.base64(node.target) });
|
|
408
936
|
}
|
|
409
|
-
return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records });
|
|
937
|
+
return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records }, undefined, true);
|
|
410
938
|
});
|
|
411
|
-
const observeChanges = ()
|
|
939
|
+
const observeChanges = Effect.fnUntraced(function* () {
|
|
412
940
|
const observation = [];
|
|
413
|
-
const paths = [[root, new Uint8Array([
|
|
941
|
+
const paths = [[state.root, new Uint8Array([SLASH_BYTE])]];
|
|
414
942
|
for (let index = 0; index < paths.length; index++) {
|
|
943
|
+
if (index % WALK_YIELD_INTERVAL === 0)
|
|
944
|
+
yield* Effect.yieldNow;
|
|
415
945
|
const current = paths[index];
|
|
416
946
|
if (current === undefined)
|
|
417
947
|
continue;
|
|
@@ -431,18 +961,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
431
961
|
childPath.set(path);
|
|
432
962
|
let offset = path.length;
|
|
433
963
|
if (path.length !== 1)
|
|
434
|
-
childPath[offset++] =
|
|
964
|
+
childPath[offset++] = SLASH_BYTE;
|
|
435
965
|
childPath.set(bytes, offset);
|
|
436
966
|
paths.push([child, childPath]);
|
|
437
967
|
}
|
|
438
968
|
}
|
|
439
969
|
return observation;
|
|
440
|
-
};
|
|
970
|
+
});
|
|
441
971
|
const captureState = Effect.fnUntraced(function* (hook) {
|
|
442
972
|
const snapshot = yield* captureSnapshot();
|
|
443
973
|
if (hook !== undefined)
|
|
444
974
|
yield* hook.betweenSnapshotAndSummary;
|
|
445
|
-
return { snapshot, observation: observeChanges() };
|
|
975
|
+
return { snapshot, observation: yield* observeChanges() };
|
|
446
976
|
});
|
|
447
977
|
const advanceRevision = (node) => {
|
|
448
978
|
node.revision = nextRevision();
|
|
@@ -451,7 +981,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
451
981
|
if (node.objectReference !== undefined)
|
|
452
982
|
return node.objectReference;
|
|
453
983
|
const reference = Object.freeze({ [ObjectReferenceId]: true });
|
|
454
|
-
objectReferences.set(reference, { volume: volumeIdentity, node });
|
|
984
|
+
objectReferences.set(reference, { volume: volumeIdentity, cell: cellFor(node), active: true });
|
|
455
985
|
node.objectReference = reference;
|
|
456
986
|
return reference;
|
|
457
987
|
};
|
|
@@ -460,18 +990,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
460
990
|
if (reference === undefined)
|
|
461
991
|
return;
|
|
462
992
|
const state = objectReferences.get(reference);
|
|
463
|
-
if (state !== undefined)
|
|
464
|
-
|
|
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
|
+
}
|
|
465
1001
|
node.objectReference = undefined;
|
|
466
1002
|
};
|
|
467
|
-
const release = (reference) =>
|
|
1003
|
+
const release = (reference) => coordinatedCleanup(Effect.sync(() => {
|
|
468
1004
|
reference.directory = undefined;
|
|
469
1005
|
reference.closed = true;
|
|
470
1006
|
}));
|
|
471
|
-
const authorize = (
|
|
1007
|
+
const authorize = (node, identity, bits, operation, path) => {
|
|
472
1008
|
if (identity.privileged)
|
|
473
1009
|
return Effect.void;
|
|
474
|
-
const metadata =
|
|
1010
|
+
const metadata = node.metadata;
|
|
475
1011
|
const shift = metadata.uid === identity.uid ?
|
|
476
1012
|
6
|
|
477
1013
|
: metadata.gid === identity.gid || identity.groups.includes(metadata.gid)
|
|
@@ -479,15 +1015,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
479
1015
|
: 0;
|
|
480
1016
|
return ((metadata.mode >> shift) & bits) === bits
|
|
481
1017
|
? Effect.void
|
|
482
|
-
: Effect.fail(
|
|
1018
|
+
: Effect.fail(path === undefined
|
|
1019
|
+
? new FsError({ code: "AccessDenied", operation })
|
|
1020
|
+
: new FsError({ code: "AccessDenied", operation, path }));
|
|
483
1021
|
};
|
|
484
1022
|
const reclaim = (file) => {
|
|
485
1023
|
if (file.metadata.nlink === 0 && file.openCount === 0) {
|
|
486
|
-
usedBytes -= BigInt(file.data.bytes.length);
|
|
1024
|
+
state.usedBytes -= BigInt(file.data.bytes.length);
|
|
487
1025
|
file.data = Content.empty();
|
|
1026
|
+
state.retainedFiles.delete(file.metadata.ino);
|
|
488
1027
|
invalidateReference(file);
|
|
489
1028
|
}
|
|
490
1029
|
};
|
|
1030
|
+
// Whether the volume's entry quota leaves room for one more name.
|
|
1031
|
+
const atEntryLimit = () => settings.maxEntries !== undefined && state.entries >= settings.maxEntries;
|
|
1032
|
+
// A new subdirectory's ".." entry is a second link to the parent; other node kinds add none.
|
|
1033
|
+
const attach = (parent, name, node, now) => {
|
|
1034
|
+
parent.entries.set(name, node);
|
|
1035
|
+
parent.metadata = {
|
|
1036
|
+
...parent.metadata,
|
|
1037
|
+
nlink: parent.metadata.nlink + (node.kind === "directory" ? 1 : 0),
|
|
1038
|
+
mtimeNs: now,
|
|
1039
|
+
ctimeNs: now
|
|
1040
|
+
};
|
|
1041
|
+
advanceRevision(parent);
|
|
1042
|
+
};
|
|
491
1043
|
const detach = (node, now) => {
|
|
492
1044
|
if (node.kind === "directory") {
|
|
493
1045
|
node.parent = undefined;
|
|
@@ -498,10 +1050,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
498
1050
|
else {
|
|
499
1051
|
node.metadata = { ...node.metadata, nlink: node.metadata.nlink - 1, ctimeNs: now };
|
|
500
1052
|
advanceRevision(node);
|
|
501
|
-
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);
|
|
502
1056
|
reclaim(node);
|
|
1057
|
+
}
|
|
503
1058
|
else if (node.metadata.nlink === 0) {
|
|
504
|
-
usedBytes -= BigInt(node.target.length);
|
|
1059
|
+
state.usedBytes -= BigInt(node.target.length);
|
|
505
1060
|
invalidateReference(node);
|
|
506
1061
|
}
|
|
507
1062
|
}
|
|
@@ -514,43 +1069,58 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
514
1069
|
}
|
|
515
1070
|
ref.closed = true;
|
|
516
1071
|
};
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
usedBytes += BigInt(size - file.data.bytes.length);
|
|
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
|
+
})))));
|
|
1082
|
+
// Replacing a payload clears setuid and setgid, and charges the volume for the size delta.
|
|
1083
|
+
const replaceContent = (file, data, now, publish = true) => {
|
|
1084
|
+
state.usedBytes += BigInt(data.length - file.data.bytes.length);
|
|
531
1085
|
file.data = Content.make(data);
|
|
532
1086
|
file.metadata = {
|
|
533
1087
|
...file.metadata,
|
|
534
|
-
size: length,
|
|
535
|
-
mode: file.metadata.mode & ~
|
|
1088
|
+
size: BigInt(data.length),
|
|
1089
|
+
mode: file.metadata.mode & ~SET_ID_BITS,
|
|
536
1090
|
mtimeNs: now,
|
|
537
1091
|
ctimeNs: now
|
|
538
1092
|
};
|
|
539
1093
|
advanceRevision(file);
|
|
540
|
-
|
|
1094
|
+
if (publish)
|
|
1095
|
+
publishNode(file);
|
|
1096
|
+
};
|
|
1097
|
+
const resize = Effect.fnUntraced(function* (file, length, operation, publish = true) {
|
|
1098
|
+
if (!Predicate.isBigInt(length) || length < 0n) {
|
|
1099
|
+
return yield* new FsError({ code: "InvalidArgument", operation: operation });
|
|
1100
|
+
}
|
|
1101
|
+
if (length > BigInt(maxFileBytes))
|
|
1102
|
+
return yield* new FsError({ code: "FileTooLarge", operation: operation });
|
|
1103
|
+
const size = Number(length);
|
|
1104
|
+
if (settings.maxBytes !== undefined &&
|
|
1105
|
+
BigInt(size - file.data.bytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes) {
|
|
1106
|
+
return yield* new FsError({ code: "NoSpace", operation: operation });
|
|
1107
|
+
}
|
|
1108
|
+
const data = new Uint8Array(size);
|
|
1109
|
+
data.set(file.data.bytes.subarray(0, size));
|
|
1110
|
+
replaceContent(file, data, yield* timestamp(operation), publish);
|
|
541
1111
|
});
|
|
542
1112
|
const fileHandle = (ref) => {
|
|
543
1113
|
const get = (operation, access) => ref.file === undefined || (access === "read" && ref.access === "write") ||
|
|
544
1114
|
(access === "write" && ref.access === "read")
|
|
545
|
-
? Effect.fail(
|
|
1115
|
+
? Effect.fail(new FsError({ code: "InvalidHandle", operation: operation }))
|
|
546
1116
|
: Effect.succeed(ref.file);
|
|
547
|
-
const read = Effect.
|
|
1117
|
+
const read = (maximum, position) => coordinated(position === undefined ? "read" : "pread", Effect.gen(function* () {
|
|
548
1118
|
const file = yield* get(position === undefined ? "read" : "pread", "read");
|
|
549
|
-
if (!
|
|
550
|
-
return yield*
|
|
1119
|
+
if (!isNatural(maximum))
|
|
1120
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "read" });
|
|
551
1121
|
const offset = position ?? ref.offset;
|
|
552
|
-
if (
|
|
553
|
-
return yield*
|
|
1122
|
+
if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
|
|
1123
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "read" });
|
|
554
1124
|
}
|
|
555
1125
|
const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
|
|
556
1126
|
const data = file.data.bytes.slice(start, start + Math.min(maximum, file.data.bytes.length - start));
|
|
@@ -560,32 +1130,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
560
1130
|
if (position === undefined)
|
|
561
1131
|
ref.offset += BigInt(data.length);
|
|
562
1132
|
return data;
|
|
563
|
-
}
|
|
1133
|
+
}));
|
|
564
1134
|
const write = Effect.fnUntraced(function* (input, position) {
|
|
565
|
-
if (!(input
|
|
566
|
-
return yield*
|
|
567
|
-
}
|
|
1135
|
+
if (!isAttachedBytes(input))
|
|
1136
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "write" });
|
|
568
1137
|
const bytes = new Uint8Array(input);
|
|
569
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1138
|
+
return yield* coordinated(position === undefined ? "write" : "pwrite", Effect.gen(function* () {
|
|
570
1139
|
const file = yield* get(position === undefined ? "write" : "pwrite", "write");
|
|
571
1140
|
const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
|
|
572
|
-
if (
|
|
573
|
-
return yield*
|
|
1141
|
+
if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
|
|
1142
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "write" });
|
|
574
1143
|
}
|
|
575
1144
|
if (bytes.length === 0) {
|
|
576
1145
|
return 0;
|
|
577
1146
|
}
|
|
578
1147
|
if (offset >= BigInt(maxFileBytes))
|
|
579
|
-
return yield*
|
|
1148
|
+
return yield* new FsError({ code: "FileTooLarge", operation: "write" });
|
|
580
1149
|
const start = Number(offset);
|
|
581
1150
|
const free = settings.maxBytes === undefined
|
|
582
1151
|
? BigInt(maxFileBytes)
|
|
583
|
-
: ByteSize.toBigInt(settings.maxBytes) - usedBytes;
|
|
1152
|
+
: ByteSize.toBigInt(settings.maxBytes) - state.usedBytes;
|
|
584
1153
|
const maximumEnd = BigInt(file.data.bytes.length) + free;
|
|
585
1154
|
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
586
1155
|
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
587
1156
|
if (count === 0)
|
|
588
|
-
return yield*
|
|
1157
|
+
return yield* new FsError({ code: "NoSpace", operation: "write" });
|
|
589
1158
|
const size = Math.max(file.data.bytes.length, start + count);
|
|
590
1159
|
// Always detach before mutation. A same-sized write is the critical
|
|
591
1160
|
// case: the current payload may belong to the base or a prior capture.
|
|
@@ -593,17 +1162,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
593
1162
|
data.set(file.data.bytes);
|
|
594
1163
|
const now = yield* timestamp("write");
|
|
595
1164
|
data.set(bytes.subarray(0, count), start);
|
|
596
|
-
|
|
597
|
-
file.data = Content.make(data);
|
|
598
|
-
file.metadata = {
|
|
599
|
-
...file.metadata,
|
|
600
|
-
size: BigInt(size),
|
|
601
|
-
mode: file.metadata.mode & ~0o6000,
|
|
602
|
-
mtimeNs: now,
|
|
603
|
-
ctimeNs: now
|
|
604
|
-
};
|
|
605
|
-
advanceRevision(file);
|
|
606
|
-
publishNode(file);
|
|
1165
|
+
replaceContent(file, data, now);
|
|
607
1166
|
if (position === undefined)
|
|
608
1167
|
ref.offset = offset + BigInt(count);
|
|
609
1168
|
return count;
|
|
@@ -624,37 +1183,45 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
624
1183
|
return yield* write(bytes, offset);
|
|
625
1184
|
}),
|
|
626
1185
|
seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
|
|
627
|
-
return yield*
|
|
1186
|
+
return yield* coordinatedRead("seek", Effect.uninterruptible(Effect.gen(function* () {
|
|
628
1187
|
const file = yield* get("seek");
|
|
629
|
-
if (
|
|
630
|
-
return yield*
|
|
1188
|
+
if (!Predicate.isBigInt(offset) || !isSeekMode(mode)) {
|
|
1189
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "seek" });
|
|
1190
|
+
}
|
|
1191
|
+
let next = mode === "current"
|
|
1192
|
+
? ref.offset + offset
|
|
1193
|
+
: mode === "end"
|
|
1194
|
+
? file.metadata.size + offset
|
|
1195
|
+
: offset;
|
|
1196
|
+
if (next < 0n || next > MAX_FILE_OFFSET) {
|
|
1197
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "seek" });
|
|
631
1198
|
}
|
|
632
|
-
let next = mode === "current" ? ref.offset + offset : mode === "end" ? file.metadata.size + offset : offset;
|
|
633
|
-
if (next < 0n || next > 0x7fffffffffffffffn)
|
|
634
|
-
return yield* failure("InvalidArgument", "seek");
|
|
635
1199
|
if (mode === "data" || mode === "hole") {
|
|
636
1200
|
if (offset >= file.metadata.size)
|
|
637
|
-
return yield*
|
|
1201
|
+
return yield* new FsError({ code: "NoData", operation: "seek" });
|
|
638
1202
|
if (mode === "hole")
|
|
639
1203
|
next = file.metadata.size;
|
|
640
1204
|
}
|
|
641
1205
|
ref.offset = next;
|
|
642
1206
|
return next;
|
|
643
|
-
}));
|
|
1207
|
+
})));
|
|
644
1208
|
}),
|
|
645
1209
|
truncate: Effect.fn("FileHandle.truncate")(function* (length) {
|
|
646
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1210
|
+
return yield* coordinated("truncate", Effect.gen(function* () {
|
|
647
1211
|
yield* resize(yield* get("truncate", "write"), length, "truncate");
|
|
648
1212
|
}));
|
|
649
1213
|
}),
|
|
650
|
-
stat:
|
|
1214
|
+
stat: coordinatedRead("stat", Effect.gen(function* () {
|
|
651
1215
|
return { ...(yield* get("stat")).metadata };
|
|
652
1216
|
})).pipe(Effect.withSpan("FileHandle.stat")),
|
|
653
|
-
sync:
|
|
654
|
-
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* () {
|
|
655
1219
|
yield* get("close");
|
|
656
1220
|
releaseFile(ref);
|
|
657
|
-
})).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"))
|
|
658
1225
|
});
|
|
659
1226
|
files.set(handle, ref);
|
|
660
1227
|
return handle;
|
|
@@ -662,50 +1229,84 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
662
1229
|
const createCaller = (reference, identity, umask) => {
|
|
663
1230
|
const referencedNode = Effect.fnUntraced(function* (target, operation) {
|
|
664
1231
|
if (reference.directory === undefined)
|
|
665
|
-
return yield*
|
|
666
|
-
if (
|
|
667
|
-
return yield*
|
|
1232
|
+
return yield* new FsError({ code: "ClosedCaller", operation: operation });
|
|
1233
|
+
if (!Predicate.isObject(target))
|
|
1234
|
+
return yield* new FsError({ code: "InvalidReference", operation: operation });
|
|
668
1235
|
const state = objectReferences.get(target);
|
|
669
1236
|
if (state === undefined)
|
|
670
|
-
return yield*
|
|
671
|
-
if (state.volume !== volumeIdentity)
|
|
672
|
-
return yield*
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1237
|
+
return yield* new FsError({ code: "InvalidReference", operation: operation });
|
|
1238
|
+
if (state.volume !== volumeIdentity) {
|
|
1239
|
+
return yield* new FsError({ code: "ForeignReference", operation: operation });
|
|
1240
|
+
}
|
|
1241
|
+
if (!state.active || activeStage?.invalidated.has(state)) {
|
|
1242
|
+
return yield* new FsError({ code: "StaleReference", operation: operation });
|
|
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;
|
|
676
1257
|
});
|
|
677
|
-
const
|
|
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
|
|
1267
|
+
});
|
|
1268
|
+
const lookup = Effect.fnUntraced(function* (path, base, operation, options = {}, referencedBase) {
|
|
678
1269
|
const { followFinalSymlink = true, allowMissing = false, parentOnly = false } = options;
|
|
679
|
-
if (reference.directory === undefined)
|
|
680
|
-
return yield*
|
|
681
|
-
|
|
682
|
-
|
|
1270
|
+
if (reference.directory === undefined) {
|
|
1271
|
+
return yield* new FsError({ code: "ClosedCaller", operation: operation, path: path.input });
|
|
1272
|
+
}
|
|
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) {
|
|
683
1279
|
const target = handles.get(base);
|
|
684
|
-
if (target === undefined)
|
|
685
|
-
return yield*
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1280
|
+
if (target === undefined) {
|
|
1281
|
+
return yield* new FsError({ code: "InvalidHandle", operation: operation, path: path.input });
|
|
1282
|
+
}
|
|
1283
|
+
if (target.volume !== volumeIdentity) {
|
|
1284
|
+
return yield* new FsError({ code: "ForeignHandle", operation: operation, path: path.input });
|
|
1285
|
+
}
|
|
1286
|
+
if (target.directory === undefined) {
|
|
1287
|
+
return yield* new FsError({ code: "InvalidHandle", operation: operation, path: path.input });
|
|
1288
|
+
}
|
|
690
1289
|
current = target.directory;
|
|
691
|
-
yield* authorize(current, identity,
|
|
1290
|
+
yield* authorize(current, identity, EXECUTE, operation, path.input);
|
|
1291
|
+
}
|
|
1292
|
+
if (current.metadata.nlink === 0) {
|
|
1293
|
+
return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
|
|
692
1294
|
}
|
|
693
|
-
if (current.metadata.nlink === 0)
|
|
694
|
-
return yield* failure("NotFound", operation, path.input);
|
|
695
1295
|
let work = path;
|
|
696
1296
|
let parent;
|
|
697
1297
|
let name;
|
|
698
1298
|
let traversals = 0;
|
|
699
1299
|
for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
|
|
700
|
-
if (current.kind !== "directory")
|
|
701
|
-
return yield*
|
|
702
|
-
|
|
1300
|
+
if (current.kind !== "directory") {
|
|
1301
|
+
return yield* new FsError({ code: "NotDirectory", operation: operation, path: path.input });
|
|
1302
|
+
}
|
|
1303
|
+
yield* authorize(current, identity, EXECUTE, operation, path.input);
|
|
703
1304
|
const component = work.components[index];
|
|
704
1305
|
if (component === undefined)
|
|
705
1306
|
break;
|
|
706
|
-
if (component ===
|
|
1307
|
+
if (component === DOT_HEX)
|
|
707
1308
|
continue;
|
|
708
|
-
if (component ===
|
|
1309
|
+
if (component === DOT_DOT_HEX) {
|
|
709
1310
|
current = current.parent ?? current;
|
|
710
1311
|
parent = undefined;
|
|
711
1312
|
name = undefined;
|
|
@@ -718,72 +1319,83 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
718
1319
|
if (allowMissing && index === work.components.length - 1 && !work.trailingSlash) {
|
|
719
1320
|
return { node: undefined, parent, name };
|
|
720
1321
|
}
|
|
721
|
-
return yield*
|
|
1322
|
+
return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
|
|
722
1323
|
}
|
|
723
1324
|
if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
|
|
724
|
-
if (child.target.length === 0)
|
|
725
|
-
return yield*
|
|
726
|
-
|
|
727
|
-
|
|
1325
|
+
if (child.target.length === 0) {
|
|
1326
|
+
return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
|
|
1327
|
+
}
|
|
1328
|
+
if (++traversals > 40) {
|
|
1329
|
+
return yield* new FsError({ code: "SymlinkLoop", operation: operation, path: path.input });
|
|
1330
|
+
}
|
|
728
1331
|
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
729
1332
|
if (settings.maxPathBytes !== undefined &&
|
|
730
1333
|
ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
|
|
731
|
-
return yield*
|
|
1334
|
+
return yield* new FsError({ code: "PathTooLong", operation: operation, path: path.input });
|
|
732
1335
|
}
|
|
733
1336
|
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
734
1337
|
expansion.set(child.target);
|
|
735
1338
|
expansion.set(suffix, child.target.length);
|
|
736
|
-
|
|
1339
|
+
const expanded = preparePath(ownedPath(expansion), operation, settings.maxPathBytes);
|
|
1340
|
+
// The expansion is synthetic: its per-component limits are the caller's to hear about,
|
|
1341
|
+
// but the path in the error has to be the one the caller passed in.
|
|
1342
|
+
if (Result.isFailure(expanded)) {
|
|
1343
|
+
return yield* new FsError({ code: expanded.failure.code, operation: operation, path: path.input });
|
|
1344
|
+
}
|
|
1345
|
+
work = expanded.success;
|
|
737
1346
|
if (work.absolute)
|
|
738
|
-
current = root;
|
|
1347
|
+
current = state.root;
|
|
739
1348
|
index = -1;
|
|
740
1349
|
}
|
|
741
1350
|
else
|
|
742
1351
|
current = child;
|
|
743
1352
|
}
|
|
744
1353
|
if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
|
|
745
|
-
return yield*
|
|
1354
|
+
return yield* new FsError({ code: "NotDirectory", operation: operation, path: path.input });
|
|
746
1355
|
}
|
|
747
1356
|
return { node: current, parent, name };
|
|
748
1357
|
});
|
|
749
1358
|
const resolveNode = Effect.fnUntraced(function* (path, base, operation, options) {
|
|
750
1359
|
const result = yield* lookup(path, base, operation, options);
|
|
751
|
-
if (result.node === undefined)
|
|
752
|
-
return yield*
|
|
1360
|
+
if (result.node === undefined) {
|
|
1361
|
+
return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
|
|
1362
|
+
}
|
|
753
1363
|
return result.node;
|
|
754
1364
|
});
|
|
755
1365
|
const locate = Effect.fnUntraced(function* (path, base, operation, options) {
|
|
756
1366
|
const result = yield* lookup(path, base, operation, options);
|
|
757
1367
|
const node = result.node;
|
|
758
|
-
if (node === undefined)
|
|
759
|
-
return yield*
|
|
760
|
-
|
|
761
|
-
|
|
1368
|
+
if (node === undefined) {
|
|
1369
|
+
return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
|
|
1370
|
+
}
|
|
1371
|
+
if (node.kind !== "directory") {
|
|
1372
|
+
return yield* new FsError({ code: "NotDirectory", operation: operation, path: path.input });
|
|
1373
|
+
}
|
|
762
1374
|
return node;
|
|
763
1375
|
});
|
|
764
1376
|
const acquireDirectory = Effect.fnUntraced(function* (input, options, operation) {
|
|
765
1377
|
const prepared = preparePath(input, operation, settings.maxPathBytes);
|
|
766
1378
|
const base = options?.relativeTo;
|
|
767
|
-
const acquired =
|
|
1379
|
+
const acquired = makeDirectoryReference();
|
|
768
1380
|
// Register before retaining a directory. Closed scopes can run this immediately,
|
|
769
1381
|
// so registration must not happen while holding the volume permit.
|
|
770
1382
|
yield* Effect.addFinalizer(() => release(acquired));
|
|
771
|
-
return yield*
|
|
1383
|
+
return yield* coordinatedRead(operation, Effect.uninterruptible(Effect.gen(function* () {
|
|
772
1384
|
if (acquired.closed)
|
|
773
1385
|
return yield* Effect.interrupt;
|
|
774
1386
|
const path = yield* Effect.fromResult(prepared);
|
|
775
1387
|
const directory = yield* locate(path, base, operation);
|
|
776
|
-
yield* authorize(directory, identity,
|
|
1388
|
+
yield* authorize(directory, identity, EXECUTE, operation, input);
|
|
777
1389
|
acquired.directory = directory;
|
|
778
1390
|
return acquired;
|
|
779
|
-
}));
|
|
1391
|
+
})));
|
|
780
1392
|
});
|
|
781
1393
|
const list = Effect.fnUntraced(function* (input, options) {
|
|
782
1394
|
const prepared = preparePath(input, "readDirectory", settings.maxPathBytes);
|
|
783
1395
|
const base = options?.relativeTo;
|
|
784
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1396
|
+
return yield* coordinated("readDirectory", Effect.gen(function* () {
|
|
785
1397
|
const directory = yield* locate(yield* Effect.fromResult(prepared), base, "readDirectory");
|
|
786
|
-
yield* authorize(directory, identity,
|
|
1398
|
+
yield* authorize(directory, identity, READ, "readDirectory", input);
|
|
787
1399
|
const result = [...directory.entries.keys()].map(nameBytes);
|
|
788
1400
|
directory.metadata = { ...directory.metadata, atimeNs: (yield* timestamp("readDirectory")) };
|
|
789
1401
|
return result;
|
|
@@ -792,19 +1404,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
792
1404
|
const readTarget = Effect.fnUntraced(function* (input, options) {
|
|
793
1405
|
const prepared = preparePath(input, "readLink", settings.maxPathBytes);
|
|
794
1406
|
const base = options?.relativeTo;
|
|
795
|
-
return yield*
|
|
1407
|
+
return yield* coordinatedRead("readLink", Effect.gen(function* () {
|
|
796
1408
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readLink", {
|
|
797
1409
|
followFinalSymlink: false
|
|
798
1410
|
});
|
|
799
|
-
if (node.kind !== "symlink")
|
|
800
|
-
return yield*
|
|
1411
|
+
if (node.kind !== "symlink") {
|
|
1412
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "readLink", path: input });
|
|
1413
|
+
}
|
|
801
1414
|
return new Uint8Array(node.target);
|
|
802
1415
|
}));
|
|
803
1416
|
});
|
|
804
1417
|
const canonical = Effect.fnUntraced(function* (input, options) {
|
|
805
1418
|
const prepared = preparePath(input, "realPath", settings.maxPathBytes);
|
|
806
1419
|
const base = options?.relativeTo;
|
|
807
|
-
return yield*
|
|
1420
|
+
return yield* coordinatedRead("realPath", Effect.gen(function* () {
|
|
808
1421
|
const result = yield* lookup(yield* Effect.fromResult(prepared), base, "realPath");
|
|
809
1422
|
const components = [];
|
|
810
1423
|
if (result.node?.kind !== "directory" && result.name !== undefined)
|
|
@@ -813,26 +1426,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
813
1426
|
while (directory !== undefined && directory.parent !== undefined) {
|
|
814
1427
|
const parent = directory.parent;
|
|
815
1428
|
const entry = [...parent.entries].find(([, child]) => child === directory);
|
|
816
|
-
if (entry === undefined)
|
|
817
|
-
return yield*
|
|
1429
|
+
if (entry === undefined) {
|
|
1430
|
+
return yield* new FsError({ code: "NotFound", operation: "realPath", path: input });
|
|
1431
|
+
}
|
|
818
1432
|
components.push(entry[0]);
|
|
819
1433
|
directory = parent;
|
|
820
1434
|
}
|
|
821
|
-
return nameBytes(
|
|
1435
|
+
return nameBytes(SLASH_HEX + components.reverse().join(SLASH_HEX));
|
|
822
1436
|
}));
|
|
823
1437
|
});
|
|
824
1438
|
const metadataNode = Effect.fnUntraced(function* (target, options, operation) {
|
|
825
|
-
if (reference.directory === undefined)
|
|
826
|
-
return yield*
|
|
827
|
-
|
|
828
|
-
|
|
1439
|
+
if (reference.directory === undefined) {
|
|
1440
|
+
return yield* new FsError({ code: "ClosedCaller", operation: operation });
|
|
1441
|
+
}
|
|
1442
|
+
if (isFileHandle(target) || isDirectoryHandle(target)) {
|
|
1443
|
+
const ref = isFileHandle(target) ? files.get(target) : handles.get(target);
|
|
829
1444
|
if (ref === undefined)
|
|
830
|
-
return yield*
|
|
831
|
-
if (ref.volume !== volumeIdentity)
|
|
832
|
-
return yield*
|
|
1445
|
+
return yield* new FsError({ code: "InvalidHandle", operation: operation });
|
|
1446
|
+
if (ref.volume !== volumeIdentity) {
|
|
1447
|
+
return yield* new FsError({ code: "ForeignHandle", operation: operation });
|
|
1448
|
+
}
|
|
833
1449
|
const node = "file" in ref ? ref.file : ref.directory;
|
|
834
1450
|
if (node === undefined)
|
|
835
|
-
return yield*
|
|
1451
|
+
return yield* new FsError({ code: "InvalidHandle", operation: operation });
|
|
836
1452
|
return node;
|
|
837
1453
|
}
|
|
838
1454
|
const path = yield* Effect.fromResult(preparePath(target, operation, settings.maxPathBytes));
|
|
@@ -842,16 +1458,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
842
1458
|
});
|
|
843
1459
|
const permittedMode = (metadata, mode, operation, path) => {
|
|
844
1460
|
if (!identity.privileged && identity.uid !== metadata.uid) {
|
|
845
|
-
return Effect.fail(
|
|
1461
|
+
return Effect.fail(path === undefined
|
|
1462
|
+
? new FsError({ code: "AccessDenied", operation })
|
|
1463
|
+
: new FsError({ code: "AccessDenied", operation, path }));
|
|
846
1464
|
}
|
|
847
1465
|
const group = identity.gid === metadata.gid || identity.groups.includes(metadata.gid);
|
|
848
1466
|
return Effect.succeed(!identity.privileged && metadata.kind === "file" && !group ? mode & ~0o2000 : mode);
|
|
849
1467
|
};
|
|
850
1468
|
const changeMode = Effect.fnUntraced(function* (target, mode, options) {
|
|
851
|
-
if (!
|
|
852
|
-
return yield*
|
|
1469
|
+
if (!isMode(mode))
|
|
1470
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "chmod" });
|
|
853
1471
|
const chosen = options === undefined ? undefined : { ...options };
|
|
854
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1472
|
+
return yield* coordinated("chmod", Effect.gen(function* () {
|
|
855
1473
|
const node = yield* metadataNode(target, chosen, "chmod");
|
|
856
1474
|
const permitted = yield* permittedMode(node.metadata, mode, "chmod");
|
|
857
1475
|
node.metadata = {
|
|
@@ -864,17 +1482,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
864
1482
|
}));
|
|
865
1483
|
});
|
|
866
1484
|
const changeOwner = Effect.fnUntraced(function* (target, owner, options) {
|
|
867
|
-
const decoded =
|
|
1485
|
+
const decoded = decodeOwnerUpdate(owner);
|
|
868
1486
|
if (Result.isFailure(decoded))
|
|
869
|
-
return yield*
|
|
1487
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "chown" });
|
|
870
1488
|
const update = { ...decoded.success };
|
|
871
1489
|
const chosen = options === undefined ? undefined : { ...options };
|
|
872
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1490
|
+
return yield* coordinated("chown", Effect.gen(function* () {
|
|
873
1491
|
const node = yield* metadataNode(target, chosen, "chown");
|
|
874
1492
|
if (!identity.privileged && (identity.uid !== node.metadata.uid ||
|
|
875
1493
|
(update.uid !== undefined && update.uid !== node.metadata.uid) ||
|
|
876
1494
|
(update.gid !== undefined && update.gid !== identity.gid && !identity.groups.includes(update.gid)))) {
|
|
877
|
-
return yield*
|
|
1495
|
+
return yield* new FsError({ code: "AccessDenied", operation: "chown" });
|
|
878
1496
|
}
|
|
879
1497
|
if (update.uid === undefined && update.gid === undefined)
|
|
880
1498
|
return;
|
|
@@ -882,7 +1500,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
882
1500
|
...node.metadata,
|
|
883
1501
|
uid: update.uid ?? node.metadata.uid,
|
|
884
1502
|
gid: update.gid ?? node.metadata.gid,
|
|
885
|
-
mode: node.kind === "file" ? node.metadata.mode & ~
|
|
1503
|
+
mode: node.kind === "file" ? node.metadata.mode & ~SET_ID_BITS : node.metadata.mode,
|
|
886
1504
|
ctimeNs: (yield* timestamp("chown"))
|
|
887
1505
|
};
|
|
888
1506
|
advanceRevision(node);
|
|
@@ -890,20 +1508,26 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
890
1508
|
}));
|
|
891
1509
|
});
|
|
892
1510
|
const changeTimes = Effect.fnUntraced(function* (target, times, options) {
|
|
893
|
-
const decoded =
|
|
1511
|
+
const decoded = decodeTimes(times);
|
|
894
1512
|
if (Result.isFailure(decoded))
|
|
895
|
-
return yield*
|
|
1513
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "utimes" });
|
|
896
1514
|
const access = { ...decoded.success.access };
|
|
897
1515
|
const modification = { ...decoded.success.modification };
|
|
898
1516
|
const chosen = options === undefined ? undefined : { ...options };
|
|
899
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1517
|
+
return yield* coordinated("utimes", Effect.gen(function* () {
|
|
900
1518
|
const node = yield* metadataNode(target, chosen, "utimes");
|
|
901
1519
|
if (access.kind === "omit" && modification.kind === "omit")
|
|
902
1520
|
return;
|
|
1521
|
+
// POSIX grants write access only when both times are UTIME_NOW; both UTIME_OMIT
|
|
1522
|
+
// returned above. Every other combination, mixed ones included, needs ownership.
|
|
903
1523
|
if (!identity.privileged && identity.uid !== node.metadata.uid) {
|
|
904
|
-
if (access.kind !== "now" || modification.kind !== "now")
|
|
905
|
-
|
|
906
|
-
|
|
1524
|
+
if (access.kind !== "now" || modification.kind !== "now") {
|
|
1525
|
+
const path = pathOf(target);
|
|
1526
|
+
return yield* path === undefined
|
|
1527
|
+
? new FsError({ code: "AccessDenied", operation: "utimes" })
|
|
1528
|
+
: new FsError({ code: "AccessDenied", operation: "utimes", path });
|
|
1529
|
+
}
|
|
1530
|
+
yield* authorize(node, identity, WRITE, "utimes", pathOf(target));
|
|
907
1531
|
}
|
|
908
1532
|
const now = yield* timestamp("utimes");
|
|
909
1533
|
node.metadata = {
|
|
@@ -924,100 +1548,652 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
924
1548
|
publishNode(node);
|
|
925
1549
|
}));
|
|
926
1550
|
});
|
|
927
|
-
const authorizeRemoval = (parent, child, operation, input) => (parent.metadata.mode &
|
|
1551
|
+
const authorizeRemoval = (parent, child, operation, input) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
|
|
928
1552
|
identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
|
|
929
|
-
? Effect.fail(
|
|
1553
|
+
? Effect.fail(input === undefined
|
|
1554
|
+
? new FsError({ code: "AccessDenied", operation })
|
|
1555
|
+
: new FsError({ code: "AccessDenied", operation, path: input }))
|
|
930
1556
|
: Effect.void;
|
|
931
1557
|
return Object.freeze({
|
|
932
1558
|
[CallerId]: true,
|
|
933
|
-
rootReference:
|
|
934
|
-
if (reference.directory === undefined)
|
|
935
|
-
return yield*
|
|
936
|
-
|
|
1559
|
+
rootReference: coordinatedRead("rootReference", Effect.gen(function* () {
|
|
1560
|
+
if (reference.directory === undefined) {
|
|
1561
|
+
return yield* new FsError({ code: "ClosedCaller", operation: "rootReference" });
|
|
1562
|
+
}
|
|
1563
|
+
return referenceFor(state.root);
|
|
937
1564
|
})).pipe(Effect.withSpan("Caller.rootReference")),
|
|
938
1565
|
lookupReference: Effect.fn("Caller.lookupReference")(function* (directoryReference, name) {
|
|
939
|
-
if (!(name
|
|
940
|
-
name.
|
|
941
|
-
return yield*
|
|
1566
|
+
if (!isAttachedBytes(name) || name.length === 0 || name.length > MAX_NAME_BYTES || name.includes(0) ||
|
|
1567
|
+
name.includes(SLASH_BYTE))
|
|
1568
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "lookupReference" });
|
|
942
1569
|
const key = Encoding.encodeHex(new Uint8Array(name));
|
|
943
|
-
if (key
|
|
944
|
-
return yield*
|
|
945
|
-
return yield*
|
|
1570
|
+
if (isDotComponent(key))
|
|
1571
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "lookupReference" });
|
|
1572
|
+
return yield* coordinatedRead("lookupReference", Effect.gen(function* () {
|
|
946
1573
|
const directory = yield* referencedNode(directoryReference, "lookupReference");
|
|
947
|
-
if (directory.kind !== "directory")
|
|
948
|
-
return yield*
|
|
949
|
-
|
|
1574
|
+
if (directory.kind !== "directory") {
|
|
1575
|
+
return yield* new FsError({ code: "NotDirectory", operation: "lookupReference" });
|
|
1576
|
+
}
|
|
1577
|
+
yield* authorize(directory, identity, EXECUTE, "lookupReference", "/");
|
|
950
1578
|
const child = directory.entries.get(key);
|
|
951
1579
|
if (child === undefined)
|
|
952
|
-
return yield*
|
|
1580
|
+
return yield* new FsError({ code: "NotFound", operation: "lookupReference" });
|
|
953
1581
|
return referenceFor(child);
|
|
954
1582
|
}));
|
|
955
1583
|
}),
|
|
956
1584
|
parentReference: Effect.fn("Caller.parentReference")(function* (directoryReference) {
|
|
957
|
-
return yield*
|
|
1585
|
+
return yield* coordinatedRead("parentReference", Effect.gen(function* () {
|
|
958
1586
|
const directory = yield* referencedNode(directoryReference, "parentReference");
|
|
959
|
-
if (directory.kind !== "directory")
|
|
960
|
-
return yield*
|
|
961
|
-
|
|
1587
|
+
if (directory.kind !== "directory") {
|
|
1588
|
+
return yield* new FsError({ code: "NotDirectory", operation: "parentReference" });
|
|
1589
|
+
}
|
|
1590
|
+
yield* authorize(directory, identity, EXECUTE, "parentReference", "/");
|
|
962
1591
|
return referenceFor(directory.parent ?? directory);
|
|
963
1592
|
}));
|
|
964
1593
|
}),
|
|
965
1594
|
observeMetadata: Effect.fn("Caller.observeMetadata")(function* (objectReference) {
|
|
966
|
-
return yield*
|
|
1595
|
+
return yield* coordinatedRead("observeMetadata", Effect.gen(function* () {
|
|
967
1596
|
const node = yield* referencedNode(objectReference, "observeMetadata");
|
|
968
1597
|
return Object.freeze({ value: Object.freeze({ ...node.metadata }), revision: node.revision });
|
|
969
1598
|
}));
|
|
970
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
|
+
}),
|
|
971
1612
|
observeDirectory: Effect.fn("Caller.observeDirectory")(function* (directoryReference) {
|
|
972
|
-
return yield*
|
|
1613
|
+
return yield* coordinatedRead("observeDirectory", Effect.gen(function* () {
|
|
973
1614
|
const directory = yield* referencedNode(directoryReference, "observeDirectory");
|
|
974
|
-
if (directory.kind !== "directory")
|
|
975
|
-
return yield*
|
|
976
|
-
|
|
1615
|
+
if (directory.kind !== "directory") {
|
|
1616
|
+
return yield* new FsError({ code: "NotDirectory", operation: "observeDirectory" });
|
|
1617
|
+
}
|
|
1618
|
+
yield* authorize(directory, identity, READ, "observeDirectory", "/");
|
|
977
1619
|
const value = Object.freeze([...directory.entries].map(([name, node]) => Object.freeze({ name: nameBytes(name), reference: referenceFor(node) })));
|
|
978
1620
|
return Object.freeze({ value, revision: directory.revision });
|
|
979
1621
|
}));
|
|
980
1622
|
}),
|
|
981
1623
|
readLinkReference: Effect.fn("Caller.readLinkReference")(function* (objectReference) {
|
|
982
|
-
return yield*
|
|
1624
|
+
return yield* coordinatedRead("readLinkReference", Effect.gen(function* () {
|
|
983
1625
|
const node = yield* referencedNode(objectReference, "readLinkReference");
|
|
984
|
-
if (node.kind !== "symlink")
|
|
985
|
-
return yield*
|
|
1626
|
+
if (node.kind !== "symlink") {
|
|
1627
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "readLinkReference" });
|
|
1628
|
+
}
|
|
986
1629
|
return new Uint8Array(node.target);
|
|
987
1630
|
}));
|
|
988
1631
|
}),
|
|
989
|
-
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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* () {
|
|
1000
2020
|
if (acquired.closed)
|
|
1001
2021
|
return yield* Effect.interrupt;
|
|
1002
2022
|
const node = yield* referencedNode(objectReference, "openReference");
|
|
1003
2023
|
if (node.kind !== "file")
|
|
1004
|
-
return yield*
|
|
1005
|
-
if (node.metadata.nlink === 0)
|
|
1006
|
-
return yield*
|
|
1007
|
-
|
|
2024
|
+
return yield* new FsError({ code: "IsDirectory", operation: "openReference" });
|
|
2025
|
+
if (node.metadata.nlink === 0) {
|
|
2026
|
+
return yield* new FsError({ code: "StaleReference", operation: "openReference" });
|
|
2027
|
+
}
|
|
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");
|
|
1008
2031
|
node.openCount += 1;
|
|
1009
2032
|
acquired.file = node;
|
|
1010
2033
|
return fileHandle(acquired);
|
|
1011
2034
|
}));
|
|
1012
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
|
+
}),
|
|
1013
2188
|
readFile: Effect.fn("Caller.readFile")(function* (input, options) {
|
|
1014
2189
|
const prepared = preparePath(input, "readFile", settings.maxPathBytes);
|
|
1015
2190
|
const base = options?.relativeTo;
|
|
1016
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2191
|
+
return yield* coordinated("readFile", Effect.gen(function* () {
|
|
1017
2192
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readFile");
|
|
1018
|
-
if (node.kind !== "file")
|
|
1019
|
-
return yield*
|
|
1020
|
-
|
|
2193
|
+
if (node.kind !== "file") {
|
|
2194
|
+
return yield* new FsError({ code: "IsDirectory", operation: "readFile", path: input });
|
|
2195
|
+
}
|
|
2196
|
+
yield* authorize(node, identity, READ, "readFile", input);
|
|
1021
2197
|
const data = new Uint8Array(node.data.bytes);
|
|
1022
2198
|
node.metadata = { ...node.metadata, atimeNs: (yield* timestamp("readFile")) };
|
|
1023
2199
|
return data;
|
|
@@ -1025,21 +2201,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1025
2201
|
}),
|
|
1026
2202
|
writeFile: Effect.fn("Caller.writeFile")(function* (input, bytes, options) {
|
|
1027
2203
|
const prepared = preparePath(input, "writeFile", settings.maxPathBytes);
|
|
1028
|
-
if (!(bytes
|
|
1029
|
-
return yield*
|
|
2204
|
+
if (!isAttachedBytes(bytes)) {
|
|
2205
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "writeFile", path: input });
|
|
1030
2206
|
}
|
|
1031
2207
|
const captured = new Uint8Array(bytes);
|
|
1032
2208
|
const { relativeTo: base, ...raw } = options;
|
|
1033
|
-
const decoded =
|
|
1034
|
-
if (Result.isFailure(decoded))
|
|
1035
|
-
return yield*
|
|
2209
|
+
const decoded = decodeWriteFileSettings(raw);
|
|
2210
|
+
if (Result.isFailure(decoded)) {
|
|
2211
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "writeFile", path: input });
|
|
2212
|
+
}
|
|
1036
2213
|
const chosen = decoded.success;
|
|
1037
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2214
|
+
return yield* coordinated("writeFile", Effect.gen(function* () {
|
|
1038
2215
|
const path = yield* Effect.fromResult(prepared);
|
|
1039
2216
|
if (chosen.create === "exclusive") {
|
|
1040
2217
|
const exists = yield* Effect.result(lookup(path, base, "writeFile", { followFinalSymlink: false }));
|
|
1041
|
-
if (Result.isSuccess(exists))
|
|
1042
|
-
return yield*
|
|
2218
|
+
if (Result.isSuccess(exists)) {
|
|
2219
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "writeFile", path: input });
|
|
2220
|
+
}
|
|
1043
2221
|
if (exists.failure.code !== "NotFound")
|
|
1044
2222
|
return yield* exists.failure;
|
|
1045
2223
|
}
|
|
@@ -1049,36 +2227,39 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1049
2227
|
});
|
|
1050
2228
|
const { name, parent } = resolved;
|
|
1051
2229
|
if (parent === undefined || name === undefined || resolved.node?.kind === "directory") {
|
|
1052
|
-
return yield*
|
|
2230
|
+
return yield* new FsError({ code: "IsDirectory", operation: "writeFile", path: input });
|
|
1053
2231
|
}
|
|
1054
2232
|
const replaced = resolved.node?.kind === "symlink" ? resolved.node : undefined;
|
|
1055
2233
|
if (replaced !== undefined && !chosen.replaceFinalSymlink) {
|
|
1056
|
-
return yield*
|
|
2234
|
+
return yield* new FsError({ code: "SymlinkLoop", operation: "writeFile", path: input });
|
|
2235
|
+
}
|
|
2236
|
+
if (chosen.access === "read") {
|
|
2237
|
+
return yield* new FsError({ code: "InvalidHandle", operation: "writeFile", path: input });
|
|
1057
2238
|
}
|
|
1058
|
-
if (chosen.access === "read")
|
|
1059
|
-
return yield* failure("InvalidHandle", "writeFile", input);
|
|
1060
2239
|
const file = resolved.node?.kind === "file" ? resolved.node : undefined;
|
|
1061
2240
|
if (file === undefined) {
|
|
1062
|
-
yield* authorize(parent, identity,
|
|
2241
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "writeFile", input);
|
|
1063
2242
|
if (replaced !== undefined)
|
|
1064
2243
|
yield* authorizeRemoval(parent, replaced, "writeFile", input);
|
|
1065
|
-
if (replaced === undefined &&
|
|
1066
|
-
return yield*
|
|
2244
|
+
if (replaced === undefined && atEntryLimit()) {
|
|
2245
|
+
return yield* new FsError({ code: "NoSpace", operation: "writeFile", path: input });
|
|
1067
2246
|
}
|
|
1068
2247
|
}
|
|
1069
|
-
else
|
|
1070
|
-
yield* authorize(file, identity, chosen.access === "readWrite" ?
|
|
2248
|
+
else {
|
|
2249
|
+
yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, "writeFile", input);
|
|
2250
|
+
}
|
|
1071
2251
|
const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, "writeFile", input);
|
|
1072
2252
|
const previous = file?.data.bytes.length ?? 0;
|
|
1073
2253
|
const initial = chosen.truncate ? 0 : previous;
|
|
1074
2254
|
const position = chosen.append ? initial : 0;
|
|
1075
2255
|
const size = Math.max(initial, position + captured.length);
|
|
1076
|
-
if (size > maxFileBytes)
|
|
1077
|
-
return yield*
|
|
2256
|
+
if (size > maxFileBytes) {
|
|
2257
|
+
return yield* new FsError({ code: "FileTooLarge", operation: "writeFile", path: input });
|
|
2258
|
+
}
|
|
1078
2259
|
const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
|
|
1079
2260
|
if (settings.maxBytes !== undefined &&
|
|
1080
|
-
BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - usedBytes + BigInt(reclaimed)) {
|
|
1081
|
-
return yield*
|
|
2261
|
+
BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes + BigInt(reclaimed)) {
|
|
2262
|
+
return yield* new FsError({ code: "NoSpace", operation: "writeFile", path: input });
|
|
1082
2263
|
}
|
|
1083
2264
|
if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
|
|
1084
2265
|
return;
|
|
@@ -1095,10 +2276,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1095
2276
|
{
|
|
1096
2277
|
kind: "file",
|
|
1097
2278
|
lineage: undefined,
|
|
1098
|
-
|
|
2279
|
+
// Assigned below on the shared path that also covers an existing file.
|
|
2280
|
+
data: Content.empty(),
|
|
1099
2281
|
openCount: 0,
|
|
1100
2282
|
metadata: {
|
|
1101
|
-
...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),
|
|
1102
2284
|
kind: "file",
|
|
1103
2285
|
nlink: 1
|
|
1104
2286
|
},
|
|
@@ -1108,21 +2290,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1108
2290
|
node.data = Content.make(data);
|
|
1109
2291
|
node.metadata = {
|
|
1110
2292
|
...node.metadata,
|
|
1111
|
-
mode: finalMode ?? node.metadata.mode & ~
|
|
2293
|
+
mode: finalMode ?? node.metadata.mode & ~SET_ID_BITS,
|
|
1112
2294
|
size: BigInt(size),
|
|
1113
2295
|
mtimeNs: now,
|
|
1114
2296
|
ctimeNs: now
|
|
1115
2297
|
};
|
|
1116
2298
|
advanceRevision(node);
|
|
1117
|
-
usedBytes += BigInt(size - previous);
|
|
2299
|
+
state.usedBytes += BigInt(size - previous);
|
|
1118
2300
|
if (file === undefined) {
|
|
1119
2301
|
if (replaced !== undefined)
|
|
1120
2302
|
detach(replaced, now);
|
|
1121
|
-
parent
|
|
1122
|
-
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1123
|
-
advanceRevision(parent);
|
|
2303
|
+
attach(parent, name, node, now);
|
|
1124
2304
|
if (replaced === undefined)
|
|
1125
|
-
entries += 1;
|
|
2305
|
+
state.entries += 1;
|
|
1126
2306
|
publishEntry(replaced === undefined ? "Create" : "Update", parent, name);
|
|
1127
2307
|
}
|
|
1128
2308
|
else
|
|
@@ -1150,12 +2330,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1150
2330
|
access: Effect.fn("Caller.access")(function* (input, bits = 0, options) {
|
|
1151
2331
|
const prepared = preparePath(input, "access", settings.maxPathBytes);
|
|
1152
2332
|
const base = options?.relativeTo;
|
|
1153
|
-
if (!Number.isInteger(bits) || bits < 0 || bits >
|
|
1154
|
-
return yield*
|
|
1155
|
-
|
|
2333
|
+
if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
|
|
2334
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "access", path: input });
|
|
2335
|
+
}
|
|
2336
|
+
return yield* coordinatedRead("access", Effect.gen(function* () {
|
|
1156
2337
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "access");
|
|
1157
|
-
if (node.kind === "file" && (bits &
|
|
1158
|
-
return yield*
|
|
2338
|
+
if (node.kind === "file" && (bits & EXECUTE) !== 0 && (node.metadata.mode & ANY_EXECUTE) === 0) {
|
|
2339
|
+
return yield* new FsError({ code: "AccessDenied", operation: "access", path: input });
|
|
1159
2340
|
}
|
|
1160
2341
|
yield* authorize(node, identity, bits, "access", input);
|
|
1161
2342
|
}));
|
|
@@ -1163,18 +2344,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1163
2344
|
truncate: Effect.fn("Caller.truncate")(function* (input, length, options) {
|
|
1164
2345
|
const prepared = preparePath(input, "truncate", settings.maxPathBytes);
|
|
1165
2346
|
const base = options?.relativeTo;
|
|
1166
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2347
|
+
return yield* coordinated("truncate", Effect.gen(function* () {
|
|
1167
2348
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "truncate");
|
|
1168
|
-
if (node.kind !== "file")
|
|
1169
|
-
return yield*
|
|
1170
|
-
|
|
2349
|
+
if (node.kind !== "file") {
|
|
2350
|
+
return yield* new FsError({ code: "IsDirectory", operation: "truncate", path: input });
|
|
2351
|
+
}
|
|
2352
|
+
yield* authorize(node, identity, WRITE, "truncate", input);
|
|
1171
2353
|
yield* resize(node, length, "truncate");
|
|
1172
2354
|
}));
|
|
1173
2355
|
}),
|
|
1174
2356
|
lstat: Effect.fn("Caller.lstat")(function* (input, options) {
|
|
1175
2357
|
const prepared = preparePath(input, "lstat", settings.maxPathBytes);
|
|
1176
2358
|
const base = options?.relativeTo;
|
|
1177
|
-
return yield*
|
|
2359
|
+
return yield* coordinatedRead("lstat", Effect.gen(function* () {
|
|
1178
2360
|
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "lstat", {
|
|
1179
2361
|
followFinalSymlink: false
|
|
1180
2362
|
});
|
|
@@ -1187,67 +2369,68 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1187
2369
|
const sourceBase = options?.sourceRelativeTo;
|
|
1188
2370
|
const destinationBase = options?.destinationRelativeTo;
|
|
1189
2371
|
const follow = options?.followSourceSymlink ?? false;
|
|
1190
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2372
|
+
return yield* coordinated("link", Effect.gen(function* () {
|
|
1191
2373
|
const node = yield* resolveNode(yield* Effect.fromResult(a), sourceBase, "link", {
|
|
1192
2374
|
followFinalSymlink: follow
|
|
1193
2375
|
});
|
|
1194
|
-
if (node.kind === "directory")
|
|
1195
|
-
return yield*
|
|
2376
|
+
if (node.kind === "directory") {
|
|
2377
|
+
return yield* new FsError({ code: "IsDirectory", operation: "link", path: source });
|
|
2378
|
+
}
|
|
1196
2379
|
const path = yield* Effect.fromResult(b);
|
|
1197
2380
|
const parent = yield* locate(path, destinationBase, "link", { parentOnly: true });
|
|
1198
|
-
yield* authorize(parent, identity,
|
|
2381
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "link", destination);
|
|
1199
2382
|
const name = path.components.at(-1);
|
|
1200
|
-
if (name
|
|
1201
|
-
return yield*
|
|
2383
|
+
if (isDotComponent(name) || parent.entries.has(name)) {
|
|
2384
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "link", path: destination });
|
|
1202
2385
|
}
|
|
1203
|
-
if (path.trailingSlash)
|
|
1204
|
-
return yield*
|
|
1205
|
-
|
|
1206
|
-
|
|
2386
|
+
if (path.trailingSlash) {
|
|
2387
|
+
return yield* new FsError({ code: "NotDirectory", operation: "link", path: destination });
|
|
2388
|
+
}
|
|
2389
|
+
if (atEntryLimit()) {
|
|
2390
|
+
return yield* new FsError({ code: "NoSpace", operation: "link", path: destination });
|
|
1207
2391
|
}
|
|
1208
2392
|
const now = yield* timestamp("link");
|
|
1209
|
-
parent
|
|
1210
|
-
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
2393
|
+
attach(parent, name, node, now);
|
|
1211
2394
|
node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1, ctimeNs: now };
|
|
1212
|
-
advanceRevision(parent);
|
|
1213
2395
|
advanceRevision(node);
|
|
1214
|
-
entries += 1;
|
|
2396
|
+
state.entries += 1;
|
|
1215
2397
|
publishEntry("Create", parent, name);
|
|
1216
2398
|
}));
|
|
1217
2399
|
}),
|
|
1218
2400
|
symlink: Effect.fn("Caller.symlink")(function* (target, input, options) {
|
|
1219
2401
|
const prepared = preparePath(input, "symlink", settings.maxPathBytes);
|
|
1220
|
-
|
|
1221
|
-
|
|
2402
|
+
const rawTarget = inputBytes(target);
|
|
2403
|
+
if (Result.isFailure(rawTarget)) {
|
|
2404
|
+
return yield* new FsError({ code: rawTarget.failure, operation: "symlink", path: target });
|
|
1222
2405
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
return yield* failure("InvalidArgument", "symlink", target);
|
|
2406
|
+
if (rawTarget.success.includes(0)) {
|
|
2407
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "symlink", path: target });
|
|
1226
2408
|
}
|
|
1227
|
-
const targetBytes = new Uint8Array(rawTarget);
|
|
2409
|
+
const targetBytes = new Uint8Array(rawTarget.success);
|
|
1228
2410
|
const base = options?.relativeTo;
|
|
1229
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2411
|
+
return yield* coordinated("symlink", Effect.gen(function* () {
|
|
1230
2412
|
const path = yield* Effect.fromResult(prepared);
|
|
1231
2413
|
const bytes = targetBytes;
|
|
1232
2414
|
const parent = yield* locate(path, base, "symlink", { parentOnly: true });
|
|
1233
|
-
yield* authorize(parent, identity,
|
|
2415
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "symlink", input);
|
|
1234
2416
|
const name = path.components.at(-1);
|
|
1235
|
-
if (name
|
|
1236
|
-
return yield*
|
|
2417
|
+
if (isDotComponent(name) || parent.entries.has(name)) {
|
|
2418
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "symlink", path: input });
|
|
2419
|
+
}
|
|
2420
|
+
if (path.trailingSlash) {
|
|
2421
|
+
return yield* new FsError({ code: "NotDirectory", operation: "symlink", path: input });
|
|
1237
2422
|
}
|
|
1238
|
-
if (
|
|
1239
|
-
return yield* failure("NotDirectory", "symlink", input);
|
|
1240
|
-
if ((settings.maxEntries !== undefined && entries >= settings.maxEntries) ||
|
|
2423
|
+
if (atEntryLimit() ||
|
|
1241
2424
|
(settings.maxBytes !== undefined &&
|
|
1242
|
-
BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes))
|
|
1243
|
-
return yield*
|
|
2425
|
+
BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes))
|
|
2426
|
+
return yield* new FsError({ code: "NoSpace", operation: "symlink", path: input });
|
|
1244
2427
|
const now = yield* timestamp("symlink");
|
|
1245
2428
|
const node = {
|
|
1246
2429
|
kind: "symlink",
|
|
1247
2430
|
lineage: undefined,
|
|
1248
2431
|
target: new Uint8Array(bytes),
|
|
1249
2432
|
metadata: {
|
|
1250
|
-
...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, 0o777, now),
|
|
2433
|
+
...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, 0o777, now),
|
|
1251
2434
|
kind: "symlink",
|
|
1252
2435
|
nlink: 1,
|
|
1253
2436
|
size: BigInt(bytes.length)
|
|
@@ -1255,12 +2438,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1255
2438
|
revision: nextRevision(),
|
|
1256
2439
|
objectReference: undefined
|
|
1257
2440
|
};
|
|
1258
|
-
parent
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
entries += 1;
|
|
1263
|
-
usedBytes += BigInt(bytes.length);
|
|
2441
|
+
attach(parent, name, node, now);
|
|
2442
|
+
state.nextInode += 1n;
|
|
2443
|
+
state.entries += 1;
|
|
2444
|
+
state.usedBytes += BigInt(bytes.length);
|
|
1264
2445
|
publishEntry("Create", parent, name);
|
|
1265
2446
|
}));
|
|
1266
2447
|
}),
|
|
@@ -1285,33 +2466,28 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1285
2466
|
open: Effect.fn("Caller.open")(function* (input, options) {
|
|
1286
2467
|
const prepared = preparePath(input, "open", settings.maxPathBytes);
|
|
1287
2468
|
const { relativeTo: base, ...raw } = options;
|
|
1288
|
-
const decoded =
|
|
1289
|
-
if (Result.isFailure(decoded))
|
|
1290
|
-
return yield*
|
|
2469
|
+
const decoded = decodeOpenSettings(raw);
|
|
2470
|
+
if (Result.isFailure(decoded)) {
|
|
2471
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "open", path: input });
|
|
2472
|
+
}
|
|
1291
2473
|
const chosen = { ...decoded.success };
|
|
1292
2474
|
if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
|
|
1293
|
-
return yield*
|
|
2475
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "open", path: input });
|
|
1294
2476
|
}
|
|
1295
2477
|
if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
1296
|
-
return yield*
|
|
2478
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "open", path: input });
|
|
1297
2479
|
}
|
|
1298
|
-
const acquired =
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
closed: false,
|
|
1302
|
-
offset: 0n,
|
|
1303
|
-
access: chosen.access,
|
|
1304
|
-
append: chosen.append ?? false
|
|
1305
|
-
};
|
|
1306
|
-
yield* Effect.addFinalizer(() => coordinated(Effect.sync(() => releaseFile(acquired))));
|
|
1307
|
-
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* () {
|
|
1308
2483
|
if (acquired.closed)
|
|
1309
2484
|
return yield* Effect.interrupt;
|
|
1310
2485
|
const path = yield* Effect.fromResult(prepared);
|
|
1311
2486
|
if (chosen.create === "exclusive") {
|
|
1312
2487
|
const existing = yield* Effect.result(lookup(path, base, "open", { followFinalSymlink: false }));
|
|
1313
|
-
if (Result.isSuccess(existing))
|
|
1314
|
-
return yield*
|
|
2488
|
+
if (Result.isSuccess(existing)) {
|
|
2489
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "open", path: input });
|
|
2490
|
+
}
|
|
1315
2491
|
if (existing.failure.code !== "NotFound")
|
|
1316
2492
|
return yield* existing.failure;
|
|
1317
2493
|
}
|
|
@@ -1320,24 +2496,25 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1320
2496
|
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1321
2497
|
});
|
|
1322
2498
|
const parent = resolved.parent;
|
|
1323
|
-
if (parent === undefined)
|
|
1324
|
-
return yield*
|
|
2499
|
+
if (parent === undefined) {
|
|
2500
|
+
return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
|
|
2501
|
+
}
|
|
1325
2502
|
const name = resolved.name;
|
|
1326
|
-
if (name
|
|
1327
|
-
return yield*
|
|
2503
|
+
if (isDotComponent(name)) {
|
|
2504
|
+
return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
|
|
1328
2505
|
}
|
|
1329
|
-
yield* authorize(parent, identity,
|
|
2506
|
+
yield* authorize(parent, identity, EXECUTE, "open", input);
|
|
1330
2507
|
let file = resolved.node;
|
|
1331
2508
|
if (file !== undefined && chosen.create === "exclusive") {
|
|
1332
|
-
return yield*
|
|
2509
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "open", path: input });
|
|
1333
2510
|
}
|
|
1334
2511
|
if (file === undefined) {
|
|
1335
2512
|
if (chosen.create === undefined || chosen.create === "never" || path.trailingSlash) {
|
|
1336
|
-
return yield*
|
|
2513
|
+
return yield* new FsError({ code: "NotFound", operation: "open", path: input });
|
|
1337
2514
|
}
|
|
1338
|
-
yield* authorize(parent, identity,
|
|
1339
|
-
if (
|
|
1340
|
-
return yield*
|
|
2515
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "open", input);
|
|
2516
|
+
if (atEntryLimit()) {
|
|
2517
|
+
return yield* new FsError({ code: "NoSpace", operation: "open", path: input });
|
|
1341
2518
|
}
|
|
1342
2519
|
const now = yield* timestamp("open");
|
|
1343
2520
|
file = {
|
|
@@ -1346,28 +2523,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1346
2523
|
data: Content.empty(),
|
|
1347
2524
|
openCount: 0,
|
|
1348
2525
|
metadata: {
|
|
1349
|
-
...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),
|
|
1350
2527
|
kind: "file",
|
|
1351
2528
|
nlink: 1
|
|
1352
2529
|
},
|
|
1353
2530
|
revision: nextRevision(),
|
|
1354
2531
|
objectReference: undefined
|
|
1355
2532
|
};
|
|
1356
|
-
parent
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
entries += 1;
|
|
1360
|
-
nextInode += 1n;
|
|
2533
|
+
attach(parent, name, file, now);
|
|
2534
|
+
state.entries += 1;
|
|
2535
|
+
state.nextInode += 1n;
|
|
1361
2536
|
publishEntry("Create", parent, name);
|
|
1362
2537
|
}
|
|
1363
2538
|
else {
|
|
1364
|
-
if (file.kind === "symlink")
|
|
1365
|
-
return yield*
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
2539
|
+
if (file.kind === "symlink") {
|
|
2540
|
+
return yield* new FsError({ code: "SymlinkLoop", operation: "open", path: input });
|
|
2541
|
+
}
|
|
2542
|
+
if (file.kind !== "file") {
|
|
2543
|
+
return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
|
|
2544
|
+
}
|
|
2545
|
+
if (path.trailingSlash) {
|
|
2546
|
+
return yield* new FsError({ code: "NotDirectory", operation: "open", path: input });
|
|
2547
|
+
}
|
|
2548
|
+
yield* authorize(file, identity, chosen.access === "read" ? READ : chosen.access === "write" ? WRITE : READ | WRITE, "open", input);
|
|
1371
2549
|
if (chosen.truncate)
|
|
1372
2550
|
yield* resize(file, 0n, "open");
|
|
1373
2551
|
}
|
|
@@ -1379,28 +2557,30 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1379
2557
|
unlink: Effect.fn("Caller.unlink")(function* (input, options) {
|
|
1380
2558
|
const prepared = preparePath(input, "unlink", settings.maxPathBytes);
|
|
1381
2559
|
const base = options?.relativeTo;
|
|
1382
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2560
|
+
return yield* coordinated("unlink", Effect.gen(function* () {
|
|
1383
2561
|
const path = yield* Effect.fromResult(prepared);
|
|
1384
2562
|
const parent = yield* locate(path, base, "unlink", { parentOnly: true });
|
|
1385
|
-
yield* authorize(parent, identity,
|
|
2563
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "unlink", input);
|
|
1386
2564
|
const name = path.components.at(-1);
|
|
1387
|
-
if (name
|
|
1388
|
-
return yield*
|
|
2565
|
+
if (isDotComponent(name)) {
|
|
2566
|
+
return yield* new FsError({ code: "IsDirectory", operation: "unlink", path: input });
|
|
1389
2567
|
}
|
|
1390
2568
|
const child = parent.entries.get(name);
|
|
1391
2569
|
if (child === undefined)
|
|
1392
|
-
return yield*
|
|
1393
|
-
if (child.kind === "directory")
|
|
1394
|
-
return yield*
|
|
1395
|
-
|
|
1396
|
-
|
|
2570
|
+
return yield* new FsError({ code: "NotFound", operation: "unlink", path: input });
|
|
2571
|
+
if (child.kind === "directory") {
|
|
2572
|
+
return yield* new FsError({ code: "IsDirectory", operation: "unlink", path: input });
|
|
2573
|
+
}
|
|
2574
|
+
if (path.trailingSlash) {
|
|
2575
|
+
return yield* new FsError({ code: "NotDirectory", operation: "unlink", path: input });
|
|
2576
|
+
}
|
|
1397
2577
|
yield* authorizeRemoval(parent, child, "unlink", input);
|
|
1398
2578
|
const now = yield* timestamp("unlink");
|
|
1399
2579
|
parent.entries.delete(name);
|
|
1400
2580
|
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1401
2581
|
advanceRevision(parent);
|
|
1402
2582
|
detach(child, now);
|
|
1403
|
-
entries -= 1;
|
|
2583
|
+
state.entries -= 1;
|
|
1404
2584
|
publishEntry("Remove", parent, name);
|
|
1405
2585
|
}));
|
|
1406
2586
|
}),
|
|
@@ -1409,31 +2589,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1409
2589
|
const newPrepared = preparePath(destination, "rename", settings.maxPathBytes);
|
|
1410
2590
|
const oldBase = options?.sourceRelativeTo;
|
|
1411
2591
|
const newBase = options?.destinationRelativeTo;
|
|
1412
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2592
|
+
return yield* coordinated("rename", Effect.gen(function* () {
|
|
1413
2593
|
const oldPath = yield* Effect.fromResult(oldPrepared);
|
|
1414
2594
|
const newPath = yield* Effect.fromResult(newPrepared);
|
|
1415
2595
|
const oldParent = yield* locate(oldPath, oldBase, "rename", { parentOnly: true });
|
|
1416
2596
|
const newParent = yield* locate(newPath, newBase, "rename", { parentOnly: true });
|
|
1417
|
-
yield* authorize(oldParent, identity,
|
|
1418
|
-
yield* authorize(newParent, identity,
|
|
2597
|
+
yield* authorize(oldParent, identity, WRITE | EXECUTE, "rename", source);
|
|
2598
|
+
yield* authorize(newParent, identity, WRITE | EXECUTE, "rename", destination);
|
|
1419
2599
|
const oldName = oldPath.components.at(-1);
|
|
1420
2600
|
const newName = newPath.components.at(-1);
|
|
1421
|
-
if (oldName
|
|
1422
|
-
|
|
1423
|
-
return yield* failure("InvalidArgument", "rename", source);
|
|
2601
|
+
if (isDotComponent(oldName) || isDotComponent(newName)) {
|
|
2602
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "rename", path: source });
|
|
1424
2603
|
}
|
|
1425
2604
|
const child = oldParent.entries.get(oldName);
|
|
1426
|
-
if (child === undefined)
|
|
1427
|
-
return yield*
|
|
2605
|
+
if (child === undefined) {
|
|
2606
|
+
return yield* new FsError({ code: "NotFound", operation: "rename", path: source });
|
|
2607
|
+
}
|
|
1428
2608
|
const replaced = newParent.entries.get(newName);
|
|
1429
2609
|
if (newPath.trailingSlash && replaced === undefined) {
|
|
1430
|
-
return yield*
|
|
2610
|
+
return yield* new FsError({ code: "NotFound", operation: "rename", path: destination });
|
|
1431
2611
|
}
|
|
1432
2612
|
if (oldPath.trailingSlash && child.kind !== "directory") {
|
|
1433
|
-
return yield*
|
|
2613
|
+
return yield* new FsError({ code: "NotDirectory", operation: "rename", path: source });
|
|
1434
2614
|
}
|
|
1435
2615
|
if (newPath.trailingSlash && replaced?.kind !== "directory") {
|
|
1436
|
-
return yield*
|
|
2616
|
+
return yield* new FsError({ code: "NotDirectory", operation: "rename", path: destination });
|
|
1437
2617
|
}
|
|
1438
2618
|
if (child === replaced)
|
|
1439
2619
|
return;
|
|
@@ -1441,22 +2621,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1441
2621
|
if (replaced !== undefined) {
|
|
1442
2622
|
yield* authorizeRemoval(newParent, replaced, "rename", destination);
|
|
1443
2623
|
if (child.kind === "directory" && replaced.kind !== "directory") {
|
|
1444
|
-
return yield*
|
|
2624
|
+
return yield* new FsError({ code: "NotDirectory", operation: "rename", path: destination });
|
|
1445
2625
|
}
|
|
1446
2626
|
if (child.kind !== "directory" && replaced.kind === "directory") {
|
|
1447
|
-
return yield*
|
|
2627
|
+
return yield* new FsError({ code: "IsDirectory", operation: "rename", path: destination });
|
|
1448
2628
|
}
|
|
1449
2629
|
if (replaced.kind === "directory" && replaced.entries.size > 0) {
|
|
1450
|
-
return yield*
|
|
2630
|
+
return yield* new FsError({ code: "NotEmpty", operation: "rename", path: destination });
|
|
1451
2631
|
}
|
|
1452
2632
|
}
|
|
1453
2633
|
for (let ancestor = newParent; ancestor !== undefined; ancestor = ancestor.parent) {
|
|
1454
|
-
if (ancestor === child)
|
|
1455
|
-
return yield*
|
|
2634
|
+
if (ancestor === child) {
|
|
2635
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "rename", path: destination });
|
|
2636
|
+
}
|
|
1456
2637
|
}
|
|
1457
2638
|
const now = yield* timestamp("rename");
|
|
1458
2639
|
// All rejection checks precede namespace, ancestry, quota, and metadata publication.
|
|
1459
|
-
const oldEvent = () => ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === root ? "" :
|
|
2640
|
+
const oldEvent = () => ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === state.root ? "" : SLASH_HEX) + oldName));
|
|
1460
2641
|
oldParent.entries.delete(oldName);
|
|
1461
2642
|
newParent.entries.set(newName, child);
|
|
1462
2643
|
if (child.kind === "directory")
|
|
@@ -1480,31 +2661,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1480
2661
|
advanceRevision(child);
|
|
1481
2662
|
if (replaced !== undefined) {
|
|
1482
2663
|
detach(replaced, now);
|
|
1483
|
-
entries -= 1;
|
|
2664
|
+
state.entries -= 1;
|
|
1484
2665
|
}
|
|
1485
|
-
watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() }));
|
|
2666
|
+
queuePublish(() => watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() })));
|
|
1486
2667
|
publishEntry("Create", newParent, newName);
|
|
1487
2668
|
}));
|
|
1488
2669
|
}),
|
|
1489
2670
|
rmdir: Effect.fn("Caller.rmdir")(function* (input, options) {
|
|
1490
2671
|
const prepared = preparePath(input, "rmdir", settings.maxPathBytes);
|
|
1491
2672
|
const base = options?.relativeTo;
|
|
1492
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2673
|
+
return yield* coordinated("rmdir", Effect.gen(function* () {
|
|
1493
2674
|
const path = yield* Effect.fromResult(prepared);
|
|
1494
2675
|
const parent = yield* locate(path, base, "rmdir", { parentOnly: true });
|
|
1495
|
-
yield* authorize(parent, identity,
|
|
2676
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "rmdir", input);
|
|
1496
2677
|
const name = path.components.at(-1);
|
|
1497
|
-
if (name
|
|
1498
|
-
return yield*
|
|
2678
|
+
if (isDotComponent(name)) {
|
|
2679
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "rmdir", path: input });
|
|
1499
2680
|
}
|
|
1500
2681
|
const child = parent.entries.get(name);
|
|
1501
2682
|
if (child === undefined)
|
|
1502
|
-
return yield*
|
|
2683
|
+
return yield* new FsError({ code: "NotFound", operation: "rmdir", path: input });
|
|
1503
2684
|
yield* authorizeRemoval(parent, child, "rmdir", input);
|
|
1504
|
-
if (child.kind !== "directory")
|
|
1505
|
-
return yield*
|
|
1506
|
-
|
|
1507
|
-
|
|
2685
|
+
if (child.kind !== "directory") {
|
|
2686
|
+
return yield* new FsError({ code: "NotDirectory", operation: "rmdir", path: input });
|
|
2687
|
+
}
|
|
2688
|
+
if (child.entries.size > 0) {
|
|
2689
|
+
return yield* new FsError({ code: "NotEmpty", operation: "rmdir", path: input });
|
|
2690
|
+
}
|
|
1508
2691
|
const now = yield* timestamp("rmdir");
|
|
1509
2692
|
parent.entries.delete(name);
|
|
1510
2693
|
parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink - 1, mtimeNs: now, ctimeNs: now };
|
|
@@ -1513,14 +2696,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1513
2696
|
advanceRevision(parent);
|
|
1514
2697
|
advanceRevision(child);
|
|
1515
2698
|
invalidateReference(child);
|
|
1516
|
-
entries -= 1;
|
|
2699
|
+
state.entries -= 1;
|
|
1517
2700
|
publishEntry("Remove", parent, name);
|
|
1518
2701
|
}));
|
|
1519
2702
|
}),
|
|
1520
2703
|
stat: Effect.fn("Caller.stat")(function* (input, options) {
|
|
1521
2704
|
const prepared = preparePath(input, "stat", settings.maxPathBytes);
|
|
1522
2705
|
const base = options?.relativeTo;
|
|
1523
|
-
return yield*
|
|
2706
|
+
return yield* coordinatedRead("stat", Effect.gen(function* () {
|
|
1524
2707
|
const path = yield* Effect.fromResult(prepared);
|
|
1525
2708
|
const directory = yield* resolveNode(path, base, "stat");
|
|
1526
2709
|
return { ...directory.metadata };
|
|
@@ -1530,18 +2713,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1530
2713
|
const prepared = preparePath(input, "mkdir", settings.maxPathBytes);
|
|
1531
2714
|
const base = options?.relativeTo;
|
|
1532
2715
|
const mode = options?.mode === undefined ? 0o777 : options.mode;
|
|
1533
|
-
if (!
|
|
1534
|
-
return yield*
|
|
1535
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
2716
|
+
if (!isMode(mode))
|
|
2717
|
+
return yield* new FsError({ code: "InvalidArgument", operation: "mkdir", path: input });
|
|
2718
|
+
return yield* coordinated("mkdir", Effect.gen(function* () {
|
|
1536
2719
|
const path = yield* Effect.fromResult(prepared);
|
|
1537
2720
|
const parent = yield* locate(path, base, "mkdir", { parentOnly: true });
|
|
1538
|
-
yield* authorize(parent, identity,
|
|
2721
|
+
yield* authorize(parent, identity, WRITE | EXECUTE, "mkdir", input);
|
|
1539
2722
|
const name = path.components.at(-1);
|
|
1540
|
-
if (name
|
|
1541
|
-
return yield*
|
|
2723
|
+
if (isDotComponent(name) || parent.entries.has(name)) {
|
|
2724
|
+
return yield* new FsError({ code: "AlreadyExists", operation: "mkdir", path: input });
|
|
1542
2725
|
}
|
|
1543
|
-
if (
|
|
1544
|
-
return yield*
|
|
2726
|
+
if (atEntryLimit()) {
|
|
2727
|
+
return yield* new FsError({ code: "NoSpace", operation: "mkdir", path: input });
|
|
1545
2728
|
}
|
|
1546
2729
|
const now = yield* timestamp("mkdir");
|
|
1547
2730
|
const child = {
|
|
@@ -1549,22 +2732,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1549
2732
|
lineage: undefined,
|
|
1550
2733
|
parent,
|
|
1551
2734
|
entries: new Map(),
|
|
1552
|
-
metadata: directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode &
|
|
2735
|
+
metadata: directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & STICKY_BIT), now),
|
|
1553
2736
|
revision: nextRevision(),
|
|
1554
2737
|
objectReference: undefined
|
|
1555
2738
|
};
|
|
1556
|
-
const parentMetadata = {
|
|
1557
|
-
...parent.metadata,
|
|
1558
|
-
nlink: parent.metadata.nlink + 1,
|
|
1559
|
-
mtimeNs: now,
|
|
1560
|
-
ctimeNs: now
|
|
1561
|
-
};
|
|
1562
2739
|
// No Effect yield or expected failure between these publication writes.
|
|
1563
|
-
parent
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
nextInode += 1n;
|
|
1567
|
-
entries += 1;
|
|
2740
|
+
attach(parent, name, child, now);
|
|
2741
|
+
state.nextInode += 1n;
|
|
2742
|
+
state.entries += 1;
|
|
1568
2743
|
publishEntry("Create", parent, name);
|
|
1569
2744
|
}));
|
|
1570
2745
|
}),
|
|
@@ -1576,12 +2751,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1576
2751
|
const acquired = yield* acquireDirectory(input, options, "openDirectory");
|
|
1577
2752
|
const handle = Object.freeze({
|
|
1578
2753
|
[DirectoryHandleId]: true,
|
|
1579
|
-
stat:
|
|
1580
|
-
? Effect.fail(
|
|
2754
|
+
stat: coordinatedRead("stat", Effect.suspend(() => acquired.directory === undefined
|
|
2755
|
+
? Effect.fail(new FsError({ code: "InvalidHandle", operation: "stat" }))
|
|
1581
2756
|
: Effect.succeed({ ...acquired.directory.metadata }))).pipe(Effect.withSpan("DirectoryHandle.stat")),
|
|
1582
|
-
close:
|
|
1583
|
-
if (acquired.directory === undefined)
|
|
1584
|
-
return Effect.fail(
|
|
2757
|
+
close: coordinatedCleanup(Effect.suspend(() => {
|
|
2758
|
+
if (acquired.directory === undefined) {
|
|
2759
|
+
return Effect.fail(new FsError({ code: "InvalidHandle", operation: "close" }));
|
|
2760
|
+
}
|
|
1585
2761
|
acquired.directory = undefined;
|
|
1586
2762
|
acquired.closed = true;
|
|
1587
2763
|
return Effect.void;
|
|
@@ -1592,86 +2768,123 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
|
|
|
1592
2768
|
})
|
|
1593
2769
|
});
|
|
1594
2770
|
};
|
|
1595
|
-
const baseObservation =
|
|
1596
|
-
const publicChange = (change) =>
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
case "Removed":
|
|
1600
|
-
case "Updated":
|
|
1601
|
-
case "Replaced":
|
|
1602
|
-
return Object.freeze({ ...change, path: ownedPath(new Uint8Array(change.path)) });
|
|
1603
|
-
case "Renamed":
|
|
1604
|
-
return Object.freeze({
|
|
1605
|
-
...change,
|
|
1606
|
-
from: ownedPath(new Uint8Array(change.from)),
|
|
1607
|
-
to: ownedPath(new Uint8Array(change.to))
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
throw new Error("Unknown internal overlay change");
|
|
1611
|
-
};
|
|
2771
|
+
const baseObservation = Predicate.isTagged("Overlay")(source) ? yield* observeChanges() : undefined;
|
|
2772
|
+
const publicChange = (change) => Predicate.isTagged("Renamed")(change)
|
|
2773
|
+
? OverlayChange.make({ ...change, from: ownedPath(change.from), to: ownedPath(change.to) })
|
|
2774
|
+
: OverlayChange.make({ ...change, path: ownedPath(change.path) });
|
|
1612
2775
|
const publicChanges = (changes) => Object.freeze(changes.map(publicChange));
|
|
1613
2776
|
const changeOptions = (options) => {
|
|
1614
2777
|
const decoded = decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options);
|
|
1615
2778
|
return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
|
|
1616
2779
|
};
|
|
2780
|
+
// An overlay is a spread copy of `volume`, so the object the caller holds is not always the one
|
|
2781
|
+
// built here. `watch` runs lazily, so it reads whichever surface was actually handed out.
|
|
2782
|
+
let surface;
|
|
1617
2783
|
const volume = Object.freeze({
|
|
1618
2784
|
[VolumeId]: true,
|
|
2785
|
+
durability: "memory-only",
|
|
2786
|
+
identity,
|
|
2787
|
+
incarnation,
|
|
2788
|
+
limits,
|
|
2789
|
+
usage: coordinatedRead("usage", Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries })))
|
|
2790
|
+
.pipe(Effect.withSpan("Volume.usage")),
|
|
1619
2791
|
watch: Effect.gen(function* () {
|
|
1620
|
-
const hook = TestHooks.getRegistrationHook(
|
|
1621
|
-
return yield* watchHub.subscribe(hook?.afterSubscribe);
|
|
2792
|
+
const hook = TestHooks.getRegistrationHook(surface);
|
|
2793
|
+
return yield* admit("watch", watchHub.subscribe(hook?.afterSubscribe));
|
|
1622
2794
|
}).pipe(Effect.withSpan("Volume.watch")),
|
|
1623
|
-
snapshot:
|
|
2795
|
+
snapshot: coordinatedRead("snapshot", captureSnapshot()).pipe(Effect.withSpan("Volume.snapshot")),
|
|
1624
2796
|
caller: Effect.fn("Volume.caller")(function* (options) {
|
|
1625
2797
|
const decoded = decodeConfiguration(RootCallerOptions, options === undefined ? {} : options);
|
|
1626
2798
|
if (Result.isFailure(decoded))
|
|
1627
2799
|
return yield* decoded.failure;
|
|
1628
2800
|
const chosen = decoded.success.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
|
|
1629
2801
|
const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
|
|
1630
|
-
return
|
|
2802
|
+
return yield* coordinatedRead("caller", Effect.sync(() => createCaller(makeDirectoryReference(state.root), identity, decoded.success.umask ?? 0o022)));
|
|
1631
2803
|
})
|
|
1632
2804
|
});
|
|
1633
|
-
|
|
1634
|
-
|
|
2805
|
+
surface = volume;
|
|
2806
|
+
if (!Predicate.isTagged("Overlay")(source) || baseObservation === undefined) {
|
|
2807
|
+
// SAFETY: Overlay sources return below, so S is non-Overlay here and VolumeFor<S> is Volume.
|
|
2808
|
+
return volume;
|
|
1635
2809
|
}
|
|
1636
2810
|
const hook = TestHooks.getObservationHook(source.base);
|
|
1637
2811
|
const overlay = Object.freeze({
|
|
1638
2812
|
...volume,
|
|
1639
2813
|
changes: Effect.fn("OverlayVolume.changes")(function* (options) {
|
|
1640
2814
|
const selected = yield* changeOptions(options);
|
|
1641
|
-
const current = yield*
|
|
2815
|
+
const current = yield* coordinatedRead("changes", observeChanges());
|
|
1642
2816
|
return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
|
|
1643
2817
|
}),
|
|
1644
2818
|
capture: Effect.fn("OverlayVolume.capture")(function* (options) {
|
|
1645
2819
|
const selected = yield* changeOptions(options);
|
|
1646
|
-
const current = yield*
|
|
2820
|
+
const current = yield* coordinatedRead("capture", captureState(hook));
|
|
1647
2821
|
return Object.freeze({
|
|
1648
2822
|
snapshot: current.snapshot,
|
|
1649
2823
|
changes: publicChanges(compareOverlay(baseObservation, current.observation, selected.includeTimestamps ?? false))
|
|
1650
2824
|
});
|
|
1651
2825
|
})
|
|
1652
2826
|
});
|
|
1653
|
-
|
|
2827
|
+
surface = overlay;
|
|
2828
|
+
return overlay;
|
|
1654
2829
|
});
|
|
1655
2830
|
/** @internal */
|
|
1656
2831
|
export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
2832
|
+
return yield* makeVolume(VolumeSource.Empty(), options).pipe(Effect.catchTag("ImageError", Effect.die));
|
|
2833
|
+
});
|
|
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) {
|
|
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
|
+
});
|
|
2877
|
+
if (shutdown === undefined)
|
|
2878
|
+
return yield* new ImageError({ code: "InvalidStructure", field: "liveImage" });
|
|
2879
|
+
return Object.freeze({ volume, shutdown });
|
|
1661
2880
|
});
|
|
1662
2881
|
/** @internal */
|
|
1663
2882
|
export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
|
|
1664
2883
|
const image = yield* Image.inspect(snapshot);
|
|
1665
|
-
|
|
1666
|
-
if (result._tag === "Overlay")
|
|
1667
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
1668
|
-
return result.volume;
|
|
2884
|
+
return yield* makeVolume(VolumeSource.Snapshot({ image }), options);
|
|
1669
2885
|
});
|
|
1670
2886
|
/** @internal */
|
|
1671
2887
|
export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
|
|
1672
2888
|
const image = yield* Image.inspect(base);
|
|
1673
|
-
|
|
1674
|
-
if (result._tag === "Volume")
|
|
1675
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
1676
|
-
return result.volume;
|
|
2889
|
+
return yield* makeVolume(VolumeSource.Overlay({ base, image }), options);
|
|
1677
2890
|
});
|