@effect-vfs/core 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BytePath.d.ts +49 -5
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +9 -9
- package/dist/Snapshot.d.ts +61 -5
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +60 -5
- package/dist/SnapshotDelta.d.ts +208 -31
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +162 -25
- package/dist/VirtualFileSystem.d.ts +1035 -348
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +620 -1772
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/bytePath.d.ts +10 -0
- package/dist/internal/bytePath.d.ts.map +1 -0
- package/dist/internal/bytePath.js +41 -0
- 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 +14 -6
- package/dist/internal/canonicalBase64.d.ts.map +1 -1
- package/dist/internal/canonicalBase64.js +29 -7
- package/dist/internal/image.d.ts +85 -104
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +99 -95
- 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 +0 -1
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +358 -325
- package/dist/internal/snapshotDeltaModel.d.ts +10 -0
- package/dist/internal/snapshotDeltaModel.d.ts.map +1 -0
- package/dist/internal/snapshotDeltaModel.js +15 -0
- package/dist/internal/virtualFileSystem/errors.d.ts +31 -0
- package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/errors.js +51 -0
- package/dist/internal/virtualFileSystem/fixture.d.ts +63 -0
- package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/fixture.js +144 -0
- package/dist/internal/{content.d.ts → virtualFileSystem/overlayContent.d.ts} +4 -14
- package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/overlayContent.js +29 -0
- package/dist/internal/{overlayChanges.d.ts → virtualFileSystem/overlayDiff.d.ts} +3 -12
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/overlayDiff.js +120 -0
- package/dist/internal/virtualFileSystem/path.d.ts +44 -0
- package/dist/internal/virtualFileSystem/path.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/path.js +127 -0
- package/dist/internal/virtualFileSystem/testHooks.d.ts +20 -0
- package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/testHooks.js +23 -0
- package/dist/internal/virtualFileSystem/watchHub.d.ts +16 -0
- package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem/watchHub.js +33 -0
- package/dist/internal/virtualFileSystem.d.ts +366 -0
- package/dist/internal/virtualFileSystem.d.ts.map +1 -0
- package/dist/internal/virtualFileSystem.js +1755 -0
- package/package.json +15 -5
- package/dist/internal/content.d.ts.map +0 -1
- package/dist/internal/content.js +0 -26
- package/dist/internal/overlayChanges.d.ts.map +0 -1
- package/dist/internal/overlayChanges.js +0 -146
- package/dist/internal/overlayTesting.d.ts +0 -12
- package/dist/internal/overlayTesting.d.ts.map +0 -1
- package/dist/internal/overlayTesting.js +0 -12
|
@@ -8,228 +8,315 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @since 0.1.0
|
|
10
10
|
*/
|
|
11
|
-
import * as ByteSize from "effect/ByteSize";
|
|
12
|
-
import * as Clock from "effect/Clock";
|
|
13
|
-
import * as Context from "effect/Context";
|
|
14
|
-
import { DecodeLimits, ImageError } from "./Snapshot.js";
|
|
15
|
-
export { DecodeLimits, ImageError, SnapshotTypeId } from "./Snapshot.js";
|
|
16
|
-
import * as Data from "effect/Data";
|
|
17
11
|
import * as Effect from "effect/Effect";
|
|
18
|
-
import * as Encoding from "effect/Encoding";
|
|
19
|
-
import * as PubSub from "effect/PubSub";
|
|
20
12
|
import * as Result from "effect/Result";
|
|
21
13
|
import * as Schema from "effect/Schema";
|
|
22
14
|
import * as SchemaIssue from "effect/SchemaIssue";
|
|
23
15
|
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
24
|
-
import * as Scope from "effect/Scope";
|
|
25
|
-
import * as Semaphore from "effect/Semaphore";
|
|
26
|
-
import * as Stream from "effect/Stream";
|
|
27
|
-
import { BytePath, getBytes as getBytePathBytes, make as makeBytePath } from "./BytePath.js";
|
|
28
16
|
export { BytePath } from "./BytePath.js";
|
|
29
|
-
|
|
17
|
+
export { DecodeLimits, ImageError, SnapshotTypeId } from "./Snapshot.js";
|
|
30
18
|
import * as Image from "./internal/image.js";
|
|
31
|
-
import { compareOverlay } from "./internal/overlayChanges.js";
|
|
32
|
-
import * as OverlayTesting from "./internal/overlayTesting.js";
|
|
33
19
|
import * as SnapshotDeltaInternal from "./internal/snapshotDelta.js";
|
|
20
|
+
import * as VfsModel from "./internal/virtualFileSystem.js";
|
|
21
|
+
import { decodeConfiguration } from "./internal/virtualFileSystem/errors.js";
|
|
22
|
+
import * as FixtureInternal from "./internal/virtualFileSystem/fixture.js";
|
|
23
|
+
import * as Path from "./internal/virtualFileSystem/path.js";
|
|
34
24
|
import * as SnapshotDeltaModel from "./SnapshotDelta.js";
|
|
35
25
|
export { SnapshotChange, SnapshotChangesOptions, SnapshotDeltaError, SnapshotDeltaLimits, SnapshotDeltaTypeId, SnapshotDifference, SnapshotNodeKind } from "./SnapshotDelta.js";
|
|
36
|
-
const VolumeId =
|
|
37
|
-
const CallerId =
|
|
38
|
-
const FileHandleId =
|
|
39
|
-
const DirectoryHandleId =
|
|
26
|
+
const VolumeId = VfsModel.VolumeId;
|
|
27
|
+
const CallerId = VfsModel.CallerId;
|
|
28
|
+
const FileHandleId = VfsModel.FileHandleId;
|
|
29
|
+
const DirectoryHandleId = VfsModel.DirectoryHandleId;
|
|
30
|
+
const ObjectReferenceId = VfsModel.ObjectReferenceId;
|
|
40
31
|
/**
|
|
41
32
|
* Schema for portable virtual filesystem error codes.
|
|
42
33
|
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
37
|
+
* import { Effect, Schema } from "effect"
|
|
38
|
+
*
|
|
39
|
+
* const asCode = Schema.decodeUnknownEffect(Vfs.FsCode)
|
|
40
|
+
*
|
|
41
|
+
* // Useful when a code has crossed a process boundary and arrives back as data.
|
|
42
|
+
* const program = Effect.gen(function*() {
|
|
43
|
+
* const payload = JSON.parse(`{"code":"NotFound","other":"Nonsense"}`)
|
|
44
|
+
*
|
|
45
|
+
* const known = yield* asCode(payload.code)
|
|
46
|
+
*
|
|
47
|
+
* const unknown = yield* asCode(payload.other).pipe(
|
|
48
|
+
* Effect.orElseSucceed(() => "not a code")
|
|
49
|
+
* )
|
|
50
|
+
*
|
|
51
|
+
* return [known, unknown]
|
|
52
|
+
* })
|
|
53
|
+
*
|
|
54
|
+
* Effect.runPromise(program).then(console.log)
|
|
55
|
+
* // [ 'NotFound', 'not a code' ]
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
43
58
|
* @category schemas
|
|
44
59
|
* @since 0.1.0
|
|
45
60
|
*/
|
|
46
|
-
export const FsCode =
|
|
47
|
-
"NotFound",
|
|
48
|
-
"AlreadyExists",
|
|
49
|
-
"NotEmpty",
|
|
50
|
-
"NotDirectory",
|
|
51
|
-
"AccessDenied",
|
|
52
|
-
"InvalidHandle",
|
|
53
|
-
"ForeignHandle",
|
|
54
|
-
"ClosedCaller",
|
|
55
|
-
"InvalidArgument",
|
|
56
|
-
"InvalidPathEncoding",
|
|
57
|
-
"PathTooLong",
|
|
58
|
-
"NoSpace",
|
|
59
|
-
"IsDirectory",
|
|
60
|
-
"FileTooLarge",
|
|
61
|
-
"NoData",
|
|
62
|
-
"SymlinkLoop",
|
|
63
|
-
"UnrepresentableName"
|
|
64
|
-
]);
|
|
61
|
+
export const FsCode = VfsModel.FsCode;
|
|
65
62
|
/**
|
|
66
63
|
* Describes an expected filesystem operation failure.
|
|
67
64
|
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
68
|
+
* import { Effect } from "effect"
|
|
69
|
+
*
|
|
70
|
+
* const program = Effect.gen(function*() {
|
|
71
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
72
|
+
*
|
|
73
|
+
* // Every filesystem operation fails with this one tagged error; `code`
|
|
74
|
+
* // distinguishes the cases, so match on it rather than on the tag alone.
|
|
75
|
+
* return yield* caller.readFile("/missing").pipe(
|
|
76
|
+
* Effect.catchTag("FsError", (error) =>
|
|
77
|
+
* error.code === "NotFound"
|
|
78
|
+
* ? Effect.succeed(new Uint8Array())
|
|
79
|
+
* : Effect.fail(error))
|
|
80
|
+
* )
|
|
81
|
+
* })
|
|
82
|
+
*
|
|
83
|
+
* Effect.runPromise(program).then(console.log)
|
|
84
|
+
* // Uint8Array(0) []
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
68
87
|
* @category errors
|
|
69
88
|
* @since 0.1.0
|
|
70
89
|
*/
|
|
71
|
-
export
|
|
72
|
-
}
|
|
90
|
+
export const FsError = VfsModel.FsError;
|
|
73
91
|
/**
|
|
74
92
|
* Describes an invalid volume or caller option and names the rejected field.
|
|
75
93
|
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
97
|
+
* import { Effect } from "effect"
|
|
98
|
+
*
|
|
99
|
+
* // `field` names the rejected option, so the caller learns which key was wrong.
|
|
100
|
+
* const program = Effect.gen(function*() {
|
|
101
|
+
* // Options that arrived from a config file or CLI flag, not a literal.
|
|
102
|
+
* const options = JSON.parse(`{"maxEntries":-1}`) as Vfs.VolumeOptions
|
|
103
|
+
*
|
|
104
|
+
* return yield* Vfs.make(options).pipe(
|
|
105
|
+
* Effect.as("ok"),
|
|
106
|
+
* Effect.catchTag("ConfigurationError", (error) => Effect.succeed(`rejected ${error.field}`))
|
|
107
|
+
* )
|
|
108
|
+
* })
|
|
109
|
+
*
|
|
110
|
+
* Effect.runPromise(program).then(console.log)
|
|
111
|
+
* // rejected maxEntries
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
76
114
|
* @category errors
|
|
77
115
|
* @since 0.1.0
|
|
78
116
|
*/
|
|
79
|
-
export
|
|
80
|
-
}
|
|
81
|
-
const Natural = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
|
|
82
|
-
const Mode = Natural.check(Schema.isLessThanOrEqualTo(0o7777));
|
|
117
|
+
export const ConfigurationError = VfsModel.ConfigurationError;
|
|
83
118
|
/**
|
|
84
119
|
* Schema for a caller's numeric identity, supplementary groups, and explicit privilege.
|
|
85
120
|
*
|
|
86
121
|
* @category schemas
|
|
87
122
|
* @since 0.1.0
|
|
88
123
|
*/
|
|
89
|
-
export const Identity =
|
|
90
|
-
/** Numeric user identifier used by ownership and permission checks. */
|
|
91
|
-
uid: Natural,
|
|
92
|
-
/** Primary numeric group identifier. */
|
|
93
|
-
gid: Natural,
|
|
94
|
-
/** Supplementary group identifiers used by group permission checks. */
|
|
95
|
-
groups: Schema.Array(Natural),
|
|
96
|
-
/** Grants root-style permission bypasses independently of `uid`. */
|
|
97
|
-
privileged: Schema.Boolean
|
|
98
|
-
});
|
|
124
|
+
export const Identity = VfsModel.Identity;
|
|
99
125
|
/**
|
|
100
126
|
* Schema for root caller credentials and creation mask.
|
|
101
127
|
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
131
|
+
* import { Effect } from "effect"
|
|
132
|
+
*
|
|
133
|
+
* const program = Effect.gen(function*() {
|
|
134
|
+
* const volume = yield* Vfs.make()
|
|
135
|
+
*
|
|
136
|
+
* // Omitting `identity` yields a privileged root caller.
|
|
137
|
+
* const root = yield* volume.caller({ umask: 0 })
|
|
138
|
+
* const user = yield* volume.caller({
|
|
139
|
+
* identity: { uid: 1000, gid: 1000, groups: [], privileged: false },
|
|
140
|
+
* umask: 0o022
|
|
141
|
+
* })
|
|
142
|
+
*
|
|
143
|
+
* yield* root.mkdir("/home", { mode: 0o777 })
|
|
144
|
+
*
|
|
145
|
+
* // The umask clears its bits from the requested mode.
|
|
146
|
+
* yield* user.mkdir("/home/user", { mode: 0o777 })
|
|
147
|
+
*
|
|
148
|
+
* return ((yield* user.stat("/home/user")).mode & 0o777).toString(8)
|
|
149
|
+
* })
|
|
150
|
+
*
|
|
151
|
+
* Effect.runPromise(program).then(console.log)
|
|
152
|
+
* // 755
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
102
155
|
* @category schemas
|
|
103
156
|
* @since 0.1.0
|
|
104
157
|
*/
|
|
105
|
-
export const RootCallerOptions =
|
|
106
|
-
/** Caller identity. Defaults to privileged uid and gid `0`. */
|
|
107
|
-
identity: Schema.optionalKey(Identity),
|
|
108
|
-
/** Creation mask applied to requested modes. Defaults to `0o022`. */
|
|
109
|
-
umask: Schema.optionalKey(Natural.check(Schema.isLessThanOrEqualTo(0o777)))
|
|
110
|
-
});
|
|
158
|
+
export const RootCallerOptions = VfsModel.RootCallerOptions;
|
|
111
159
|
/**
|
|
112
160
|
* Schema for optional volume capacity and path limits.
|
|
113
161
|
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
165
|
+
* import { ByteSize, Effect } from "effect"
|
|
166
|
+
*
|
|
167
|
+
* // Every limit is optional, and each one is enforced once set.
|
|
168
|
+
* const program = Effect.gen(function*() {
|
|
169
|
+
* const volume = yield* Vfs.make({ maxFileBytes: ByteSize.bytes(4) })
|
|
170
|
+
* const caller = yield* volume.caller()
|
|
171
|
+
*
|
|
172
|
+
* return yield* caller.writeFile("/big.bin", new Uint8Array(16), {
|
|
173
|
+
* access: "write",
|
|
174
|
+
* create: "exclusive"
|
|
175
|
+
* }).pipe(
|
|
176
|
+
* Effect.as("written"),
|
|
177
|
+
* Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
|
|
178
|
+
* )
|
|
179
|
+
* })
|
|
180
|
+
*
|
|
181
|
+
* Effect.runPromise(program).then(console.log)
|
|
182
|
+
* // FileTooLarge
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
114
185
|
* @category schemas
|
|
115
186
|
* @since 0.1.0
|
|
116
187
|
*/
|
|
117
|
-
export const VolumeOptions =
|
|
118
|
-
/** Maximum number of filesystem nodes. Omission leaves the count unbounded. */
|
|
119
|
-
maxEntries: Schema.optionalKey(Natural),
|
|
120
|
-
/** Maximum combined regular-file content in bytes. */
|
|
121
|
-
maxBytes: Schema.optionalKey(Schema.ByteSize),
|
|
122
|
-
/** Maximum content size of one regular file in bytes. */
|
|
123
|
-
maxFileBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isLessThanOrEqualTo(size, ByteSize.bytes(0xffffffff)) ? undefined : "must be at most 4294967295 bytes"))),
|
|
124
|
-
/** Maximum encoded byte length of an absolute or relative path. */
|
|
125
|
-
maxPathBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isGreaterThanOrEqualTo(size, ByteSize.bytes(1)) ? undefined : "must be at least 1 byte")))
|
|
126
|
-
});
|
|
127
|
-
// Match snapshot v1's canonical signed decimal timestamp domain.
|
|
128
|
-
const timestampLimit = 10n ** 128n - 1n;
|
|
129
|
-
const Timestamp = Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(-timestampLimit), Schema.isLessThanOrEqualToBigInt(timestampLimit));
|
|
188
|
+
export const VolumeOptions = VfsModel.VolumeOptions;
|
|
130
189
|
/**
|
|
131
190
|
* Schema for filesystem node metadata with bigint inode, size, and nanosecond fields.
|
|
132
191
|
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
195
|
+
* import { Effect } from "effect"
|
|
196
|
+
*
|
|
197
|
+
* // Sizes and inode numbers are bigint; times are nanosecond timestamps.
|
|
198
|
+
* const program = Effect.gen(function*() {
|
|
199
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
200
|
+
*
|
|
201
|
+
* yield* caller.writeFile("/f", new Uint8Array([1, 2, 3]), {
|
|
202
|
+
* access: "write",
|
|
203
|
+
* create: "exclusive"
|
|
204
|
+
* })
|
|
205
|
+
*
|
|
206
|
+
* const metadata = yield* caller.stat("/f")
|
|
207
|
+
*
|
|
208
|
+
* return [metadata.kind, metadata.size, metadata.nlink]
|
|
209
|
+
* })
|
|
210
|
+
*
|
|
211
|
+
* Effect.runPromise(program).then(console.log)
|
|
212
|
+
* // [ 'file', 3n, 1 ]
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
133
215
|
* @category schemas
|
|
134
216
|
* @since 0.1.0
|
|
135
217
|
*/
|
|
136
|
-
export const Metadata =
|
|
137
|
-
kind: Schema.Literals(["directory", "file", "symlink"]),
|
|
138
|
-
ino: Schema.BigInt,
|
|
139
|
-
nlink: Natural,
|
|
140
|
-
size: Schema.BigInt,
|
|
141
|
-
uid: Natural,
|
|
142
|
-
gid: Natural,
|
|
143
|
-
mode: Mode,
|
|
144
|
-
atimeNs: Timestamp,
|
|
145
|
-
mtimeNs: Timestamp,
|
|
146
|
-
ctimeNs: Timestamp,
|
|
147
|
-
birthtimeNs: Timestamp
|
|
148
|
-
});
|
|
218
|
+
export const Metadata = VfsModel.Metadata;
|
|
149
219
|
/**
|
|
150
220
|
* Schema for an owner update. Omitted fields retain their existing values.
|
|
151
221
|
*
|
|
152
222
|
* @category schemas
|
|
153
223
|
* @since 0.1.0
|
|
154
224
|
*/
|
|
155
|
-
export const OwnerUpdate =
|
|
225
|
+
export const OwnerUpdate = VfsModel.OwnerUpdate;
|
|
156
226
|
/**
|
|
157
227
|
* Schema for setting a timestamp to the clock, retaining it, or supplying nanoseconds.
|
|
158
228
|
*
|
|
159
229
|
* @category schemas
|
|
160
230
|
* @since 0.1.0
|
|
161
231
|
*/
|
|
162
|
-
export const TimeUpdate =
|
|
163
|
-
Schema.Struct({ kind: Schema.Literal("now") }),
|
|
164
|
-
Schema.Struct({ kind: Schema.Literal("omit") }),
|
|
165
|
-
Schema.Struct({ kind: Schema.Literal("value"), nanoseconds: Timestamp })
|
|
166
|
-
]);
|
|
232
|
+
export const TimeUpdate = VfsModel.TimeUpdate;
|
|
167
233
|
/**
|
|
168
234
|
* Schema for independent access and modification time updates.
|
|
169
235
|
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
239
|
+
* import { Effect } from "effect"
|
|
240
|
+
*
|
|
241
|
+
* const program = Effect.gen(function*() {
|
|
242
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
243
|
+
*
|
|
244
|
+
* yield* caller.writeFile("/f", new Uint8Array([1]), {
|
|
245
|
+
* access: "write",
|
|
246
|
+
* create: "exclusive"
|
|
247
|
+
* })
|
|
248
|
+
*
|
|
249
|
+
* // Each field is set to a value, to the current clock, or left alone.
|
|
250
|
+
* yield* caller.utimes("/f", {
|
|
251
|
+
* access: { kind: "value", nanoseconds: 1_000n },
|
|
252
|
+
* modification: { kind: "omit" }
|
|
253
|
+
* })
|
|
254
|
+
*
|
|
255
|
+
* const metadata = yield* caller.stat("/f")
|
|
256
|
+
*
|
|
257
|
+
* return [metadata.atimeNs, metadata.mtimeNs === 1_000n]
|
|
258
|
+
* })
|
|
259
|
+
*
|
|
260
|
+
* Effect.runPromise(program).then(console.log)
|
|
261
|
+
* // [ 1000n, false ]
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
170
264
|
* @category schemas
|
|
171
265
|
* @since 0.1.0
|
|
172
266
|
*/
|
|
173
|
-
export const Times =
|
|
267
|
+
export const Times = VfsModel.Times;
|
|
174
268
|
/**
|
|
175
269
|
* Schema for file seek origins, including dense-file data and hole queries.
|
|
176
270
|
*
|
|
177
271
|
* @category schemas
|
|
178
272
|
* @since 0.1.0
|
|
179
273
|
*/
|
|
180
|
-
export const SeekMode =
|
|
274
|
+
export const SeekMode = VfsModel.SeekMode;
|
|
181
275
|
/**
|
|
182
276
|
* Schema for file access, creation, append, truncate, and symlink behavior.
|
|
183
277
|
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```ts
|
|
280
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
281
|
+
* import { Effect } from "effect"
|
|
282
|
+
*
|
|
283
|
+
* // `access` is required; `create` decides whether a missing file is an error.
|
|
284
|
+
* const program = Effect.gen(function*() {
|
|
285
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
286
|
+
*
|
|
287
|
+
* const handle = yield* caller.open("/log.txt", {
|
|
288
|
+
* access: "readWrite",
|
|
289
|
+
* create: "ifMissing",
|
|
290
|
+
* append: true
|
|
291
|
+
* })
|
|
292
|
+
*
|
|
293
|
+
* yield* handle.write(new Uint8Array([1, 2]))
|
|
294
|
+
*
|
|
295
|
+
* return (yield* handle.stat).size
|
|
296
|
+
* }).pipe(Effect.scoped)
|
|
297
|
+
*
|
|
298
|
+
* Effect.runPromise(program).then(console.log)
|
|
299
|
+
* // 2n
|
|
300
|
+
* ```
|
|
301
|
+
*
|
|
184
302
|
* @category schemas
|
|
185
303
|
* @since 0.1.0
|
|
186
304
|
*/
|
|
187
|
-
export const OpenSettings =
|
|
188
|
-
/** Permitted operations on the returned handle. */
|
|
189
|
-
access: Schema.Literals(["read", "write", "readWrite"]),
|
|
190
|
-
/** Creation policy. Defaults to `"never"`. */
|
|
191
|
-
create: Schema.optionalKey(Schema.Literals(["never", "ifMissing", "exclusive"])),
|
|
192
|
-
/** Requested mode for a new file, before applying the caller's umask. */
|
|
193
|
-
mode: Schema.optionalKey(Mode),
|
|
194
|
-
/** Write at the current end of file regardless of the handle cursor. */
|
|
195
|
-
append: Schema.optionalKey(Schema.Boolean),
|
|
196
|
-
/** Truncate an existing regular file to zero bytes during open. */
|
|
197
|
-
truncate: Schema.optionalKey(Schema.Boolean),
|
|
198
|
-
/** Follow the final symbolic link. Defaults to `true`. */
|
|
199
|
-
followFinalSymlink: Schema.optionalKey(Schema.Boolean)
|
|
200
|
-
});
|
|
201
|
-
const WriteFileSettings = Schema.Struct({
|
|
202
|
-
...OpenSettings.fields,
|
|
203
|
-
/** Replace the final symbolic link itself instead of its target. */
|
|
204
|
-
replaceFinalSymlink: Schema.optionalKey(Schema.Boolean),
|
|
205
|
-
/** Mode to apply after replacing an existing file. */
|
|
206
|
-
finalMode: Schema.optionalKey(Mode)
|
|
207
|
-
});
|
|
305
|
+
export const OpenSettings = VfsModel.OpenSettings;
|
|
208
306
|
/**
|
|
209
307
|
* Schema for the filesystem entry kinds reported by overlay summaries.
|
|
210
308
|
*
|
|
211
309
|
* @category schemas
|
|
212
310
|
* @since 0.1.0
|
|
213
311
|
*/
|
|
214
|
-
export const OverlayNodeKind =
|
|
312
|
+
export const OverlayNodeKind = VfsModel.OverlayNodeKind;
|
|
215
313
|
/**
|
|
216
314
|
* Schema for observable fields that can differ from an overlay's immutable base.
|
|
217
315
|
*
|
|
218
316
|
* @category schemas
|
|
219
317
|
* @since 0.1.0
|
|
220
318
|
*/
|
|
221
|
-
export const OverlayDifference =
|
|
222
|
-
"content",
|
|
223
|
-
"mode",
|
|
224
|
-
"uid",
|
|
225
|
-
"gid",
|
|
226
|
-
"atimeNs",
|
|
227
|
-
"mtimeNs",
|
|
228
|
-
"ctimeNs",
|
|
229
|
-
"birthtimeNs"
|
|
230
|
-
]);
|
|
231
|
-
const OverlayDifferences = Schema.Array(OverlayDifference);
|
|
232
|
-
const NonEmptyOverlayDifferences = OverlayDifferences.check(Schema.isMinLength(1));
|
|
319
|
+
export const OverlayDifference = VfsModel.OverlayDifference;
|
|
233
320
|
/**
|
|
234
321
|
* Schema for a final-state overlay difference.
|
|
235
322
|
*
|
|
@@ -241,44 +328,101 @@ const NonEmptyOverlayDifferences = OverlayDifferences.check(Schema.isMinLength(1
|
|
|
241
328
|
* @category schemas
|
|
242
329
|
* @since 0.1.0
|
|
243
330
|
*/
|
|
244
|
-
export const OverlayChange =
|
|
245
|
-
Schema.TaggedStruct("Added", { path: BytePath, kind: OverlayNodeKind }),
|
|
246
|
-
Schema.TaggedStruct("Removed", { path: BytePath, kind: OverlayNodeKind }),
|
|
247
|
-
Schema.TaggedStruct("Replaced", {
|
|
248
|
-
path: BytePath,
|
|
249
|
-
beforeKind: OverlayNodeKind,
|
|
250
|
-
afterKind: OverlayNodeKind,
|
|
251
|
-
differences: OverlayDifferences
|
|
252
|
-
}),
|
|
253
|
-
Schema.TaggedStruct("Renamed", {
|
|
254
|
-
from: BytePath,
|
|
255
|
-
to: BytePath,
|
|
256
|
-
kind: OverlayNodeKind,
|
|
257
|
-
differences: OverlayDifferences
|
|
258
|
-
}),
|
|
259
|
-
Schema.TaggedStruct("Updated", { path: BytePath, kind: OverlayNodeKind, differences: NonEmptyOverlayDifferences })
|
|
260
|
-
]);
|
|
331
|
+
export const OverlayChange = VfsModel.OverlayChange;
|
|
261
332
|
/**
|
|
262
333
|
* Schema for overlay summary filtering.
|
|
263
334
|
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
338
|
+
* import { Effect } from "effect"
|
|
339
|
+
*
|
|
340
|
+
* const program = Effect.gen(function*() {
|
|
341
|
+
* const base = yield* Vfs.make()
|
|
342
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
343
|
+
*
|
|
344
|
+
* yield* (yield* workspace.caller()).mkdir("/out")
|
|
345
|
+
*
|
|
346
|
+
* // Timestamps are excluded by default, since they change on every write.
|
|
347
|
+
* const plain = yield* workspace.changes()
|
|
348
|
+
* const timed = yield* workspace.changes({ includeTimestamps: true })
|
|
349
|
+
*
|
|
350
|
+
* return [plain.length, timed.length]
|
|
351
|
+
* })
|
|
352
|
+
*
|
|
353
|
+
* Effect.runPromise(program).then(console.log)
|
|
354
|
+
* // [ 1, 2 ]
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
264
357
|
* @category schemas
|
|
265
358
|
* @since 0.1.0
|
|
266
359
|
*/
|
|
267
|
-
export const OverlayChangesOptions =
|
|
268
|
-
/** Include access, modification, change, and birth-time differences. Defaults to `false`. */
|
|
269
|
-
includeTimestamps: Schema.optionalKey(Schema.Boolean)
|
|
270
|
-
});
|
|
360
|
+
export const OverlayChangesOptions = VfsModel.OverlayChangesOptions;
|
|
271
361
|
/**
|
|
272
362
|
* Optional Effect service for providing an existing filesystem caller.
|
|
273
363
|
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```ts
|
|
366
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
367
|
+
* import { Effect } from "effect"
|
|
368
|
+
*
|
|
369
|
+
* // Code depending on the service does not choose the volume it runs against.
|
|
370
|
+
* const readVersion = Effect.gen(function*() {
|
|
371
|
+
* const caller = yield* Vfs.CurrentFileSystem
|
|
372
|
+
*
|
|
373
|
+
* return yield* caller.readFile("/version")
|
|
374
|
+
* })
|
|
375
|
+
*
|
|
376
|
+
* const program = Effect.gen(function*() {
|
|
377
|
+
* const volume = yield* Vfs.make()
|
|
378
|
+
* const caller = yield* volume.caller()
|
|
379
|
+
*
|
|
380
|
+
* yield* caller.writeFile("/version", new Uint8Array([49]), {
|
|
381
|
+
* access: "write",
|
|
382
|
+
* create: "exclusive"
|
|
383
|
+
* })
|
|
384
|
+
*
|
|
385
|
+
* return yield* readVersion.pipe(Effect.provideService(Vfs.CurrentFileSystem, caller))
|
|
386
|
+
* })
|
|
387
|
+
*
|
|
388
|
+
* Effect.runPromise(program).then(console.log)
|
|
389
|
+
* // Uint8Array(1) [ 49 ]
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @see The testing guide at `/guides/testing-with-an-isolated-filesystem`.
|
|
274
393
|
* @category services
|
|
275
394
|
* @since 0.1.0
|
|
276
395
|
*/
|
|
277
|
-
export
|
|
278
|
-
}
|
|
396
|
+
export const CurrentFileSystem = VfsModel.CurrentFileSystem;
|
|
279
397
|
/**
|
|
280
398
|
* Encodes a snapshot as owned UTF-8 JSON bytes using the version 1 snapshot format.
|
|
281
399
|
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
403
|
+
* import { Effect } from "effect"
|
|
404
|
+
*
|
|
405
|
+
* // Bytes are portable: store or transmit them, then decode under explicit limits.
|
|
406
|
+
* const program = Effect.gen(function*() {
|
|
407
|
+
* const volume = yield* Vfs.make()
|
|
408
|
+
* const caller = yield* volume.caller()
|
|
409
|
+
*
|
|
410
|
+
* yield* caller.writeFile("/f", new Uint8Array([1, 2, 3]), {
|
|
411
|
+
* access: "write",
|
|
412
|
+
* create: "exclusive"
|
|
413
|
+
* })
|
|
414
|
+
*
|
|
415
|
+
* const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
|
|
416
|
+
*
|
|
417
|
+
* // The bytes are UTF-8 JSON carrying the version 1 snapshot format.
|
|
418
|
+
* return JSON.parse(new TextDecoder().decode(bytes)).version
|
|
419
|
+
* })
|
|
420
|
+
*
|
|
421
|
+
* Effect.runPromise(program).then(console.log)
|
|
422
|
+
* // 1
|
|
423
|
+
* ```
|
|
424
|
+
*
|
|
425
|
+
* @see {@link decodeSnapshot} for reading the bytes back under limits.
|
|
282
426
|
* @category serialization
|
|
283
427
|
* @since 0.1.0
|
|
284
428
|
*/
|
|
@@ -286,6 +430,32 @@ export const encodeSnapshot = Image.encodeSnapshot;
|
|
|
286
430
|
/**
|
|
287
431
|
* Decodes version 1 snapshot bytes while enforcing explicit input and payload limits.
|
|
288
432
|
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```ts
|
|
435
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
436
|
+
* import { ByteSize, Effect } from "effect"
|
|
437
|
+
*
|
|
438
|
+
* // Limits are mandatory: decoding is the boundary where untrusted input arrives.
|
|
439
|
+
* const program = Effect.gen(function*() {
|
|
440
|
+
* const volume = yield* Vfs.make()
|
|
441
|
+
* const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
|
|
442
|
+
*
|
|
443
|
+
* return yield* Vfs.decodeSnapshot(bytes, {
|
|
444
|
+
* maxEncodedBytes: ByteSize.bytes(1),
|
|
445
|
+
* maxRecords: 10_000,
|
|
446
|
+
* maxEntries: 10_000,
|
|
447
|
+
* maxDecodedBytes: ByteSize.megabytes(16)
|
|
448
|
+
* }).pipe(
|
|
449
|
+
* Effect.as("accepted"),
|
|
450
|
+
* Effect.catchTag("ImageError", (error) => Effect.succeed([error.code, error.field]))
|
|
451
|
+
* )
|
|
452
|
+
* })
|
|
453
|
+
*
|
|
454
|
+
* Effect.runPromise(program).then(console.log)
|
|
455
|
+
* // [ 'LimitExceeded', 'encodedBytes' ]
|
|
456
|
+
* ```
|
|
457
|
+
*
|
|
458
|
+
* @see {@link DecodeLimits} for what each bound protects.
|
|
289
459
|
* @category serialization
|
|
290
460
|
* @since 0.1.0
|
|
291
461
|
*/
|
|
@@ -296,8 +466,38 @@ const deltaLimits = (limits) => {
|
|
|
296
466
|
};
|
|
297
467
|
/**
|
|
298
468
|
* Computes an exact portable delta between two immutable snapshots.
|
|
299
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
469
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
470
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```ts
|
|
474
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
475
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
476
|
+
* import { Effect } from "effect"
|
|
477
|
+
*
|
|
478
|
+
* // Two snapshots in, one opaque delta out. `inspectSnapshotDelta` reads it.
|
|
479
|
+
* const program = Effect.gen(function*() {
|
|
480
|
+
* const volume = yield* Vfs.make()
|
|
481
|
+
* const caller = yield* volume.caller()
|
|
482
|
+
* const base = yield* volume.snapshot
|
|
483
|
+
*
|
|
484
|
+
* yield* caller.writeFile("/added.txt", new Uint8Array([1]), {
|
|
485
|
+
* access: "write",
|
|
486
|
+
* create: "exclusive"
|
|
487
|
+
* })
|
|
300
488
|
*
|
|
489
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
490
|
+
*
|
|
491
|
+
* const changes = yield* Vfs.inspectSnapshotDelta(base, delta)
|
|
492
|
+
*
|
|
493
|
+
* return new TextDecoder().decode(yield* Vfs.pathToBytes(changes[0]!.path))
|
|
494
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
495
|
+
*
|
|
496
|
+
* Effect.runPromise(program).then(console.log)
|
|
497
|
+
* // /added.txt
|
|
498
|
+
* ```
|
|
499
|
+
*
|
|
500
|
+
* @see {@link inspectSnapshotDelta} to read a delta, {@link applySnapshotDelta} to apply one.
|
|
301
501
|
* @category snapshots
|
|
302
502
|
* @since 0.1.0
|
|
303
503
|
*/
|
|
@@ -306,7 +506,37 @@ export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(functi
|
|
|
306
506
|
});
|
|
307
507
|
/**
|
|
308
508
|
* Verifies an exact snapshot delta against its base and derives an owned path-oriented summary.
|
|
309
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
509
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
510
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```ts
|
|
514
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
515
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
516
|
+
* import { Effect } from "effect"
|
|
517
|
+
*
|
|
518
|
+
* // Inspection verifies the delta against its base before describing it.
|
|
519
|
+
* const program = Effect.gen(function*() {
|
|
520
|
+
* const volume = yield* Vfs.make()
|
|
521
|
+
* const caller = yield* volume.caller()
|
|
522
|
+
* const base = yield* volume.snapshot
|
|
523
|
+
*
|
|
524
|
+
* yield* caller.mkdir("/logs")
|
|
525
|
+
*
|
|
526
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
527
|
+
* const changes = yield* Vfs.inspectSnapshotDelta(base, delta)
|
|
528
|
+
*
|
|
529
|
+
* return changes.map((change) =>
|
|
530
|
+
* change._tag === "Updated" ? change.afterKind : change.kind)
|
|
531
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
532
|
+
*
|
|
533
|
+
* Effect.runPromise(program).then(console.log)
|
|
534
|
+
* // [ 'directory' ]
|
|
535
|
+
* ```
|
|
536
|
+
*
|
|
537
|
+
* @see {@link SnapshotChangesOptions} for including timestamp differences.
|
|
538
|
+
* @category snapshots
|
|
539
|
+
* @since 0.1.0
|
|
310
540
|
*/
|
|
311
541
|
export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshotDelta")(function* (base, delta, options, limits) {
|
|
312
542
|
const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {});
|
|
@@ -316,8 +546,40 @@ export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshot
|
|
|
316
546
|
});
|
|
317
547
|
/**
|
|
318
548
|
* Applies an exact delta to its semantically matching base and returns a new snapshot.
|
|
319
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
549
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
550
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
555
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
556
|
+
* import { Effect } from "effect"
|
|
557
|
+
*
|
|
558
|
+
* const program = Effect.gen(function*() {
|
|
559
|
+
* const volume = yield* Vfs.make()
|
|
560
|
+
* const caller = yield* volume.caller()
|
|
561
|
+
* const base = yield* volume.snapshot
|
|
562
|
+
*
|
|
563
|
+
* yield* caller.writeFile("/f", new Uint8Array([7]), {
|
|
564
|
+
* access: "write",
|
|
565
|
+
* create: "exclusive"
|
|
566
|
+
* })
|
|
320
567
|
*
|
|
568
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
569
|
+
*
|
|
570
|
+
* // Applying the delta to the same base reproduces the target snapshot.
|
|
571
|
+
* const applied = yield* Vfs.applySnapshotDelta(base, delta)
|
|
572
|
+
* const restored = yield* Vfs.fromSnapshot(applied)
|
|
573
|
+
* const reader = yield* restored.caller()
|
|
574
|
+
*
|
|
575
|
+
* return yield* reader.readFile("/f")
|
|
576
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
577
|
+
*
|
|
578
|
+
* Effect.runPromise(program).then(console.log)
|
|
579
|
+
* // Uint8Array(1) [ 7 ]
|
|
580
|
+
* ```
|
|
581
|
+
*
|
|
582
|
+
* @see {@link SnapshotDeltaFromBytes} for moving a delta across a boundary.
|
|
321
583
|
* @category snapshots
|
|
322
584
|
* @since 0.1.0
|
|
323
585
|
*/
|
|
@@ -328,6 +590,37 @@ const deltaSchemaIssue = (cause, input, options) => new SchemaIssue.InvalidValue
|
|
|
328
590
|
/**
|
|
329
591
|
* Creates an Effect Schema codec between owned bytes and opaque snapshot deltas.
|
|
330
592
|
* Omission uses `SnapshotDeltaLimits.default`.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```ts
|
|
596
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
597
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
598
|
+
* import { Effect, Schema } from "effect"
|
|
599
|
+
*
|
|
600
|
+
* // Omitting limits uses `SnapshotDeltaLimits.default`.
|
|
601
|
+
* const codec = Vfs.SnapshotDeltaFromBytes()
|
|
602
|
+
*
|
|
603
|
+
* const program = Effect.gen(function*() {
|
|
604
|
+
* const volume = yield* Vfs.make()
|
|
605
|
+
* const base = yield* volume.snapshot
|
|
606
|
+
*
|
|
607
|
+
* yield* (yield* volume.caller()).mkdir("/out")
|
|
608
|
+
*
|
|
609
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
610
|
+
* const bytes = yield* Schema.encodeEffect(codec)(delta)
|
|
611
|
+
*
|
|
612
|
+
* // The delta survives the trip through bytes and still applies to its base.
|
|
613
|
+
* const decoded = yield* Schema.decodeEffect(codec)(bytes)
|
|
614
|
+
*
|
|
615
|
+
* return (yield* Vfs.inspectSnapshotDelta(base, decoded)).map((change) => change._tag)
|
|
616
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
617
|
+
*
|
|
618
|
+
* Effect.runPromise(program).then(console.log)
|
|
619
|
+
* // [ 'Added' ]
|
|
620
|
+
* ```
|
|
621
|
+
*
|
|
622
|
+
* @category schemas
|
|
623
|
+
* @since 0.1.0
|
|
331
624
|
*/
|
|
332
625
|
export const SnapshotDeltaFromBytes = (limits) => {
|
|
333
626
|
const selected = Schema.decodeResult(SnapshotDeltaModel.SnapshotDeltaLimits, { onExcessProperty: "error" })(limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
|
|
@@ -340,29 +633,6 @@ export const SnapshotDeltaFromBytes = (limits) => {
|
|
|
340
633
|
: SnapshotDeltaInternal.encodeSnapshotDelta(delta, selected.success).pipe(Effect.mapError((cause) => deltaSchemaIssue(cause, delta, options)))
|
|
341
634
|
})));
|
|
342
635
|
};
|
|
343
|
-
const failure = (code, operation, path) => new FsError({ code, operation, ...(path === undefined ? {} : { path }) });
|
|
344
|
-
const ownedPath = (bytes) => {
|
|
345
|
-
return makeBytePath(bytes);
|
|
346
|
-
};
|
|
347
|
-
const strictString = (bytes, operation) => Effect.try({
|
|
348
|
-
try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes),
|
|
349
|
-
catch: () => failure("UnrepresentableName", operation)
|
|
350
|
-
});
|
|
351
|
-
const nameBytes = (name) => {
|
|
352
|
-
return Result.getOrThrow(Encoding.decodeHex(name));
|
|
353
|
-
};
|
|
354
|
-
// A zero-length view distinguishes a detached buffer from a valid empty buffer.
|
|
355
|
-
const attachedBuffer = (bytes) => {
|
|
356
|
-
try {
|
|
357
|
-
new Uint8Array(bytes.buffer, bytes.byteOffset, 0);
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
catch (error) {
|
|
361
|
-
if (error instanceof TypeError)
|
|
362
|
-
return false;
|
|
363
|
-
throw error;
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
636
|
/**
|
|
367
637
|
* Creates an opaque byte path by copying the input when the Effect executes.
|
|
368
638
|
*
|
|
@@ -370,1412 +640,70 @@ const attachedBuffer = (bytes) => {
|
|
|
370
640
|
*
|
|
371
641
|
* Shared-memory-backed and detached views fail with `InvalidArgument`.
|
|
372
642
|
*
|
|
643
|
+
* @example
|
|
644
|
+
* ```ts
|
|
645
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
646
|
+
* import { Effect } from "effect"
|
|
647
|
+
*
|
|
648
|
+
* // Any byte except NUL is accepted. Empty, NUL-bearing, detached, and shared-memory
|
|
649
|
+
* // input all fail with `InvalidArgument`.
|
|
650
|
+
* const program = Effect.gen(function*() {
|
|
651
|
+
* const path = yield* Vfs.pathFromBytes(new Uint8Array([47, 116, 109, 112]))
|
|
652
|
+
*
|
|
653
|
+
* const rejected = yield* Vfs.pathFromBytes(new Uint8Array([47, 0])).pipe(
|
|
654
|
+
* Effect.as("accepted"),
|
|
655
|
+
* Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
|
|
656
|
+
* )
|
|
657
|
+
*
|
|
658
|
+
* return [yield* Vfs.pathToBytes(path), rejected]
|
|
659
|
+
* })
|
|
660
|
+
*
|
|
661
|
+
* Effect.runPromise(program).then(console.log)
|
|
662
|
+
* // [ Uint8Array(4) [ 47, 116, 109, 112 ], 'InvalidArgument' ]
|
|
663
|
+
* ```
|
|
664
|
+
*
|
|
373
665
|
* @category constructors
|
|
374
666
|
* @since 0.1.0
|
|
375
667
|
*/
|
|
376
|
-
export const pathFromBytes =
|
|
377
|
-
if (!(bytes instanceof Uint8Array) || !(bytes.buffer instanceof ArrayBuffer)) {
|
|
378
|
-
return yield* failure("InvalidArgument", "pathFromBytes");
|
|
379
|
-
}
|
|
380
|
-
if (!attachedBuffer(bytes))
|
|
381
|
-
return yield* failure("InvalidArgument", "pathFromBytes");
|
|
382
|
-
const owned = new Uint8Array(bytes);
|
|
383
|
-
if (owned.length === 0 || owned.includes(0))
|
|
384
|
-
return yield* failure("InvalidArgument", "pathFromBytes");
|
|
385
|
-
return makeBytePath(owned);
|
|
386
|
-
});
|
|
668
|
+
export const pathFromBytes = Path.pathFromBytes;
|
|
387
669
|
/**
|
|
388
670
|
* Copies the bytes held by an opaque byte path.
|
|
389
671
|
*
|
|
390
672
|
* @category getters
|
|
391
673
|
* @since 0.1.0
|
|
392
674
|
*/
|
|
393
|
-
export const pathToBytes =
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
return Result.fail(failure("InvalidArgument", operation));
|
|
426
|
-
if (bytes.length === 0)
|
|
427
|
-
return Result.fail(failure("NotFound", operation, input));
|
|
428
|
-
if (bytes.includes(0))
|
|
429
|
-
return Result.fail(failure("InvalidArgument", operation, input));
|
|
430
|
-
if (maxPathBytes !== undefined && ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxPathBytes)) {
|
|
431
|
-
return Result.fail(failure("PathTooLong", operation, input));
|
|
432
|
-
}
|
|
433
|
-
const components = [];
|
|
434
|
-
const suffixes = [];
|
|
435
|
-
let start = 0;
|
|
436
|
-
for (let index = 0; index <= bytes.length; index++) {
|
|
437
|
-
if (index !== bytes.length && bytes[index] !== 47)
|
|
438
|
-
continue;
|
|
439
|
-
if (index > start) {
|
|
440
|
-
// Provisional component bound from decision 0019; names are compared as bytes.
|
|
441
|
-
if (index - start > 255)
|
|
442
|
-
return Result.fail(failure("PathTooLong", operation, input));
|
|
443
|
-
components.push(Encoding.encodeHex(bytes.subarray(start, index)));
|
|
444
|
-
suffixes.push(bytes.subarray(index));
|
|
445
|
-
}
|
|
446
|
-
start = index + 1;
|
|
447
|
-
}
|
|
448
|
-
return Result.succeed({
|
|
449
|
-
input,
|
|
450
|
-
absolute: bytes[0] === 47,
|
|
451
|
-
trailingSlash: bytes.at(-1) === 47,
|
|
452
|
-
bytes,
|
|
453
|
-
suffixes,
|
|
454
|
-
components
|
|
455
|
-
});
|
|
456
|
-
};
|
|
457
|
-
const configurationField = (issue) => {
|
|
458
|
-
if (issue._tag === "Pointer")
|
|
459
|
-
return issue.path.map(String).join(".");
|
|
460
|
-
if (issue._tag === "Composite")
|
|
461
|
-
return configurationField(issue.issues[0]);
|
|
462
|
-
return "options";
|
|
463
|
-
};
|
|
464
|
-
const decodeConfiguration = (schema, value) => Schema.decodeUnknownResult(schema, { onExcessProperty: "error" })(value).pipe(Result.mapError((error) => new ConfigurationError({ field: configurationField(error.issue) })));
|
|
465
|
-
const directoryMetadata = (ino, uid, gid, mode, now) => ({
|
|
466
|
-
kind: "directory",
|
|
467
|
-
ino,
|
|
468
|
-
uid,
|
|
469
|
-
gid,
|
|
470
|
-
mode,
|
|
471
|
-
nlink: 2,
|
|
472
|
-
size: 0n,
|
|
473
|
-
atimeNs: now,
|
|
474
|
-
mtimeNs: now,
|
|
475
|
-
ctimeNs: now,
|
|
476
|
-
birthtimeNs: now
|
|
477
|
-
});
|
|
478
|
-
const storedMetadata = (metadata) => ({
|
|
479
|
-
uid: metadata.uid,
|
|
480
|
-
gid: metadata.gid,
|
|
481
|
-
mode: metadata.mode,
|
|
482
|
-
atimeNs: String(metadata.atimeNs),
|
|
483
|
-
mtimeNs: String(metadata.mtimeNs),
|
|
484
|
-
ctimeNs: String(metadata.ctimeNs),
|
|
485
|
-
birthtimeNs: String(metadata.birthtimeNs)
|
|
486
|
-
});
|
|
487
|
-
/** Each execution constructs a fresh volume and captures its Clock. */
|
|
488
|
-
const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (source, options) {
|
|
489
|
-
const image = source._tag === "Empty" ? undefined : source.image;
|
|
490
|
-
const decoded = decodeConfiguration(VolumeOptions, options === undefined ? {} : options);
|
|
491
|
-
if (Result.isFailure(decoded))
|
|
492
|
-
return yield* decoded.failure;
|
|
493
|
-
const settings = { ...decoded.success };
|
|
494
|
-
const clock = yield* Clock.clockWith(Effect.succeed);
|
|
495
|
-
const initialTime = clock.currentTimeNanosUnsafe();
|
|
496
|
-
if (!Schema.is(Timestamp)(initialTime)) {
|
|
497
|
-
return yield* new ConfigurationError({ field: "clock.currentTimeNanos" });
|
|
498
|
-
}
|
|
499
|
-
const timestamp = (operation) => Effect.suspend(() => {
|
|
500
|
-
const now = clock.currentTimeNanosUnsafe();
|
|
501
|
-
return Schema.is(Timestamp)(now) ? Effect.succeed(now) : Effect.fail(failure("InvalidArgument", operation));
|
|
502
|
-
});
|
|
503
|
-
const volumeIdentity = Symbol();
|
|
504
|
-
const gate = Semaphore.makeUnsafe(1);
|
|
505
|
-
const root = {
|
|
506
|
-
kind: "directory",
|
|
507
|
-
lineage: image?.root,
|
|
508
|
-
parent: undefined,
|
|
509
|
-
entries: new Map(),
|
|
510
|
-
metadata: directoryMetadata(1n, 0, 0, 0o755, initialTime)
|
|
511
|
-
};
|
|
512
|
-
let nextInode = 2n;
|
|
513
|
-
let entries = 0;
|
|
514
|
-
let usedBytes = 0n;
|
|
515
|
-
// The schema caps this value at uint32, so this boundary conversion is exact.
|
|
516
|
-
const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(0xffffffff)));
|
|
517
|
-
if (image !== undefined) {
|
|
518
|
-
const incoming = new Map();
|
|
519
|
-
let content = 0n;
|
|
520
|
-
let count = 0;
|
|
521
|
-
for (const record of image.records) {
|
|
522
|
-
if (record.kind === "directory")
|
|
523
|
-
count += record.entries.length;
|
|
524
|
-
else {
|
|
525
|
-
const length = Image.decodedLength(record.kind === "file" ? record.data : record.target);
|
|
526
|
-
if (record.kind === "file" && length > maxFileBytes) {
|
|
527
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "maxFileBytes" });
|
|
528
|
-
}
|
|
529
|
-
content += BigInt(length);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
if ((settings.maxEntries !== undefined && count > settings.maxEntries) ||
|
|
533
|
-
(settings.maxBytes !== undefined && content > ByteSize.toBigInt(settings.maxBytes))) {
|
|
534
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "volume" });
|
|
535
|
-
}
|
|
536
|
-
const baseContents = source._tag === "Overlay" ? Content.forOverlay(source.base, image) : undefined;
|
|
537
|
-
for (const record of image.records) {
|
|
538
|
-
const metadata = {
|
|
539
|
-
...record.metadata,
|
|
540
|
-
kind: record.kind,
|
|
541
|
-
ino: record.id === image.root ? 1n : nextInode++,
|
|
542
|
-
nlink: record.kind === "directory" ? 2 : 0,
|
|
543
|
-
size: 0n,
|
|
544
|
-
atimeNs: BigInt(record.metadata.atimeNs),
|
|
545
|
-
mtimeNs: BigInt(record.metadata.mtimeNs),
|
|
546
|
-
ctimeNs: BigInt(record.metadata.ctimeNs),
|
|
547
|
-
birthtimeNs: BigInt(record.metadata.birthtimeNs)
|
|
548
|
-
};
|
|
549
|
-
if (record.kind === "directory") {
|
|
550
|
-
const node = record.id === image.root
|
|
551
|
-
? root
|
|
552
|
-
: { kind: "directory", lineage: record.id, parent: undefined, entries: new Map(), metadata };
|
|
553
|
-
node.metadata = metadata;
|
|
554
|
-
incoming.set(record.id, node);
|
|
555
|
-
}
|
|
556
|
-
else if (record.kind === "file") {
|
|
557
|
-
const data = baseContents?.get(record.id) ?? Content.make(Image.bytes(record.data));
|
|
558
|
-
incoming.set(record.id, {
|
|
559
|
-
kind: "file",
|
|
560
|
-
lineage: record.id,
|
|
561
|
-
data,
|
|
562
|
-
openCount: 0,
|
|
563
|
-
metadata: { ...metadata, size: BigInt(data.bytes.length) }
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
const target = Image.bytes(record.target);
|
|
568
|
-
incoming.set(record.id, {
|
|
569
|
-
kind: "symlink",
|
|
570
|
-
lineage: record.id,
|
|
571
|
-
target,
|
|
572
|
-
metadata: { ...metadata, size: BigInt(target.length) }
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
for (const record of image.records) {
|
|
577
|
-
if (record.kind !== "directory")
|
|
578
|
-
continue;
|
|
579
|
-
const parent = incoming.get(record.id);
|
|
580
|
-
if (parent?.kind !== "directory")
|
|
581
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
582
|
-
for (const entry of record.entries) {
|
|
583
|
-
const node = incoming.get(entry.target);
|
|
584
|
-
if (node === undefined)
|
|
585
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
586
|
-
parent.entries.set(Encoding.encodeHex(Image.bytes(entry.name)), node);
|
|
587
|
-
if (node.kind === "directory") {
|
|
588
|
-
node.parent = parent;
|
|
589
|
-
parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink + 1 };
|
|
590
|
-
}
|
|
591
|
-
else
|
|
592
|
-
node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1 };
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
entries = count;
|
|
596
|
-
usedBytes = content;
|
|
597
|
-
}
|
|
598
|
-
const events = yield* PubSub.unbounded();
|
|
599
|
-
let subscribers = 0;
|
|
600
|
-
const directoryHex = (directory) => {
|
|
601
|
-
const names = [];
|
|
602
|
-
let current = directory;
|
|
603
|
-
while (current.parent !== undefined) {
|
|
604
|
-
const parent = current.parent;
|
|
605
|
-
const found = [...parent.entries].find(([, node]) => node === current);
|
|
606
|
-
if (found === undefined)
|
|
607
|
-
break;
|
|
608
|
-
names.push(found[0]);
|
|
609
|
-
current = parent;
|
|
610
|
-
}
|
|
611
|
-
return "2f" + names.reverse().join("2f");
|
|
612
|
-
};
|
|
613
|
-
const publishEntry = (_tag, parent, name) => {
|
|
614
|
-
if (subscribers === 0)
|
|
615
|
-
return;
|
|
616
|
-
const prefix = directoryHex(parent);
|
|
617
|
-
PubSub.publishUnsafe(events, { _tag, path: ownedPath(nameBytes(prefix + (prefix === "2f" ? "" : "2f") + name)) });
|
|
618
|
-
};
|
|
619
|
-
const publishNode = (target) => {
|
|
620
|
-
if (subscribers === 0)
|
|
621
|
-
return;
|
|
622
|
-
if (target === root) {
|
|
623
|
-
PubSub.publishUnsafe(events, { _tag: "Update", path: ownedPath(new Uint8Array([47])) });
|
|
624
|
-
}
|
|
625
|
-
const pending = [[root, "2f"]];
|
|
626
|
-
while (pending.length > 0) {
|
|
627
|
-
const next = pending.pop();
|
|
628
|
-
if (next === undefined)
|
|
629
|
-
break;
|
|
630
|
-
const [directory, prefix] = next;
|
|
631
|
-
for (const [name, node] of directory.entries) {
|
|
632
|
-
const path = prefix + name;
|
|
633
|
-
if (node === target) {
|
|
634
|
-
PubSub.publishUnsafe(events, { _tag: "Update", path: ownedPath(nameBytes(path)) });
|
|
635
|
-
}
|
|
636
|
-
if (node.kind === "directory")
|
|
637
|
-
pending.push([node, path + "2f"]);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
const captureSnapshot = Effect.fnUntraced(function* () {
|
|
642
|
-
const ids = new Map([[root, "0"]]);
|
|
643
|
-
const pending = [root];
|
|
644
|
-
const records = [];
|
|
645
|
-
for (let index = 0; index < pending.length; index++) {
|
|
646
|
-
const node = pending[index];
|
|
647
|
-
if (node === undefined)
|
|
648
|
-
continue;
|
|
649
|
-
const id = ids.get(node);
|
|
650
|
-
if (id === undefined)
|
|
651
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
652
|
-
const metadata = storedMetadata(node.metadata);
|
|
653
|
-
if (node.kind === "directory") {
|
|
654
|
-
const children = [];
|
|
655
|
-
for (const [name, child] of node.entries) {
|
|
656
|
-
let target = ids.get(child);
|
|
657
|
-
if (target === undefined) {
|
|
658
|
-
target = String(ids.size);
|
|
659
|
-
ids.set(child, target);
|
|
660
|
-
pending.push(child);
|
|
661
|
-
}
|
|
662
|
-
children.push({ name: Image.base64(nameBytes(name)), target });
|
|
663
|
-
}
|
|
664
|
-
records.push({ id, kind: "directory", metadata, entries: children });
|
|
665
|
-
}
|
|
666
|
-
else if (node.kind === "file") {
|
|
667
|
-
records.push({ id, kind: "file", metadata, data: Image.base64(node.data.bytes) });
|
|
668
|
-
}
|
|
669
|
-
else
|
|
670
|
-
records.push({ id, kind: "symlink", metadata, target: Image.base64(node.target) });
|
|
671
|
-
}
|
|
672
|
-
return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records });
|
|
673
|
-
});
|
|
674
|
-
const observeChanges = () => {
|
|
675
|
-
const observation = [];
|
|
676
|
-
const paths = [[root, new Uint8Array([47])]];
|
|
677
|
-
for (let index = 0; index < paths.length; index++) {
|
|
678
|
-
const current = paths[index];
|
|
679
|
-
if (current === undefined)
|
|
680
|
-
continue;
|
|
681
|
-
const [node, path] = current;
|
|
682
|
-
observation.push({
|
|
683
|
-
path: new Uint8Array(path),
|
|
684
|
-
lineage: node.lineage,
|
|
685
|
-
kind: node.kind,
|
|
686
|
-
content: node.kind === "file" ? node.data.bytes : node.kind === "symlink" ? node.target : undefined,
|
|
687
|
-
metadata: storedMetadata(node.metadata)
|
|
688
|
-
});
|
|
689
|
-
if (node.kind !== "directory")
|
|
690
|
-
continue;
|
|
691
|
-
for (const [name, child] of node.entries) {
|
|
692
|
-
const bytes = nameBytes(name);
|
|
693
|
-
const childPath = new Uint8Array(path.length + (path.length === 1 ? 0 : 1) + bytes.length);
|
|
694
|
-
childPath.set(path);
|
|
695
|
-
let offset = path.length;
|
|
696
|
-
if (path.length !== 1)
|
|
697
|
-
childPath[offset++] = 47;
|
|
698
|
-
childPath.set(bytes, offset);
|
|
699
|
-
paths.push([child, childPath]);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
return observation;
|
|
703
|
-
};
|
|
704
|
-
const captureState = Effect.fnUntraced(function* (hook) {
|
|
705
|
-
const snapshot = yield* captureSnapshot();
|
|
706
|
-
if (hook !== undefined)
|
|
707
|
-
yield* hook.betweenSnapshotAndSummary;
|
|
708
|
-
return { snapshot, observation: observeChanges() };
|
|
709
|
-
});
|
|
710
|
-
// Permit waits stay interruptible. State transitions and resource registration do not.
|
|
711
|
-
const coordinated = (effect) => gate.withPermit(Effect.uninterruptible(effect));
|
|
712
|
-
const release = (reference) => coordinated(Effect.sync(() => {
|
|
713
|
-
reference.directory = undefined;
|
|
714
|
-
reference.closed = true;
|
|
715
|
-
}));
|
|
716
|
-
const authorize = (directory, identity, bits, operation, path) => {
|
|
717
|
-
if (identity.privileged)
|
|
718
|
-
return Effect.void;
|
|
719
|
-
const metadata = directory.metadata;
|
|
720
|
-
const shift = metadata.uid === identity.uid ?
|
|
721
|
-
6
|
|
722
|
-
: metadata.gid === identity.gid || identity.groups.includes(metadata.gid)
|
|
723
|
-
? 3
|
|
724
|
-
: 0;
|
|
725
|
-
return ((metadata.mode >> shift) & bits) === bits
|
|
726
|
-
? Effect.void
|
|
727
|
-
: Effect.fail(failure("AccessDenied", operation, path));
|
|
728
|
-
};
|
|
729
|
-
const reclaim = (file) => {
|
|
730
|
-
if (file.metadata.nlink === 0 && file.openCount === 0) {
|
|
731
|
-
usedBytes -= BigInt(file.data.bytes.length);
|
|
732
|
-
file.data = Content.empty();
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
const detach = (node, now) => {
|
|
736
|
-
if (node.kind === "directory") {
|
|
737
|
-
node.parent = undefined;
|
|
738
|
-
node.metadata = { ...node.metadata, nlink: 0, ctimeNs: now };
|
|
739
|
-
}
|
|
740
|
-
else {
|
|
741
|
-
node.metadata = { ...node.metadata, nlink: node.metadata.nlink - 1, ctimeNs: now };
|
|
742
|
-
if (node.kind === "file")
|
|
743
|
-
reclaim(node);
|
|
744
|
-
else if (node.metadata.nlink === 0)
|
|
745
|
-
usedBytes -= BigInt(node.target.length);
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
const releaseFile = (ref) => {
|
|
749
|
-
if (ref.file !== undefined) {
|
|
750
|
-
ref.file.openCount -= 1;
|
|
751
|
-
reclaim(ref.file);
|
|
752
|
-
ref.file = undefined;
|
|
753
|
-
}
|
|
754
|
-
ref.closed = true;
|
|
755
|
-
};
|
|
756
|
-
const resize = Effect.fnUntraced(function* (file, length, operation) {
|
|
757
|
-
if (typeof length !== "bigint" || length < 0n)
|
|
758
|
-
return yield* failure("InvalidArgument", operation);
|
|
759
|
-
if (length > BigInt(maxFileBytes))
|
|
760
|
-
return yield* failure("FileTooLarge", operation);
|
|
761
|
-
const size = Number(length);
|
|
762
|
-
if (settings.maxBytes !== undefined &&
|
|
763
|
-
BigInt(size - file.data.bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes) {
|
|
764
|
-
return yield* failure("NoSpace", operation);
|
|
765
|
-
}
|
|
766
|
-
const data = new Uint8Array(size);
|
|
767
|
-
data.set(file.data.bytes.subarray(0, size));
|
|
768
|
-
const now = yield* timestamp(operation);
|
|
769
|
-
usedBytes += BigInt(size - file.data.bytes.length);
|
|
770
|
-
file.data = Content.make(data);
|
|
771
|
-
file.metadata = {
|
|
772
|
-
...file.metadata,
|
|
773
|
-
size: length,
|
|
774
|
-
mode: file.metadata.mode & ~0o6000,
|
|
775
|
-
mtimeNs: now,
|
|
776
|
-
ctimeNs: now
|
|
777
|
-
};
|
|
778
|
-
publishNode(file);
|
|
779
|
-
});
|
|
780
|
-
const fileHandle = (ref) => {
|
|
781
|
-
const get = (operation, access) => ref.file === undefined || (access === "read" && ref.access === "write") ||
|
|
782
|
-
(access === "write" && ref.access === "read")
|
|
783
|
-
? Effect.fail(failure("InvalidHandle", operation))
|
|
784
|
-
: Effect.succeed(ref.file);
|
|
785
|
-
const read = Effect.fnUntraced(function* (maximum, position) {
|
|
786
|
-
const file = yield* get(position === undefined ? "read" : "pread", "read");
|
|
787
|
-
if (!Schema.is(Natural)(maximum))
|
|
788
|
-
return yield* failure("InvalidArgument", "read");
|
|
789
|
-
const offset = position ?? ref.offset;
|
|
790
|
-
if (typeof offset !== "bigint" || offset < 0n || offset > 0x7fffffffffffffffn) {
|
|
791
|
-
return yield* failure("InvalidArgument", "read");
|
|
792
|
-
}
|
|
793
|
-
const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
|
|
794
|
-
const data = file.data.bytes.slice(start, start + Math.min(maximum, file.data.bytes.length - start));
|
|
795
|
-
if (maximum > 0) {
|
|
796
|
-
file.metadata = { ...file.metadata, atimeNs: (yield* timestamp("read")) };
|
|
797
|
-
}
|
|
798
|
-
if (position === undefined)
|
|
799
|
-
ref.offset += BigInt(data.length);
|
|
800
|
-
return data;
|
|
801
|
-
}, coordinated);
|
|
802
|
-
const write = Effect.fnUntraced(function* (input, position) {
|
|
803
|
-
if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer) || !attachedBuffer(input)) {
|
|
804
|
-
return yield* failure("InvalidArgument", "write");
|
|
805
|
-
}
|
|
806
|
-
const bytes = new Uint8Array(input);
|
|
807
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
808
|
-
const file = yield* get(position === undefined ? "write" : "pwrite", "write");
|
|
809
|
-
const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
|
|
810
|
-
if (typeof offset !== "bigint" || offset < 0n || offset > 0x7fffffffffffffffn) {
|
|
811
|
-
return yield* failure("InvalidArgument", "write");
|
|
812
|
-
}
|
|
813
|
-
if (bytes.length === 0) {
|
|
814
|
-
return 0;
|
|
815
|
-
}
|
|
816
|
-
if (offset >= BigInt(maxFileBytes))
|
|
817
|
-
return yield* failure("FileTooLarge", "write");
|
|
818
|
-
const start = Number(offset);
|
|
819
|
-
const free = settings.maxBytes === undefined
|
|
820
|
-
? BigInt(maxFileBytes)
|
|
821
|
-
: ByteSize.toBigInt(settings.maxBytes) - usedBytes;
|
|
822
|
-
const maximumEnd = BigInt(file.data.bytes.length) + free;
|
|
823
|
-
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
824
|
-
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
825
|
-
if (count === 0)
|
|
826
|
-
return yield* failure("NoSpace", "write");
|
|
827
|
-
const size = Math.max(file.data.bytes.length, start + count);
|
|
828
|
-
// Always detach before mutation. A same-sized write is the critical
|
|
829
|
-
// case: the current payload may belong to the base or a prior capture.
|
|
830
|
-
const data = new Uint8Array(size);
|
|
831
|
-
data.set(file.data.bytes);
|
|
832
|
-
const now = yield* timestamp("write");
|
|
833
|
-
data.set(bytes.subarray(0, count), start);
|
|
834
|
-
usedBytes += BigInt(size - file.data.bytes.length);
|
|
835
|
-
file.data = Content.make(data);
|
|
836
|
-
file.metadata = {
|
|
837
|
-
...file.metadata,
|
|
838
|
-
size: BigInt(size),
|
|
839
|
-
mode: file.metadata.mode & ~0o6000,
|
|
840
|
-
mtimeNs: now,
|
|
841
|
-
ctimeNs: now
|
|
842
|
-
};
|
|
843
|
-
publishNode(file);
|
|
844
|
-
if (position === undefined)
|
|
845
|
-
ref.offset = offset + BigInt(count);
|
|
846
|
-
return count;
|
|
847
|
-
}));
|
|
848
|
-
});
|
|
849
|
-
const handle = Object.freeze({
|
|
850
|
-
[FileHandleId]: true,
|
|
851
|
-
read: Effect.fn("FileHandle.read")(function* (maximum) {
|
|
852
|
-
return yield* read(maximum);
|
|
853
|
-
}),
|
|
854
|
-
pread: Effect.fn("FileHandle.pread")(function* (maximum, offset) {
|
|
855
|
-
return yield* read(maximum, offset);
|
|
856
|
-
}),
|
|
857
|
-
write: Effect.fn("FileHandle.write")(function* (bytes) {
|
|
858
|
-
return yield* write(bytes);
|
|
859
|
-
}),
|
|
860
|
-
pwrite: Effect.fn("FileHandle.pwrite")(function* (bytes, offset) {
|
|
861
|
-
return yield* write(bytes, offset);
|
|
862
|
-
}),
|
|
863
|
-
seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
|
|
864
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
865
|
-
const file = yield* get("seek");
|
|
866
|
-
if (typeof offset !== "bigint" || !Schema.is(SeekMode)(mode)) {
|
|
867
|
-
return yield* failure("InvalidArgument", "seek");
|
|
868
|
-
}
|
|
869
|
-
let next = mode === "current" ? ref.offset + offset : mode === "end" ? file.metadata.size + offset : offset;
|
|
870
|
-
if (next < 0n || next > 0x7fffffffffffffffn)
|
|
871
|
-
return yield* failure("InvalidArgument", "seek");
|
|
872
|
-
if (mode === "data" || mode === "hole") {
|
|
873
|
-
if (offset >= file.metadata.size)
|
|
874
|
-
return yield* failure("NoData", "seek");
|
|
875
|
-
if (mode === "hole")
|
|
876
|
-
next = file.metadata.size;
|
|
877
|
-
}
|
|
878
|
-
ref.offset = next;
|
|
879
|
-
return next;
|
|
880
|
-
}));
|
|
881
|
-
}),
|
|
882
|
-
truncate: Effect.fn("FileHandle.truncate")(function* (length) {
|
|
883
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
884
|
-
yield* resize(yield* get("truncate", "write"), length, "truncate");
|
|
885
|
-
}));
|
|
886
|
-
}),
|
|
887
|
-
stat: coordinated(Effect.gen(function* () {
|
|
888
|
-
return { ...(yield* get("stat")).metadata };
|
|
889
|
-
})).pipe(Effect.withSpan("FileHandle.stat")),
|
|
890
|
-
sync: coordinated(Effect.suspend(() => Effect.asVoid(get("sync")))).pipe(Effect.withSpan("FileHandle.sync")),
|
|
891
|
-
close: coordinated(Effect.gen(function* () {
|
|
892
|
-
yield* get("close");
|
|
893
|
-
releaseFile(ref);
|
|
894
|
-
})).pipe(Effect.withSpan("FileHandle.close"))
|
|
895
|
-
});
|
|
896
|
-
files.set(handle, ref);
|
|
897
|
-
return handle;
|
|
898
|
-
};
|
|
899
|
-
const createCaller = (reference, identity, umask) => {
|
|
900
|
-
const lookup = Effect.fnUntraced(function* (path, base, operation, options = {}) {
|
|
901
|
-
const { followFinalSymlink = true, allowMissing = false, parentOnly = false } = options;
|
|
902
|
-
if (reference.directory === undefined)
|
|
903
|
-
return yield* failure("ClosedCaller", operation, path.input);
|
|
904
|
-
let current = path.absolute ? root : reference.directory;
|
|
905
|
-
if (!path.absolute && base !== undefined) {
|
|
906
|
-
const target = handles.get(base);
|
|
907
|
-
if (target === undefined)
|
|
908
|
-
return yield* failure("InvalidHandle", operation, path.input);
|
|
909
|
-
if (target.volume !== volumeIdentity)
|
|
910
|
-
return yield* failure("ForeignHandle", operation, path.input);
|
|
911
|
-
if (target.directory === undefined)
|
|
912
|
-
return yield* failure("InvalidHandle", operation, path.input);
|
|
913
|
-
current = target.directory;
|
|
914
|
-
yield* authorize(current, identity, 1, operation, path.input);
|
|
915
|
-
}
|
|
916
|
-
if (current.metadata.nlink === 0)
|
|
917
|
-
return yield* failure("NotFound", operation, path.input);
|
|
918
|
-
let work = path;
|
|
919
|
-
let parent;
|
|
920
|
-
let name;
|
|
921
|
-
let traversals = 0;
|
|
922
|
-
for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
|
|
923
|
-
if (current.kind !== "directory")
|
|
924
|
-
return yield* failure("NotDirectory", operation, path.input);
|
|
925
|
-
yield* authorize(current, identity, 1, operation, path.input);
|
|
926
|
-
const component = work.components[index];
|
|
927
|
-
if (component === undefined)
|
|
928
|
-
break;
|
|
929
|
-
if (component === "2e")
|
|
930
|
-
continue;
|
|
931
|
-
if (component === "2e2e") {
|
|
932
|
-
current = current.parent ?? current;
|
|
933
|
-
parent = undefined;
|
|
934
|
-
name = undefined;
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
parent = current;
|
|
938
|
-
name = component;
|
|
939
|
-
const child = current.entries.get(component);
|
|
940
|
-
if (child === undefined) {
|
|
941
|
-
if (allowMissing && index === work.components.length - 1 && !work.trailingSlash) {
|
|
942
|
-
return { node: undefined, parent, name };
|
|
943
|
-
}
|
|
944
|
-
return yield* failure("NotFound", operation, path.input);
|
|
945
|
-
}
|
|
946
|
-
if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
|
|
947
|
-
if (child.target.length === 0)
|
|
948
|
-
return yield* failure("NotFound", operation, path.input);
|
|
949
|
-
if (++traversals > 40)
|
|
950
|
-
return yield* failure("SymlinkLoop", operation, path.input);
|
|
951
|
-
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
952
|
-
if (settings.maxPathBytes !== undefined &&
|
|
953
|
-
ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
|
|
954
|
-
return yield* failure("PathTooLong", operation, path.input);
|
|
955
|
-
}
|
|
956
|
-
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
957
|
-
expansion.set(child.target);
|
|
958
|
-
expansion.set(suffix, child.target.length);
|
|
959
|
-
work = yield* Effect.fromResult(preparePath(ownedPath(expansion), operation, settings.maxPathBytes));
|
|
960
|
-
if (work.absolute)
|
|
961
|
-
current = root;
|
|
962
|
-
index = -1;
|
|
963
|
-
}
|
|
964
|
-
else
|
|
965
|
-
current = child;
|
|
966
|
-
}
|
|
967
|
-
if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
|
|
968
|
-
return yield* failure("NotDirectory", operation, path.input);
|
|
969
|
-
}
|
|
970
|
-
return { node: current, parent, name };
|
|
971
|
-
});
|
|
972
|
-
const resolveNode = Effect.fnUntraced(function* (path, base, operation, options) {
|
|
973
|
-
const result = yield* lookup(path, base, operation, options);
|
|
974
|
-
if (result.node === undefined)
|
|
975
|
-
return yield* failure("NotFound", operation, path.input);
|
|
976
|
-
return result.node;
|
|
977
|
-
});
|
|
978
|
-
const locate = Effect.fnUntraced(function* (path, base, operation, options) {
|
|
979
|
-
const result = yield* lookup(path, base, operation, options);
|
|
980
|
-
const node = result.node;
|
|
981
|
-
if (node === undefined)
|
|
982
|
-
return yield* failure("NotFound", operation, path.input);
|
|
983
|
-
if (node.kind !== "directory")
|
|
984
|
-
return yield* failure("NotDirectory", operation, path.input);
|
|
985
|
-
return node;
|
|
986
|
-
});
|
|
987
|
-
const acquireDirectory = Effect.fnUntraced(function* (input, options, operation) {
|
|
988
|
-
const prepared = preparePath(input, operation, settings.maxPathBytes);
|
|
989
|
-
const base = options?.relativeTo;
|
|
990
|
-
const acquired = { volume: volumeIdentity, directory: undefined, closed: false };
|
|
991
|
-
// Register before retaining a directory. Closed scopes can run this immediately,
|
|
992
|
-
// so registration must not happen while holding the volume permit.
|
|
993
|
-
yield* Effect.addFinalizer(() => release(acquired));
|
|
994
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
995
|
-
if (acquired.closed)
|
|
996
|
-
return yield* Effect.interrupt;
|
|
997
|
-
const path = yield* Effect.fromResult(prepared);
|
|
998
|
-
const directory = yield* locate(path, base, operation);
|
|
999
|
-
yield* authorize(directory, identity, 1, operation, input);
|
|
1000
|
-
acquired.directory = directory;
|
|
1001
|
-
return acquired;
|
|
1002
|
-
}));
|
|
1003
|
-
});
|
|
1004
|
-
const list = Effect.fnUntraced(function* (input, options) {
|
|
1005
|
-
const prepared = preparePath(input, "readDirectory", settings.maxPathBytes);
|
|
1006
|
-
const base = options?.relativeTo;
|
|
1007
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1008
|
-
const directory = yield* locate(yield* Effect.fromResult(prepared), base, "readDirectory");
|
|
1009
|
-
yield* authorize(directory, identity, 4, "readDirectory", input);
|
|
1010
|
-
const result = [...directory.entries.keys()].map(nameBytes);
|
|
1011
|
-
directory.metadata = { ...directory.metadata, atimeNs: (yield* timestamp("readDirectory")) };
|
|
1012
|
-
return result;
|
|
1013
|
-
}));
|
|
1014
|
-
});
|
|
1015
|
-
const readTarget = Effect.fnUntraced(function* (input, options) {
|
|
1016
|
-
const prepared = preparePath(input, "readLink", settings.maxPathBytes);
|
|
1017
|
-
const base = options?.relativeTo;
|
|
1018
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1019
|
-
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readLink", {
|
|
1020
|
-
followFinalSymlink: false
|
|
1021
|
-
});
|
|
1022
|
-
if (node.kind !== "symlink")
|
|
1023
|
-
return yield* failure("InvalidArgument", "readLink", input);
|
|
1024
|
-
return new Uint8Array(node.target);
|
|
1025
|
-
}));
|
|
1026
|
-
});
|
|
1027
|
-
const canonical = Effect.fnUntraced(function* (input, options) {
|
|
1028
|
-
const prepared = preparePath(input, "realPath", settings.maxPathBytes);
|
|
1029
|
-
const base = options?.relativeTo;
|
|
1030
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1031
|
-
const result = yield* lookup(yield* Effect.fromResult(prepared), base, "realPath");
|
|
1032
|
-
const components = [];
|
|
1033
|
-
if (result.node?.kind !== "directory" && result.name !== undefined)
|
|
1034
|
-
components.push(result.name);
|
|
1035
|
-
let directory = result.node?.kind === "directory" ? result.node : result.parent;
|
|
1036
|
-
while (directory !== undefined && directory.parent !== undefined) {
|
|
1037
|
-
const parent = directory.parent;
|
|
1038
|
-
const entry = [...parent.entries].find(([, child]) => child === directory);
|
|
1039
|
-
if (entry === undefined)
|
|
1040
|
-
return yield* failure("NotFound", "realPath", input);
|
|
1041
|
-
components.push(entry[0]);
|
|
1042
|
-
directory = parent;
|
|
1043
|
-
}
|
|
1044
|
-
return nameBytes("2f" + components.reverse().join("2f"));
|
|
1045
|
-
}));
|
|
1046
|
-
});
|
|
1047
|
-
const metadataNode = Effect.fnUntraced(function* (target, options, operation) {
|
|
1048
|
-
if (reference.directory === undefined)
|
|
1049
|
-
return yield* failure("ClosedCaller", operation);
|
|
1050
|
-
if (typeof target === "object" && target !== null && (FileHandleId in target || DirectoryHandleId in target)) {
|
|
1051
|
-
const ref = FileHandleId in target ? files.get(target) : handles.get(target);
|
|
1052
|
-
if (ref === undefined)
|
|
1053
|
-
return yield* failure("InvalidHandle", operation);
|
|
1054
|
-
if (ref.volume !== volumeIdentity)
|
|
1055
|
-
return yield* failure("ForeignHandle", operation);
|
|
1056
|
-
const node = "file" in ref ? ref.file : ref.directory;
|
|
1057
|
-
if (node === undefined)
|
|
1058
|
-
return yield* failure("InvalidHandle", operation);
|
|
1059
|
-
return node;
|
|
1060
|
-
}
|
|
1061
|
-
const path = yield* Effect.fromResult(preparePath(target, operation, settings.maxPathBytes));
|
|
1062
|
-
return yield* resolveNode(path, options?.relativeTo, operation, {
|
|
1063
|
-
followFinalSymlink: options?.followFinalSymlink !== false
|
|
1064
|
-
});
|
|
1065
|
-
});
|
|
1066
|
-
const permittedMode = (metadata, mode, operation, path) => {
|
|
1067
|
-
if (!identity.privileged && identity.uid !== metadata.uid) {
|
|
1068
|
-
return Effect.fail(failure("AccessDenied", operation, path));
|
|
1069
|
-
}
|
|
1070
|
-
const group = identity.gid === metadata.gid || identity.groups.includes(metadata.gid);
|
|
1071
|
-
return Effect.succeed(!identity.privileged && metadata.kind === "file" && !group ? mode & ~0o2000 : mode);
|
|
1072
|
-
};
|
|
1073
|
-
const changeMode = Effect.fnUntraced(function* (target, mode, options) {
|
|
1074
|
-
if (!Schema.is(Mode)(mode))
|
|
1075
|
-
return yield* failure("InvalidArgument", "chmod");
|
|
1076
|
-
const chosen = options === undefined ? undefined : { ...options };
|
|
1077
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1078
|
-
const node = yield* metadataNode(target, chosen, "chmod");
|
|
1079
|
-
const permitted = yield* permittedMode(node.metadata, mode, "chmod");
|
|
1080
|
-
node.metadata = {
|
|
1081
|
-
...node.metadata,
|
|
1082
|
-
mode: permitted,
|
|
1083
|
-
ctimeNs: (yield* timestamp("chmod"))
|
|
1084
|
-
};
|
|
1085
|
-
publishNode(node);
|
|
1086
|
-
}));
|
|
1087
|
-
});
|
|
1088
|
-
const changeOwner = Effect.fnUntraced(function* (target, owner, options) {
|
|
1089
|
-
const decoded = Schema.decodeResult(OwnerUpdate, { onExcessProperty: "error" })(owner);
|
|
1090
|
-
if (Result.isFailure(decoded))
|
|
1091
|
-
return yield* failure("InvalidArgument", "chown");
|
|
1092
|
-
const update = { ...decoded.success };
|
|
1093
|
-
const chosen = options === undefined ? undefined : { ...options };
|
|
1094
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1095
|
-
const node = yield* metadataNode(target, chosen, "chown");
|
|
1096
|
-
if (!identity.privileged && (identity.uid !== node.metadata.uid ||
|
|
1097
|
-
(update.uid !== undefined && update.uid !== node.metadata.uid) ||
|
|
1098
|
-
(update.gid !== undefined && update.gid !== identity.gid && !identity.groups.includes(update.gid)))) {
|
|
1099
|
-
return yield* failure("AccessDenied", "chown");
|
|
1100
|
-
}
|
|
1101
|
-
if (update.uid === undefined && update.gid === undefined)
|
|
1102
|
-
return;
|
|
1103
|
-
node.metadata = {
|
|
1104
|
-
...node.metadata,
|
|
1105
|
-
uid: update.uid ?? node.metadata.uid,
|
|
1106
|
-
gid: update.gid ?? node.metadata.gid,
|
|
1107
|
-
mode: node.kind === "file" ? node.metadata.mode & ~0o6000 : node.metadata.mode,
|
|
1108
|
-
ctimeNs: (yield* timestamp("chown"))
|
|
1109
|
-
};
|
|
1110
|
-
publishNode(node);
|
|
1111
|
-
}));
|
|
1112
|
-
});
|
|
1113
|
-
const changeTimes = Effect.fnUntraced(function* (target, times, options) {
|
|
1114
|
-
const decoded = Schema.decodeResult(Times, { onExcessProperty: "error" })(times);
|
|
1115
|
-
if (Result.isFailure(decoded))
|
|
1116
|
-
return yield* failure("InvalidArgument", "utimes");
|
|
1117
|
-
const access = { ...decoded.success.access };
|
|
1118
|
-
const modification = { ...decoded.success.modification };
|
|
1119
|
-
const chosen = options === undefined ? undefined : { ...options };
|
|
1120
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1121
|
-
const node = yield* metadataNode(target, chosen, "utimes");
|
|
1122
|
-
if (access.kind === "omit" && modification.kind === "omit")
|
|
1123
|
-
return;
|
|
1124
|
-
if (!identity.privileged && identity.uid !== node.metadata.uid) {
|
|
1125
|
-
if (access.kind !== "now" || modification.kind !== "now")
|
|
1126
|
-
return yield* failure("AccessDenied", "utimes");
|
|
1127
|
-
yield* authorize(node, identity, 2, "utimes", "/");
|
|
1128
|
-
}
|
|
1129
|
-
const now = yield* timestamp("utimes");
|
|
1130
|
-
node.metadata = {
|
|
1131
|
-
...node.metadata,
|
|
1132
|
-
atimeNs: access.kind === "omit"
|
|
1133
|
-
? node.metadata.atimeNs
|
|
1134
|
-
: access.kind === "now"
|
|
1135
|
-
? now
|
|
1136
|
-
: access.nanoseconds,
|
|
1137
|
-
mtimeNs: modification.kind === "omit"
|
|
1138
|
-
? node.metadata.mtimeNs
|
|
1139
|
-
: modification.kind === "now"
|
|
1140
|
-
? now
|
|
1141
|
-
: modification.nanoseconds,
|
|
1142
|
-
ctimeNs: now
|
|
1143
|
-
};
|
|
1144
|
-
publishNode(node);
|
|
1145
|
-
}));
|
|
1146
|
-
});
|
|
1147
|
-
const authorizeRemoval = (parent, child, operation, input) => (parent.metadata.mode & 0o1000) !== 0 && !identity.privileged &&
|
|
1148
|
-
identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
|
|
1149
|
-
? Effect.fail(failure("AccessDenied", operation, input))
|
|
1150
|
-
: Effect.void;
|
|
1151
|
-
return Object.freeze({
|
|
1152
|
-
[CallerId]: true,
|
|
1153
|
-
readFile: Effect.fn("Caller.readFile")(function* (input, options) {
|
|
1154
|
-
const prepared = preparePath(input, "readFile", settings.maxPathBytes);
|
|
1155
|
-
const base = options?.relativeTo;
|
|
1156
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1157
|
-
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readFile");
|
|
1158
|
-
if (node.kind !== "file")
|
|
1159
|
-
return yield* failure("IsDirectory", "readFile", input);
|
|
1160
|
-
yield* authorize(node, identity, 4, "readFile", input);
|
|
1161
|
-
const data = new Uint8Array(node.data.bytes);
|
|
1162
|
-
node.metadata = { ...node.metadata, atimeNs: (yield* timestamp("readFile")) };
|
|
1163
|
-
return data;
|
|
1164
|
-
}));
|
|
1165
|
-
}),
|
|
1166
|
-
writeFile: Effect.fn("Caller.writeFile")(function* (input, bytes, options) {
|
|
1167
|
-
const prepared = preparePath(input, "writeFile", settings.maxPathBytes);
|
|
1168
|
-
if (!(bytes instanceof Uint8Array) || !(bytes.buffer instanceof ArrayBuffer) || !attachedBuffer(bytes)) {
|
|
1169
|
-
return yield* failure("InvalidArgument", "writeFile", input);
|
|
1170
|
-
}
|
|
1171
|
-
const captured = new Uint8Array(bytes);
|
|
1172
|
-
const { relativeTo: base, ...raw } = options;
|
|
1173
|
-
const decoded = Schema.decodeResult(WriteFileSettings, { onExcessProperty: "error" })(raw);
|
|
1174
|
-
if (Result.isFailure(decoded))
|
|
1175
|
-
return yield* failure("InvalidArgument", "writeFile", input);
|
|
1176
|
-
const chosen = decoded.success;
|
|
1177
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1178
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1179
|
-
if (chosen.create === "exclusive") {
|
|
1180
|
-
const exists = yield* Effect.result(lookup(path, base, "writeFile", { followFinalSymlink: false }));
|
|
1181
|
-
if (Result.isSuccess(exists))
|
|
1182
|
-
return yield* failure("AlreadyExists", "writeFile", input);
|
|
1183
|
-
if (exists.failure.code !== "NotFound")
|
|
1184
|
-
return yield* exists.failure;
|
|
1185
|
-
}
|
|
1186
|
-
const resolved = yield* lookup(path, base, "writeFile", {
|
|
1187
|
-
followFinalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false,
|
|
1188
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1189
|
-
});
|
|
1190
|
-
const { name, parent } = resolved;
|
|
1191
|
-
if (parent === undefined || name === undefined || resolved.node?.kind === "directory") {
|
|
1192
|
-
return yield* failure("IsDirectory", "writeFile", input);
|
|
1193
|
-
}
|
|
1194
|
-
const replaced = resolved.node?.kind === "symlink" ? resolved.node : undefined;
|
|
1195
|
-
if (replaced !== undefined && !chosen.replaceFinalSymlink) {
|
|
1196
|
-
return yield* failure("SymlinkLoop", "writeFile", input);
|
|
1197
|
-
}
|
|
1198
|
-
if (chosen.access === "read")
|
|
1199
|
-
return yield* failure("InvalidHandle", "writeFile", input);
|
|
1200
|
-
const file = resolved.node?.kind === "file" ? resolved.node : undefined;
|
|
1201
|
-
if (file === undefined) {
|
|
1202
|
-
yield* authorize(parent, identity, 3, "writeFile", input);
|
|
1203
|
-
if (replaced !== undefined)
|
|
1204
|
-
yield* authorizeRemoval(parent, replaced, "writeFile", input);
|
|
1205
|
-
if (replaced === undefined && settings.maxEntries !== undefined && entries >= settings.maxEntries) {
|
|
1206
|
-
return yield* failure("NoSpace", "writeFile", input);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
else
|
|
1210
|
-
yield* authorize(file, identity, chosen.access === "readWrite" ? 6 : 2, "writeFile", input);
|
|
1211
|
-
const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, "writeFile", input);
|
|
1212
|
-
const previous = file?.data.bytes.length ?? 0;
|
|
1213
|
-
const initial = chosen.truncate ? 0 : previous;
|
|
1214
|
-
const position = chosen.append ? initial : 0;
|
|
1215
|
-
const size = Math.max(initial, position + captured.length);
|
|
1216
|
-
if (size > maxFileBytes)
|
|
1217
|
-
return yield* failure("FileTooLarge", "writeFile", input);
|
|
1218
|
-
const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
|
|
1219
|
-
if (settings.maxBytes !== undefined &&
|
|
1220
|
-
BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - usedBytes + BigInt(reclaimed)) {
|
|
1221
|
-
return yield* failure("NoSpace", "writeFile", input);
|
|
1222
|
-
}
|
|
1223
|
-
if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
let data = captured;
|
|
1227
|
-
if (position !== 0 || size !== captured.length) {
|
|
1228
|
-
data = new Uint8Array(size);
|
|
1229
|
-
if (file !== undefined && !chosen.truncate)
|
|
1230
|
-
data.set(file.data.bytes);
|
|
1231
|
-
data.set(captured, position);
|
|
1232
|
-
}
|
|
1233
|
-
const now = yield* timestamp("writeFile");
|
|
1234
|
-
const node = file ??
|
|
1235
|
-
{
|
|
1236
|
-
kind: "file",
|
|
1237
|
-
lineage: undefined,
|
|
1238
|
-
data: Content.make(data),
|
|
1239
|
-
openCount: 0,
|
|
1240
|
-
metadata: {
|
|
1241
|
-
...directoryMetadata(nextInode++, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
1242
|
-
kind: "file",
|
|
1243
|
-
nlink: 1
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
node.data = Content.make(data);
|
|
1247
|
-
node.metadata = {
|
|
1248
|
-
...node.metadata,
|
|
1249
|
-
mode: finalMode ?? node.metadata.mode & ~0o6000,
|
|
1250
|
-
size: BigInt(size),
|
|
1251
|
-
mtimeNs: now,
|
|
1252
|
-
ctimeNs: now
|
|
1253
|
-
};
|
|
1254
|
-
usedBytes += BigInt(size - previous);
|
|
1255
|
-
if (file === undefined) {
|
|
1256
|
-
if (replaced !== undefined)
|
|
1257
|
-
detach(replaced, now);
|
|
1258
|
-
parent.entries.set(name, node);
|
|
1259
|
-
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1260
|
-
if (replaced === undefined)
|
|
1261
|
-
entries += 1;
|
|
1262
|
-
publishEntry(replaced === undefined ? "Create" : "Update", parent, name);
|
|
1263
|
-
}
|
|
1264
|
-
else
|
|
1265
|
-
publishNode(node);
|
|
1266
|
-
}));
|
|
1267
|
-
}),
|
|
1268
|
-
chmod: Effect.fn("Caller.chmod")(function* (path, mode, options) {
|
|
1269
|
-
yield* changeMode(path, mode, options);
|
|
1270
|
-
}),
|
|
1271
|
-
chmodHandle: Effect.fn("Caller.chmodHandle")(function* (handle, mode) {
|
|
1272
|
-
yield* changeMode(handle, mode);
|
|
1273
|
-
}),
|
|
1274
|
-
chown: Effect.fn("Caller.chown")(function* (path, owner, options) {
|
|
1275
|
-
yield* changeOwner(path, owner, options);
|
|
1276
|
-
}),
|
|
1277
|
-
chownHandle: Effect.fn("Caller.chownHandle")(function* (handle, owner) {
|
|
1278
|
-
yield* changeOwner(handle, owner);
|
|
1279
|
-
}),
|
|
1280
|
-
utimes: Effect.fn("Caller.utimes")(function* (path, times, options) {
|
|
1281
|
-
yield* changeTimes(path, times, options);
|
|
1282
|
-
}),
|
|
1283
|
-
utimesHandle: Effect.fn("Caller.utimesHandle")(function* (handle, times) {
|
|
1284
|
-
yield* changeTimes(handle, times);
|
|
1285
|
-
}),
|
|
1286
|
-
access: Effect.fn("Caller.access")(function* (input, bits = 0, options) {
|
|
1287
|
-
const prepared = preparePath(input, "access", settings.maxPathBytes);
|
|
1288
|
-
const base = options?.relativeTo;
|
|
1289
|
-
if (!Number.isInteger(bits) || bits < 0 || bits > 7)
|
|
1290
|
-
return yield* failure("InvalidArgument", "access", input);
|
|
1291
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1292
|
-
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "access");
|
|
1293
|
-
if (node.kind === "file" && (bits & 1) !== 0 && (node.metadata.mode & 0o111) === 0) {
|
|
1294
|
-
return yield* failure("AccessDenied", "access", input);
|
|
1295
|
-
}
|
|
1296
|
-
yield* authorize(node, identity, bits, "access", input);
|
|
1297
|
-
}));
|
|
1298
|
-
}),
|
|
1299
|
-
truncate: Effect.fn("Caller.truncate")(function* (input, length, options) {
|
|
1300
|
-
const prepared = preparePath(input, "truncate", settings.maxPathBytes);
|
|
1301
|
-
const base = options?.relativeTo;
|
|
1302
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1303
|
-
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "truncate");
|
|
1304
|
-
if (node.kind !== "file")
|
|
1305
|
-
return yield* failure("IsDirectory", "truncate", input);
|
|
1306
|
-
yield* authorize(node, identity, 2, "truncate", input);
|
|
1307
|
-
yield* resize(node, length, "truncate");
|
|
1308
|
-
}));
|
|
1309
|
-
}),
|
|
1310
|
-
lstat: Effect.fn("Caller.lstat")(function* (input, options) {
|
|
1311
|
-
const prepared = preparePath(input, "lstat", settings.maxPathBytes);
|
|
1312
|
-
const base = options?.relativeTo;
|
|
1313
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1314
|
-
const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "lstat", {
|
|
1315
|
-
followFinalSymlink: false
|
|
1316
|
-
});
|
|
1317
|
-
return { ...node.metadata };
|
|
1318
|
-
}));
|
|
1319
|
-
}),
|
|
1320
|
-
link: Effect.fn("Caller.link")(function* (source, destination, options) {
|
|
1321
|
-
const a = preparePath(source, "link", settings.maxPathBytes);
|
|
1322
|
-
const b = preparePath(destination, "link", settings.maxPathBytes);
|
|
1323
|
-
const sourceBase = options?.sourceRelativeTo;
|
|
1324
|
-
const destinationBase = options?.destinationRelativeTo;
|
|
1325
|
-
const follow = options?.followSourceSymlink ?? false;
|
|
1326
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1327
|
-
const node = yield* resolveNode(yield* Effect.fromResult(a), sourceBase, "link", {
|
|
1328
|
-
followFinalSymlink: follow
|
|
1329
|
-
});
|
|
1330
|
-
if (node.kind === "directory")
|
|
1331
|
-
return yield* failure("IsDirectory", "link", source);
|
|
1332
|
-
const path = yield* Effect.fromResult(b);
|
|
1333
|
-
const parent = yield* locate(path, destinationBase, "link", { parentOnly: true });
|
|
1334
|
-
yield* authorize(parent, identity, 3, "link", destination);
|
|
1335
|
-
const name = path.components.at(-1);
|
|
1336
|
-
if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
|
|
1337
|
-
return yield* failure("AlreadyExists", "link", destination);
|
|
1338
|
-
}
|
|
1339
|
-
if (path.trailingSlash)
|
|
1340
|
-
return yield* failure("NotDirectory", "link", destination);
|
|
1341
|
-
if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
|
|
1342
|
-
return yield* failure("NoSpace", "link", destination);
|
|
1343
|
-
}
|
|
1344
|
-
const now = yield* timestamp("link");
|
|
1345
|
-
parent.entries.set(name, node);
|
|
1346
|
-
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1347
|
-
node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1, ctimeNs: now };
|
|
1348
|
-
entries += 1;
|
|
1349
|
-
publishEntry("Create", parent, name);
|
|
1350
|
-
}));
|
|
1351
|
-
}),
|
|
1352
|
-
symlink: Effect.fn("Caller.symlink")(function* (target, input, options) {
|
|
1353
|
-
const prepared = preparePath(input, "symlink", settings.maxPathBytes);
|
|
1354
|
-
if (typeof target === "string" && !wellFormed(target)) {
|
|
1355
|
-
return yield* failure("InvalidPathEncoding", "symlink", target);
|
|
1356
|
-
}
|
|
1357
|
-
const rawTarget = typeof target === "string" ? new TextEncoder().encode(target) : getBytePathBytes(target);
|
|
1358
|
-
if (rawTarget === undefined || rawTarget.includes(0)) {
|
|
1359
|
-
return yield* failure("InvalidArgument", "symlink", target);
|
|
1360
|
-
}
|
|
1361
|
-
const targetBytes = new Uint8Array(rawTarget);
|
|
1362
|
-
const base = options?.relativeTo;
|
|
1363
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1364
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1365
|
-
const bytes = targetBytes;
|
|
1366
|
-
const parent = yield* locate(path, base, "symlink", { parentOnly: true });
|
|
1367
|
-
yield* authorize(parent, identity, 3, "symlink", input);
|
|
1368
|
-
const name = path.components.at(-1);
|
|
1369
|
-
if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
|
|
1370
|
-
return yield* failure("AlreadyExists", "symlink", input);
|
|
1371
|
-
}
|
|
1372
|
-
if (path.trailingSlash)
|
|
1373
|
-
return yield* failure("NotDirectory", "symlink", input);
|
|
1374
|
-
if ((settings.maxEntries !== undefined && entries >= settings.maxEntries) ||
|
|
1375
|
-
(settings.maxBytes !== undefined &&
|
|
1376
|
-
BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes))
|
|
1377
|
-
return yield* failure("NoSpace", "symlink", input);
|
|
1378
|
-
const now = yield* timestamp("symlink");
|
|
1379
|
-
const node = {
|
|
1380
|
-
kind: "symlink",
|
|
1381
|
-
lineage: undefined,
|
|
1382
|
-
target: new Uint8Array(bytes),
|
|
1383
|
-
metadata: {
|
|
1384
|
-
...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, 0o777, now),
|
|
1385
|
-
kind: "symlink",
|
|
1386
|
-
nlink: 1,
|
|
1387
|
-
size: BigInt(bytes.length)
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
parent.entries.set(name, node);
|
|
1391
|
-
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1392
|
-
nextInode += 1n;
|
|
1393
|
-
entries += 1;
|
|
1394
|
-
usedBytes += BigInt(bytes.length);
|
|
1395
|
-
publishEntry("Create", parent, name);
|
|
1396
|
-
}));
|
|
1397
|
-
}),
|
|
1398
|
-
readDirectoryBytes: Effect.fn("Caller.readDirectoryBytes")(function* (input, options) {
|
|
1399
|
-
return yield* list(input, options);
|
|
1400
|
-
}),
|
|
1401
|
-
readDirectory: Effect.fn("Caller.readDirectory")(function* (input, options) {
|
|
1402
|
-
return yield* Effect.forEach(yield* list(input, options), (bytes) => strictString(bytes, "readDirectory"));
|
|
1403
|
-
}),
|
|
1404
|
-
readLinkBytes: Effect.fn("Caller.readLinkBytes")(function* (input, options) {
|
|
1405
|
-
return yield* readTarget(input, options);
|
|
1406
|
-
}),
|
|
1407
|
-
readLink: Effect.fn("Caller.readLink")(function* (input, options) {
|
|
1408
|
-
return yield* strictString(yield* readTarget(input, options), "readLink");
|
|
1409
|
-
}),
|
|
1410
|
-
realPathBytes: Effect.fn("Caller.realPathBytes")(function* (input, options) {
|
|
1411
|
-
return ownedPath(yield* canonical(input, options));
|
|
1412
|
-
}),
|
|
1413
|
-
realPath: Effect.fn("Caller.realPath")(function* (input, options) {
|
|
1414
|
-
return yield* strictString(yield* canonical(input, options), "realPath");
|
|
1415
|
-
}),
|
|
1416
|
-
open: Effect.fn("Caller.open")(function* (input, options) {
|
|
1417
|
-
const prepared = preparePath(input, "open", settings.maxPathBytes);
|
|
1418
|
-
const { relativeTo: base, ...raw } = options;
|
|
1419
|
-
const decoded = Schema.decodeResult(OpenSettings, { onExcessProperty: "error" })(raw);
|
|
1420
|
-
if (Result.isFailure(decoded))
|
|
1421
|
-
return yield* failure("InvalidArgument", "open", input);
|
|
1422
|
-
const chosen = { ...decoded.success };
|
|
1423
|
-
if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
|
|
1424
|
-
return yield* failure("InvalidArgument", "open", input);
|
|
1425
|
-
}
|
|
1426
|
-
if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
1427
|
-
return yield* failure("InvalidArgument", "open", input);
|
|
1428
|
-
}
|
|
1429
|
-
const acquired = {
|
|
1430
|
-
volume: volumeIdentity,
|
|
1431
|
-
file: undefined,
|
|
1432
|
-
closed: false,
|
|
1433
|
-
offset: 0n,
|
|
1434
|
-
access: chosen.access,
|
|
1435
|
-
append: chosen.append ?? false
|
|
1436
|
-
};
|
|
1437
|
-
yield* Effect.addFinalizer(() => coordinated(Effect.sync(() => releaseFile(acquired))));
|
|
1438
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1439
|
-
if (acquired.closed)
|
|
1440
|
-
return yield* Effect.interrupt;
|
|
1441
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1442
|
-
if (chosen.create === "exclusive") {
|
|
1443
|
-
const existing = yield* Effect.result(lookup(path, base, "open", { followFinalSymlink: false }));
|
|
1444
|
-
if (Result.isSuccess(existing))
|
|
1445
|
-
return yield* failure("AlreadyExists", "open", input);
|
|
1446
|
-
if (existing.failure.code !== "NotFound")
|
|
1447
|
-
return yield* existing.failure;
|
|
1448
|
-
}
|
|
1449
|
-
const resolved = yield* lookup(path, base, "open", {
|
|
1450
|
-
followFinalSymlink: chosen.followFinalSymlink !== false,
|
|
1451
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1452
|
-
});
|
|
1453
|
-
const parent = resolved.parent;
|
|
1454
|
-
if (parent === undefined)
|
|
1455
|
-
return yield* failure("IsDirectory", "open", input);
|
|
1456
|
-
const name = resolved.name;
|
|
1457
|
-
if (name === undefined || name === "2e" || name === "2e2e") {
|
|
1458
|
-
return yield* failure("IsDirectory", "open", input);
|
|
1459
|
-
}
|
|
1460
|
-
yield* authorize(parent, identity, 1, "open", input);
|
|
1461
|
-
let file = resolved.node;
|
|
1462
|
-
if (file !== undefined && chosen.create === "exclusive") {
|
|
1463
|
-
return yield* failure("AlreadyExists", "open", input);
|
|
1464
|
-
}
|
|
1465
|
-
if (file === undefined) {
|
|
1466
|
-
if (chosen.create === undefined || chosen.create === "never" || path.trailingSlash) {
|
|
1467
|
-
return yield* failure("NotFound", "open", input);
|
|
1468
|
-
}
|
|
1469
|
-
yield* authorize(parent, identity, 3, "open", input);
|
|
1470
|
-
if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
|
|
1471
|
-
return yield* failure("NoSpace", "open", input);
|
|
1472
|
-
}
|
|
1473
|
-
const now = yield* timestamp("open");
|
|
1474
|
-
file = {
|
|
1475
|
-
kind: "file",
|
|
1476
|
-
lineage: undefined,
|
|
1477
|
-
data: Content.empty(),
|
|
1478
|
-
openCount: 0,
|
|
1479
|
-
metadata: {
|
|
1480
|
-
...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
1481
|
-
kind: "file",
|
|
1482
|
-
nlink: 1
|
|
1483
|
-
}
|
|
1484
|
-
};
|
|
1485
|
-
parent.entries.set(name, file);
|
|
1486
|
-
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1487
|
-
entries += 1;
|
|
1488
|
-
nextInode += 1n;
|
|
1489
|
-
publishEntry("Create", parent, name);
|
|
1490
|
-
}
|
|
1491
|
-
else {
|
|
1492
|
-
if (file.kind === "symlink")
|
|
1493
|
-
return yield* failure("SymlinkLoop", "open", input);
|
|
1494
|
-
if (file.kind !== "file")
|
|
1495
|
-
return yield* failure("IsDirectory", "open", input);
|
|
1496
|
-
if (path.trailingSlash)
|
|
1497
|
-
return yield* failure("NotDirectory", "open", input);
|
|
1498
|
-
yield* authorize(file, identity, chosen.access === "read" ? 4 : chosen.access === "write" ? 2 : 6, "open", input);
|
|
1499
|
-
if (chosen.truncate)
|
|
1500
|
-
yield* resize(file, 0n, "open");
|
|
1501
|
-
}
|
|
1502
|
-
file.openCount += 1;
|
|
1503
|
-
acquired.file = file;
|
|
1504
|
-
return fileHandle(acquired);
|
|
1505
|
-
}));
|
|
1506
|
-
}),
|
|
1507
|
-
unlink: Effect.fn("Caller.unlink")(function* (input, options) {
|
|
1508
|
-
const prepared = preparePath(input, "unlink", settings.maxPathBytes);
|
|
1509
|
-
const base = options?.relativeTo;
|
|
1510
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1511
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1512
|
-
const parent = yield* locate(path, base, "unlink", { parentOnly: true });
|
|
1513
|
-
yield* authorize(parent, identity, 3, "unlink", input);
|
|
1514
|
-
const name = path.components.at(-1);
|
|
1515
|
-
if (name === undefined || name === "2e" || name === "2e2e") {
|
|
1516
|
-
return yield* failure("IsDirectory", "unlink", input);
|
|
1517
|
-
}
|
|
1518
|
-
const child = parent.entries.get(name);
|
|
1519
|
-
if (child === undefined)
|
|
1520
|
-
return yield* failure("NotFound", "unlink", input);
|
|
1521
|
-
if (child.kind === "directory")
|
|
1522
|
-
return yield* failure("IsDirectory", "unlink", input);
|
|
1523
|
-
if (path.trailingSlash)
|
|
1524
|
-
return yield* failure("NotDirectory", "unlink", input);
|
|
1525
|
-
yield* authorizeRemoval(parent, child, "unlink", input);
|
|
1526
|
-
const now = yield* timestamp("unlink");
|
|
1527
|
-
parent.entries.delete(name);
|
|
1528
|
-
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1529
|
-
detach(child, now);
|
|
1530
|
-
entries -= 1;
|
|
1531
|
-
publishEntry("Remove", parent, name);
|
|
1532
|
-
}));
|
|
1533
|
-
}),
|
|
1534
|
-
rename: Effect.fn("Caller.rename")(function* (source, destination, options) {
|
|
1535
|
-
const oldPrepared = preparePath(source, "rename", settings.maxPathBytes);
|
|
1536
|
-
const newPrepared = preparePath(destination, "rename", settings.maxPathBytes);
|
|
1537
|
-
const oldBase = options?.sourceRelativeTo;
|
|
1538
|
-
const newBase = options?.destinationRelativeTo;
|
|
1539
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1540
|
-
const oldPath = yield* Effect.fromResult(oldPrepared);
|
|
1541
|
-
const newPath = yield* Effect.fromResult(newPrepared);
|
|
1542
|
-
const oldParent = yield* locate(oldPath, oldBase, "rename", { parentOnly: true });
|
|
1543
|
-
const newParent = yield* locate(newPath, newBase, "rename", { parentOnly: true });
|
|
1544
|
-
yield* authorize(oldParent, identity, 3, "rename", source);
|
|
1545
|
-
yield* authorize(newParent, identity, 3, "rename", destination);
|
|
1546
|
-
const oldName = oldPath.components.at(-1);
|
|
1547
|
-
const newName = newPath.components.at(-1);
|
|
1548
|
-
if (oldName === undefined || newName === undefined || oldName === "2e" || oldName === "2e2e" ||
|
|
1549
|
-
newName === "2e" || newName === "2e2e") {
|
|
1550
|
-
return yield* failure("InvalidArgument", "rename", source);
|
|
1551
|
-
}
|
|
1552
|
-
const child = oldParent.entries.get(oldName);
|
|
1553
|
-
if (child === undefined)
|
|
1554
|
-
return yield* failure("NotFound", "rename", source);
|
|
1555
|
-
const replaced = newParent.entries.get(newName);
|
|
1556
|
-
if (newPath.trailingSlash && replaced === undefined) {
|
|
1557
|
-
return yield* failure("NotFound", "rename", destination);
|
|
1558
|
-
}
|
|
1559
|
-
if (oldPath.trailingSlash && child.kind !== "directory") {
|
|
1560
|
-
return yield* failure("NotDirectory", "rename", source);
|
|
1561
|
-
}
|
|
1562
|
-
if (newPath.trailingSlash && replaced?.kind !== "directory") {
|
|
1563
|
-
return yield* failure("NotDirectory", "rename", destination);
|
|
1564
|
-
}
|
|
1565
|
-
if (child === replaced)
|
|
1566
|
-
return;
|
|
1567
|
-
yield* authorizeRemoval(oldParent, child, "rename", source);
|
|
1568
|
-
if (replaced !== undefined) {
|
|
1569
|
-
yield* authorizeRemoval(newParent, replaced, "rename", destination);
|
|
1570
|
-
if (child.kind === "directory" && replaced.kind !== "directory") {
|
|
1571
|
-
return yield* failure("NotDirectory", "rename", destination);
|
|
1572
|
-
}
|
|
1573
|
-
if (child.kind !== "directory" && replaced.kind === "directory") {
|
|
1574
|
-
return yield* failure("IsDirectory", "rename", destination);
|
|
1575
|
-
}
|
|
1576
|
-
if (replaced.kind === "directory" && replaced.entries.size > 0) {
|
|
1577
|
-
return yield* failure("NotEmpty", "rename", destination);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
for (let ancestor = newParent; ancestor !== undefined; ancestor = ancestor.parent) {
|
|
1581
|
-
if (ancestor === child)
|
|
1582
|
-
return yield* failure("InvalidArgument", "rename", destination);
|
|
1583
|
-
}
|
|
1584
|
-
const now = yield* timestamp("rename");
|
|
1585
|
-
// All rejection checks precede namespace, ancestry, quota, and metadata publication.
|
|
1586
|
-
const oldEvent = subscribers > 0
|
|
1587
|
-
? ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === root ? "" : "2f") + oldName))
|
|
1588
|
-
: undefined;
|
|
1589
|
-
oldParent.entries.delete(oldName);
|
|
1590
|
-
newParent.entries.set(newName, child);
|
|
1591
|
-
if (child.kind === "directory")
|
|
1592
|
-
child.parent = newParent;
|
|
1593
|
-
oldParent.metadata = {
|
|
1594
|
-
...oldParent.metadata,
|
|
1595
|
-
nlink: oldParent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
|
|
1596
|
-
mtimeNs: now,
|
|
1597
|
-
ctimeNs: now
|
|
1598
|
-
};
|
|
1599
|
-
newParent.metadata = {
|
|
1600
|
-
...newParent.metadata,
|
|
1601
|
-
nlink: newParent.metadata.nlink + (child.kind === "directory" && replaced === undefined ? 1 : 0),
|
|
1602
|
-
mtimeNs: now,
|
|
1603
|
-
ctimeNs: now
|
|
1604
|
-
};
|
|
1605
|
-
child.metadata = { ...child.metadata, ctimeNs: now };
|
|
1606
|
-
if (replaced !== undefined) {
|
|
1607
|
-
detach(replaced, now);
|
|
1608
|
-
entries -= 1;
|
|
1609
|
-
}
|
|
1610
|
-
if (oldEvent !== undefined)
|
|
1611
|
-
PubSub.publishUnsafe(events, { _tag: "Remove", path: oldEvent });
|
|
1612
|
-
publishEntry("Create", newParent, newName);
|
|
1613
|
-
}));
|
|
1614
|
-
}),
|
|
1615
|
-
rmdir: Effect.fn("Caller.rmdir")(function* (input, options) {
|
|
1616
|
-
const prepared = preparePath(input, "rmdir", settings.maxPathBytes);
|
|
1617
|
-
const base = options?.relativeTo;
|
|
1618
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1619
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1620
|
-
const parent = yield* locate(path, base, "rmdir", { parentOnly: true });
|
|
1621
|
-
yield* authorize(parent, identity, 3, "rmdir", input);
|
|
1622
|
-
const name = path.components.at(-1);
|
|
1623
|
-
if (name === undefined || name === "2e" || name === "2e2e") {
|
|
1624
|
-
return yield* failure("InvalidArgument", "rmdir", input);
|
|
1625
|
-
}
|
|
1626
|
-
const child = parent.entries.get(name);
|
|
1627
|
-
if (child === undefined)
|
|
1628
|
-
return yield* failure("NotFound", "rmdir", input);
|
|
1629
|
-
yield* authorizeRemoval(parent, child, "rmdir", input);
|
|
1630
|
-
if (child.kind !== "directory")
|
|
1631
|
-
return yield* failure("NotDirectory", "rmdir", input);
|
|
1632
|
-
if (child.entries.size > 0)
|
|
1633
|
-
return yield* failure("NotEmpty", "rmdir", input);
|
|
1634
|
-
const now = yield* timestamp("rmdir");
|
|
1635
|
-
parent.entries.delete(name);
|
|
1636
|
-
parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink - 1, mtimeNs: now, ctimeNs: now };
|
|
1637
|
-
child.parent = undefined;
|
|
1638
|
-
child.metadata = { ...child.metadata, nlink: 0, ctimeNs: now };
|
|
1639
|
-
entries -= 1;
|
|
1640
|
-
publishEntry("Remove", parent, name);
|
|
1641
|
-
}));
|
|
1642
|
-
}),
|
|
1643
|
-
stat: Effect.fn("Caller.stat")(function* (input, options) {
|
|
1644
|
-
const prepared = preparePath(input, "stat", settings.maxPathBytes);
|
|
1645
|
-
const base = options?.relativeTo;
|
|
1646
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1647
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1648
|
-
const directory = yield* resolveNode(path, base, "stat");
|
|
1649
|
-
return { ...directory.metadata };
|
|
1650
|
-
}));
|
|
1651
|
-
}),
|
|
1652
|
-
mkdir: Effect.fn("Caller.mkdir")(function* (input, options) {
|
|
1653
|
-
const prepared = preparePath(input, "mkdir", settings.maxPathBytes);
|
|
1654
|
-
const base = options?.relativeTo;
|
|
1655
|
-
const mode = options?.mode === undefined ? 0o777 : options.mode;
|
|
1656
|
-
if (!Schema.is(Mode)(mode))
|
|
1657
|
-
return yield* failure("InvalidArgument", "mkdir", input);
|
|
1658
|
-
return yield* coordinated(Effect.gen(function* () {
|
|
1659
|
-
const path = yield* Effect.fromResult(prepared);
|
|
1660
|
-
const parent = yield* locate(path, base, "mkdir", { parentOnly: true });
|
|
1661
|
-
yield* authorize(parent, identity, 3, "mkdir", input);
|
|
1662
|
-
const name = path.components.at(-1);
|
|
1663
|
-
if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
|
|
1664
|
-
return yield* failure("AlreadyExists", "mkdir", input);
|
|
1665
|
-
}
|
|
1666
|
-
if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
|
|
1667
|
-
return yield* failure("NoSpace", "mkdir", input);
|
|
1668
|
-
}
|
|
1669
|
-
const now = yield* timestamp("mkdir");
|
|
1670
|
-
const child = {
|
|
1671
|
-
kind: "directory",
|
|
1672
|
-
lineage: undefined,
|
|
1673
|
-
parent,
|
|
1674
|
-
entries: new Map(),
|
|
1675
|
-
metadata: directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & 0o1000), now)
|
|
1676
|
-
};
|
|
1677
|
-
const parentMetadata = {
|
|
1678
|
-
...parent.metadata,
|
|
1679
|
-
nlink: parent.metadata.nlink + 1,
|
|
1680
|
-
mtimeNs: now,
|
|
1681
|
-
ctimeNs: now
|
|
1682
|
-
};
|
|
1683
|
-
// No Effect yield or expected failure between these publication writes.
|
|
1684
|
-
parent.entries.set(name, child);
|
|
1685
|
-
parent.metadata = parentMetadata;
|
|
1686
|
-
nextInode += 1n;
|
|
1687
|
-
entries += 1;
|
|
1688
|
-
publishEntry("Create", parent, name);
|
|
1689
|
-
}));
|
|
1690
|
-
}),
|
|
1691
|
-
withDirectory: Effect.fn("Caller.withDirectory")(function* (input, options) {
|
|
1692
|
-
const acquired = yield* acquireDirectory(input, options, "withDirectory");
|
|
1693
|
-
return createCaller(acquired, identity, umask);
|
|
1694
|
-
}),
|
|
1695
|
-
openDirectory: Effect.fn("Caller.openDirectory")(function* (input, options) {
|
|
1696
|
-
const acquired = yield* acquireDirectory(input, options, "openDirectory");
|
|
1697
|
-
const handle = Object.freeze({
|
|
1698
|
-
[DirectoryHandleId]: true,
|
|
1699
|
-
stat: coordinated(Effect.suspend(() => acquired.directory === undefined
|
|
1700
|
-
? Effect.fail(failure("InvalidHandle", "stat"))
|
|
1701
|
-
: Effect.succeed({ ...acquired.directory.metadata }))).pipe(Effect.withSpan("DirectoryHandle.stat")),
|
|
1702
|
-
close: coordinated(Effect.suspend(() => {
|
|
1703
|
-
if (acquired.directory === undefined)
|
|
1704
|
-
return Effect.fail(failure("InvalidHandle", "close"));
|
|
1705
|
-
acquired.directory = undefined;
|
|
1706
|
-
acquired.closed = true;
|
|
1707
|
-
return Effect.void;
|
|
1708
|
-
})).pipe(Effect.withSpan("DirectoryHandle.close"))
|
|
1709
|
-
});
|
|
1710
|
-
handles.set(handle, acquired);
|
|
1711
|
-
return handle;
|
|
1712
|
-
})
|
|
1713
|
-
});
|
|
1714
|
-
};
|
|
1715
|
-
const baseObservation = source._tag === "Overlay" ? observeChanges() : undefined;
|
|
1716
|
-
const publicChange = (change) => {
|
|
1717
|
-
switch (change._tag) {
|
|
1718
|
-
case "Added":
|
|
1719
|
-
case "Removed":
|
|
1720
|
-
case "Updated":
|
|
1721
|
-
case "Replaced":
|
|
1722
|
-
return Object.freeze({ ...change, path: ownedPath(new Uint8Array(change.path)) });
|
|
1723
|
-
case "Renamed":
|
|
1724
|
-
return Object.freeze({
|
|
1725
|
-
...change,
|
|
1726
|
-
from: ownedPath(new Uint8Array(change.from)),
|
|
1727
|
-
to: ownedPath(new Uint8Array(change.to))
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
throw new Error("Unknown internal overlay change");
|
|
1731
|
-
};
|
|
1732
|
-
const publicChanges = (changes) => Object.freeze(changes.map(publicChange));
|
|
1733
|
-
const changeOptions = (options) => {
|
|
1734
|
-
const decoded = decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options);
|
|
1735
|
-
return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
|
|
1736
|
-
};
|
|
1737
|
-
const volume = Object.freeze({
|
|
1738
|
-
[VolumeId]: true,
|
|
1739
|
-
watch: Effect.gen(function* () {
|
|
1740
|
-
const subscription = yield* PubSub.subscribe(events);
|
|
1741
|
-
subscribers += 1;
|
|
1742
|
-
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
1743
|
-
subscribers -= 1;
|
|
1744
|
-
}));
|
|
1745
|
-
return Stream.fromEffectRepeat(PubSub.take(subscription));
|
|
1746
|
-
}).pipe(Effect.withSpan("Volume.watch")),
|
|
1747
|
-
snapshot: coordinated(captureSnapshot()).pipe(Effect.withSpan("Volume.snapshot")),
|
|
1748
|
-
caller: Effect.fn("Volume.caller")(function* (options) {
|
|
1749
|
-
const decoded = decodeConfiguration(RootCallerOptions, options === undefined ? {} : options);
|
|
1750
|
-
if (Result.isFailure(decoded))
|
|
1751
|
-
return yield* decoded.failure;
|
|
1752
|
-
const chosen = decoded.success.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
|
|
1753
|
-
const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
|
|
1754
|
-
return createCaller({ volume: volumeIdentity, directory: root, closed: false }, identity, decoded.success.umask ?? 0o022);
|
|
1755
|
-
})
|
|
1756
|
-
});
|
|
1757
|
-
if (source._tag !== "Overlay" || baseObservation === undefined) {
|
|
1758
|
-
return { _tag: "Volume", volume };
|
|
1759
|
-
}
|
|
1760
|
-
const hook = OverlayTesting.getObservationHook(source.base);
|
|
1761
|
-
const overlay = Object.freeze({
|
|
1762
|
-
...volume,
|
|
1763
|
-
changes: Effect.fn("OverlayVolume.changes")(function* (options) {
|
|
1764
|
-
const selected = yield* changeOptions(options);
|
|
1765
|
-
const current = yield* coordinated(Effect.sync(observeChanges));
|
|
1766
|
-
return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
|
|
1767
|
-
}),
|
|
1768
|
-
capture: Effect.fn("OverlayVolume.capture")(function* (options) {
|
|
1769
|
-
const selected = yield* changeOptions(options);
|
|
1770
|
-
const current = yield* coordinated(captureState(hook));
|
|
1771
|
-
return Object.freeze({
|
|
1772
|
-
snapshot: current.snapshot,
|
|
1773
|
-
changes: publicChanges(compareOverlay(baseObservation, current.observation, selected.includeTimestamps ?? false))
|
|
1774
|
-
});
|
|
1775
|
-
})
|
|
1776
|
-
});
|
|
1777
|
-
return { _tag: "Overlay", volume: overlay };
|
|
1778
|
-
});
|
|
675
|
+
export const pathToBytes = Path.pathToBytes;
|
|
676
|
+
/**
|
|
677
|
+
* Schema for a complete fixture namespace with optional metadata and forward hard links.
|
|
678
|
+
*
|
|
679
|
+
* **Details**
|
|
680
|
+
*
|
|
681
|
+
* Fixture paths must be absolute, unique, and explicitly include their parent directories.
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* ```ts
|
|
685
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
686
|
+
* import { Effect, Schema } from "effect"
|
|
687
|
+
*
|
|
688
|
+
* // Fixtures must list every parent directory before its children.
|
|
689
|
+
* const program = Schema.decodeUnknownEffect(Vfs.Fixture)({
|
|
690
|
+
* entries: [
|
|
691
|
+
* { kind: "file", path: "/etc/hosts", bytes: new TextEncoder().encode("127.0.0.1") }
|
|
692
|
+
* ]
|
|
693
|
+
* }).pipe(
|
|
694
|
+
* Effect.flatMap(Vfs.fromFixture),
|
|
695
|
+
* Effect.as("built"),
|
|
696
|
+
* Effect.catchTag("ImageError", (error) => Effect.succeed(`rejected: ${error.code}`))
|
|
697
|
+
* )
|
|
698
|
+
*
|
|
699
|
+
* Effect.runPromise(program).then(console.log)
|
|
700
|
+
* // rejected: InvalidStructure
|
|
701
|
+
* ```
|
|
702
|
+
*
|
|
703
|
+
* @category schemas
|
|
704
|
+
* @since 0.1.0
|
|
705
|
+
*/
|
|
706
|
+
export const Fixture = VfsModel.Fixture;
|
|
1779
707
|
/**
|
|
1780
708
|
* Creates a fresh empty volume and captures the current Effect `Clock`.
|
|
1781
709
|
*
|
|
@@ -1784,28 +712,71 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (source,
|
|
|
1784
712
|
* Each execution creates independent storage. Snapshot image failures cannot
|
|
1785
713
|
* arise because this constructor does not accept persisted input.
|
|
1786
714
|
*
|
|
715
|
+
* @example
|
|
716
|
+
* ```ts
|
|
717
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
718
|
+
* import { Effect } from "effect"
|
|
719
|
+
*
|
|
720
|
+
* const program = Effect.gen(function*() {
|
|
721
|
+
* const volume = yield* Vfs.make()
|
|
722
|
+
* const caller = yield* volume.caller()
|
|
723
|
+
*
|
|
724
|
+
* yield* caller.mkdir("/work")
|
|
725
|
+
* yield* caller.writeFile("/work/notes.txt", new Uint8Array([104, 105]), { access: "write", create: "exclusive" })
|
|
726
|
+
*
|
|
727
|
+
* return yield* caller.readFile("/work/notes.txt")
|
|
728
|
+
* })
|
|
729
|
+
*
|
|
730
|
+
* Effect.runPromise(program).then(console.log)
|
|
731
|
+
* // Uint8Array(2) [ 104, 105 ]
|
|
732
|
+
* ```
|
|
733
|
+
*
|
|
734
|
+
* @see The testing guide at `/guides/testing-with-an-isolated-filesystem`.
|
|
1787
735
|
* @category constructors
|
|
1788
736
|
* @since 0.1.0
|
|
1789
737
|
*/
|
|
1790
|
-
export const make =
|
|
1791
|
-
const result = yield* makeVolume({ _tag: "Empty" }, options).pipe(Effect.catchTag("ImageError", Effect.die));
|
|
1792
|
-
if (result._tag === "Overlay")
|
|
1793
|
-
return yield* Effect.die(new Error("empty volume constructed as overlay"));
|
|
1794
|
-
return result.volume;
|
|
1795
|
-
});
|
|
738
|
+
export const make = VfsModel.make;
|
|
1796
739
|
/**
|
|
1797
740
|
* Restores a fresh volume from an opaque snapshot under the supplied destination limits.
|
|
1798
741
|
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```ts
|
|
744
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
745
|
+
* import { Effect } from "effect"
|
|
746
|
+
*
|
|
747
|
+
* const program = Effect.gen(function*() {
|
|
748
|
+
* const original = yield* Vfs.make()
|
|
749
|
+
* const source = yield* original.caller()
|
|
750
|
+
*
|
|
751
|
+
* yield* source.writeFile("/seed.txt", new TextEncoder().encode("one"), {
|
|
752
|
+
* access: "write",
|
|
753
|
+
* create: "exclusive"
|
|
754
|
+
* })
|
|
755
|
+
*
|
|
756
|
+
* const restored = yield* Vfs.fromSnapshot(yield* original.snapshot)
|
|
757
|
+
* const copy = yield* restored.caller()
|
|
758
|
+
*
|
|
759
|
+
* // Writing to the restore leaves the original untouched: the two are independent.
|
|
760
|
+
* yield* copy.writeFile("/seed.txt", new TextEncoder().encode("two"), {
|
|
761
|
+
* access: "write",
|
|
762
|
+
* truncate: true
|
|
763
|
+
* })
|
|
764
|
+
*
|
|
765
|
+
* return [
|
|
766
|
+
* new TextDecoder().decode(yield* source.readFile("/seed.txt")),
|
|
767
|
+
* new TextDecoder().decode(yield* copy.readFile("/seed.txt"))
|
|
768
|
+
* ]
|
|
769
|
+
* })
|
|
770
|
+
*
|
|
771
|
+
* Effect.runPromise(program).then(console.log)
|
|
772
|
+
* // [ 'one', 'two' ]
|
|
773
|
+
* ```
|
|
774
|
+
*
|
|
775
|
+
* @see {@link makeOverlay} for a copy-on-write workspace over the same base.
|
|
1799
776
|
* @category constructors
|
|
1800
777
|
* @since 0.1.0
|
|
1801
778
|
*/
|
|
1802
|
-
export const fromSnapshot =
|
|
1803
|
-
const image = yield* Image.inspect(snapshot);
|
|
1804
|
-
const result = yield* makeVolume({ _tag: "Snapshot", image }, options);
|
|
1805
|
-
if (result._tag === "Overlay")
|
|
1806
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
1807
|
-
return result.volume;
|
|
1808
|
-
});
|
|
779
|
+
export const fromSnapshot = VfsModel.fromSnapshot;
|
|
1809
780
|
/**
|
|
1810
781
|
* Creates an isolated writable volume relative to one immutable snapshot base.
|
|
1811
782
|
*
|
|
@@ -1819,199 +790,76 @@ export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function
|
|
|
1819
790
|
* Invalid base snapshots fail with `ImageError`; invalid volume limits fail
|
|
1820
791
|
* with `ConfigurationError`. Each execution creates a fresh workspace.
|
|
1821
792
|
*
|
|
1822
|
-
* @
|
|
1823
|
-
*
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
const image = yield* Image.inspect(base);
|
|
1827
|
-
const result = yield* makeVolume({ _tag: "Overlay", base, image }, options);
|
|
1828
|
-
if (result._tag === "Volume")
|
|
1829
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
1830
|
-
return result.volume;
|
|
1831
|
-
});
|
|
1832
|
-
const FixtureMetadata = Schema.Struct({
|
|
1833
|
-
uid: Schema.optionalKey(Natural),
|
|
1834
|
-
gid: Schema.optionalKey(Natural),
|
|
1835
|
-
mode: Schema.optionalKey(Mode),
|
|
1836
|
-
atimeNs: Schema.optionalKey(Timestamp),
|
|
1837
|
-
mtimeNs: Schema.optionalKey(Timestamp),
|
|
1838
|
-
ctimeNs: Schema.optionalKey(Timestamp),
|
|
1839
|
-
birthtimeNs: Schema.optionalKey(Timestamp)
|
|
1840
|
-
});
|
|
1841
|
-
const FixturePath = Schema.Union([
|
|
1842
|
-
Schema.String,
|
|
1843
|
-
BytePath
|
|
1844
|
-
]);
|
|
1845
|
-
/**
|
|
1846
|
-
* Schema for a complete fixture namespace with optional metadata and forward hard links.
|
|
793
|
+
* @example
|
|
794
|
+
* ```ts
|
|
795
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
796
|
+
* import { Effect } from "effect"
|
|
1847
797
|
*
|
|
1848
|
-
*
|
|
798
|
+
* const program = Effect.gen(function*() {
|
|
799
|
+
* const base = yield* Vfs.make()
|
|
800
|
+
* const origin = yield* base.caller()
|
|
1849
801
|
*
|
|
1850
|
-
*
|
|
802
|
+
* yield* origin.writeFile("/config.json", new TextEncoder().encode("{}"), {
|
|
803
|
+
* access: "write",
|
|
804
|
+
* create: "exclusive"
|
|
805
|
+
* })
|
|
1851
806
|
*
|
|
1852
|
-
*
|
|
807
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
808
|
+
*
|
|
809
|
+
* yield* (yield* workspace.caller()).writeFile("/config.json", new TextEncoder().encode("[]"), {
|
|
810
|
+
* access: "write",
|
|
811
|
+
* truncate: true
|
|
812
|
+
* })
|
|
813
|
+
*
|
|
814
|
+
* // The write landed in the workspace only; the base still reads its own value.
|
|
815
|
+
* return [
|
|
816
|
+
* new TextDecoder().decode(yield* origin.readFile("/config.json")),
|
|
817
|
+
* (yield* workspace.changes()).map((change) => change._tag)
|
|
818
|
+
* ]
|
|
819
|
+
* })
|
|
820
|
+
*
|
|
821
|
+
* Effect.runPromise(program).then(console.log)
|
|
822
|
+
* // [ '{}', [ 'Updated' ] ]
|
|
823
|
+
* ```
|
|
824
|
+
*
|
|
825
|
+
* @see The overlay guide at `/guides/overlay-filesystems`.
|
|
826
|
+
* @category constructors
|
|
1853
827
|
* @since 0.1.0
|
|
1854
828
|
*/
|
|
1855
|
-
export const
|
|
1856
|
-
rootMetadata: Schema.optionalKey(FixtureMetadata),
|
|
1857
|
-
entries: Schema.Array(Schema.Union([
|
|
1858
|
-
Schema.Struct({
|
|
1859
|
-
kind: Schema.Literal("directory"),
|
|
1860
|
-
path: FixturePath,
|
|
1861
|
-
metadata: Schema.optionalKey(FixtureMetadata)
|
|
1862
|
-
}),
|
|
1863
|
-
Schema.Struct({
|
|
1864
|
-
kind: Schema.Literal("file"),
|
|
1865
|
-
path: FixturePath,
|
|
1866
|
-
bytes: Schema.Uint8Array,
|
|
1867
|
-
metadata: Schema.optionalKey(FixtureMetadata)
|
|
1868
|
-
}),
|
|
1869
|
-
Schema.Struct({
|
|
1870
|
-
kind: Schema.Literal("symlink"),
|
|
1871
|
-
path: FixturePath,
|
|
1872
|
-
target: FixturePath,
|
|
1873
|
-
metadata: Schema.optionalKey(FixtureMetadata)
|
|
1874
|
-
}),
|
|
1875
|
-
Schema.Struct({ kind: Schema.Literal("hardLink"), path: FixturePath, target: FixturePath })
|
|
1876
|
-
]))
|
|
1877
|
-
});
|
|
829
|
+
export const makeOverlay = VfsModel.makeOverlay;
|
|
1878
830
|
/**
|
|
1879
831
|
* Builds a fresh volume from a validated final-state fixture.
|
|
1880
832
|
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```ts
|
|
835
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
836
|
+
* import { Effect } from "effect"
|
|
837
|
+
*
|
|
838
|
+
* const encoder = new TextEncoder()
|
|
839
|
+
*
|
|
840
|
+
* const program = Effect.gen(function*() {
|
|
841
|
+
* // Parent directories must be listed explicitly, before their children.
|
|
842
|
+
* const volume = yield* Vfs.fromFixture({
|
|
843
|
+
* entries: [
|
|
844
|
+
* { kind: "directory", path: "/project" },
|
|
845
|
+
* {
|
|
846
|
+
* kind: "file",
|
|
847
|
+
* path: "/project/package.json",
|
|
848
|
+
* bytes: encoder.encode(`{"version":"1.2.3"}`)
|
|
849
|
+
* },
|
|
850
|
+
* { kind: "symlink", path: "/project/latest", target: "/project/package.json" }
|
|
851
|
+
* ]
|
|
852
|
+
* })
|
|
853
|
+
*
|
|
854
|
+
* return yield* (yield* volume.caller()).readDirectory("/project")
|
|
855
|
+
* })
|
|
856
|
+
*
|
|
857
|
+
* Effect.runPromise(program).then(console.log)
|
|
858
|
+
* // [ 'package.json', 'latest' ]
|
|
859
|
+
* ```
|
|
860
|
+
*
|
|
861
|
+
* @see The fixtures guide at `/guides/fixtures-and-snapshots`.
|
|
1881
862
|
* @category constructors
|
|
1882
863
|
* @since 0.1.0
|
|
1883
864
|
*/
|
|
1884
|
-
export const fromFixture =
|
|
1885
|
-
const config = decodeConfiguration(VolumeOptions, options ?? {});
|
|
1886
|
-
if (Result.isFailure(config))
|
|
1887
|
-
return yield* config.failure;
|
|
1888
|
-
const decoded = Schema.decodeResult(Fixture, { onExcessProperty: "error" })(fixture);
|
|
1889
|
-
if (Result.isFailure(decoded))
|
|
1890
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "fixture" });
|
|
1891
|
-
const source = decoded.success;
|
|
1892
|
-
for (const entry of source.entries) {
|
|
1893
|
-
if (entry.kind === "file" && (!(entry.bytes.buffer instanceof ArrayBuffer) || !attachedBuffer(entry.bytes)))
|
|
1894
|
-
return yield* new ImageError({ code: "InvalidEncoding", field: "bytes" });
|
|
1895
|
-
}
|
|
1896
|
-
const metadata = (kind, overrides) => ({
|
|
1897
|
-
uid: overrides?.uid ?? 0,
|
|
1898
|
-
gid: overrides?.gid ?? 0,
|
|
1899
|
-
mode: overrides?.mode ?? (kind === "directory" ? 0o755 : kind === "file" ? 0o644 : 0o777),
|
|
1900
|
-
atimeNs: String(overrides?.atimeNs ?? 0n),
|
|
1901
|
-
mtimeNs: String(overrides?.mtimeNs ?? 0n),
|
|
1902
|
-
ctimeNs: String(overrides?.ctimeNs ?? 0n),
|
|
1903
|
-
birthtimeNs: String(overrides?.birthtimeNs ?? 0n)
|
|
1904
|
-
});
|
|
1905
|
-
const declarations = new Map();
|
|
1906
|
-
const aliases = new Map();
|
|
1907
|
-
const paths = new Map();
|
|
1908
|
-
const root = {
|
|
1909
|
-
id: "root",
|
|
1910
|
-
kind: "directory",
|
|
1911
|
-
metadata: metadata("directory", source.rootMetadata),
|
|
1912
|
-
entries: []
|
|
1913
|
-
};
|
|
1914
|
-
declarations.set("", root);
|
|
1915
|
-
const fixturePath = (input) => preparePath(input, "fixture", config.success.maxPathBytes).pipe(Result.flatMap((path) => !path.absolute || path.components.length === 0 ||
|
|
1916
|
-
path.components.some((name) => name === "2e" || name === "2e2e")
|
|
1917
|
-
? Result.fail(failure("InvalidArgument", "fixture", input)) :
|
|
1918
|
-
Result.succeed(path.components)));
|
|
1919
|
-
// All byte inputs become immutable strings before the first successful suspension.
|
|
1920
|
-
for (const entry of source.entries) {
|
|
1921
|
-
const parsed = fixturePath(entry.path);
|
|
1922
|
-
if (Result.isFailure(parsed)) {
|
|
1923
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "path" });
|
|
1924
|
-
}
|
|
1925
|
-
const components = parsed.success;
|
|
1926
|
-
const key = components.join("/");
|
|
1927
|
-
if (paths.has(key))
|
|
1928
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "duplicate" });
|
|
1929
|
-
paths.set(key, components);
|
|
1930
|
-
if (entry.kind === "hardLink") {
|
|
1931
|
-
const target = fixturePath(entry.target);
|
|
1932
|
-
if (Result.isFailure(target))
|
|
1933
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "target" });
|
|
1934
|
-
aliases.set(key, target.success.join("/"));
|
|
1935
|
-
}
|
|
1936
|
-
else if (entry.kind === "directory") {
|
|
1937
|
-
declarations.set(key, {
|
|
1938
|
-
id: String(paths.size),
|
|
1939
|
-
kind: "directory",
|
|
1940
|
-
metadata: metadata("directory", entry.metadata),
|
|
1941
|
-
entries: []
|
|
1942
|
-
});
|
|
1943
|
-
}
|
|
1944
|
-
else if (entry.kind === "file") {
|
|
1945
|
-
declarations.set(key, {
|
|
1946
|
-
id: String(paths.size),
|
|
1947
|
-
kind: "file",
|
|
1948
|
-
metadata: metadata("file", entry.metadata),
|
|
1949
|
-
data: Image.base64(entry.bytes)
|
|
1950
|
-
});
|
|
1951
|
-
}
|
|
1952
|
-
else {
|
|
1953
|
-
if (typeof entry.target === "string" && !wellFormed(entry.target)) {
|
|
1954
|
-
return yield* new ImageError({
|
|
1955
|
-
code: "InvalidEncoding",
|
|
1956
|
-
field: "target"
|
|
1957
|
-
});
|
|
1958
|
-
}
|
|
1959
|
-
const target = typeof entry.target === "string"
|
|
1960
|
-
? new TextEncoder().encode(entry.target)
|
|
1961
|
-
: getBytePathBytes(entry.target);
|
|
1962
|
-
if (target === undefined || target.includes(0)) {
|
|
1963
|
-
return yield* new ImageError({
|
|
1964
|
-
code: "InvalidStructure",
|
|
1965
|
-
field: "target"
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
declarations.set(key, {
|
|
1969
|
-
id: String(paths.size),
|
|
1970
|
-
kind: "symlink",
|
|
1971
|
-
metadata: metadata("symlink", entry.metadata),
|
|
1972
|
-
target: Image.base64(target)
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
for (const [key] of aliases) {
|
|
1977
|
-
let target = key;
|
|
1978
|
-
const seen = new Set();
|
|
1979
|
-
while (!declarations.has(target)) {
|
|
1980
|
-
if (seen.has(target))
|
|
1981
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "hardLink" });
|
|
1982
|
-
seen.add(target);
|
|
1983
|
-
const next = aliases.get(target);
|
|
1984
|
-
if (next === undefined)
|
|
1985
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "hardLink" });
|
|
1986
|
-
target = next;
|
|
1987
|
-
}
|
|
1988
|
-
const node = declarations.get(target);
|
|
1989
|
-
if (node === undefined || node.kind === "directory") {
|
|
1990
|
-
return yield* new ImageError({
|
|
1991
|
-
code: "InvalidStructure",
|
|
1992
|
-
field: "hardLink"
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
for (const alias of seen)
|
|
1996
|
-
declarations.set(alias, node);
|
|
1997
|
-
}
|
|
1998
|
-
const children = new Map();
|
|
1999
|
-
for (const [key, components] of paths) {
|
|
2000
|
-
const parent = declarations.get(components.slice(0, -1).join("/"));
|
|
2001
|
-
const child = declarations.get(key);
|
|
2002
|
-
const name = components.at(-1);
|
|
2003
|
-
if (parent?.kind !== "directory" || child === undefined || name === undefined) {
|
|
2004
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "parent" });
|
|
2005
|
-
}
|
|
2006
|
-
const entries = children.get(parent.id) ?? [];
|
|
2007
|
-
entries.push({ name: Image.base64(nameBytes(name)), target: child.id });
|
|
2008
|
-
children.set(parent.id, entries);
|
|
2009
|
-
}
|
|
2010
|
-
const records = [...new Set(declarations.values())].map((record) => record.kind === "directory" ? { ...record, entries: children.get(record.id) ?? [] } : record);
|
|
2011
|
-
const snapshot = yield* Image.capture({ format: "effect-vfs", version: 1, root: "root", records });
|
|
2012
|
-
const image = yield* Image.inspect(snapshot);
|
|
2013
|
-
const result = yield* makeVolume({ _tag: "Snapshot", image }, config.success);
|
|
2014
|
-
if (result._tag === "Overlay")
|
|
2015
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
2016
|
-
return result.volume;
|
|
2017
|
-
});
|
|
865
|
+
export const fromFixture = FixtureInternal.fromFixture;
|