@poe-platform/safe-fs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/safe-fs/bridge/confinement.d.ts +3 -0
- package/dist/safe-fs/bridge/confinement.js +171 -0
- package/dist/safe-fs/bridge/filesystem.d.ts +88 -0
- package/dist/safe-fs/bridge/filesystem.js +411 -0
- package/dist/safe-fs/bridge/index.d.ts +6 -0
- package/dist/safe-fs/bridge/index.js +18 -0
- package/dist/safe-fs/bridge/stats.d.ts +4 -0
- package/dist/safe-fs/bridge/stats.js +37 -0
- package/dist/safe-fs/bridge/types.d.ts +75 -0
- package/dist/safe-fs/bridge/types.js +1 -0
- package/dist/safe-fs/bridge/values.d.ts +6 -0
- package/dist/safe-fs/bridge/values.js +65 -0
- package/dist/safe-fs/config/memory.d.ts +3 -0
- package/dist/safe-fs/config/memory.js +9 -0
- package/dist/safe-fs/config/real.d.ts +5 -0
- package/dist/safe-fs/config/real.js +16 -0
- package/dist/safe-fs/config.d.ts +15 -0
- package/dist/safe-fs/config.js +46 -0
- package/dist/safe-fs/config.node.d.ts +2 -0
- package/dist/safe-fs/config.node.js +22 -0
- package/dist/safe-fs/contracts/abort.d.ts +5 -0
- package/dist/safe-fs/contracts/abort.js +34 -0
- package/dist/safe-fs/contracts/cleanup.d.ts +1 -0
- package/dist/safe-fs/contracts/cleanup.js +9 -0
- package/dist/safe-fs/contracts/errors.d.ts +50 -0
- package/dist/safe-fs/contracts/errors.js +77 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +96 -0
- package/dist/safe-fs/contracts/filesystem.js +1 -0
- package/dist/safe-fs/contracts/index.d.ts +4 -0
- package/dist/safe-fs/contracts/index.js +4 -0
- package/dist/safe-fs/contracts/io.d.ts +8 -0
- package/dist/safe-fs/contracts/io.js +92 -0
- package/dist/safe-fs/contracts/path.d.ts +8 -0
- package/dist/safe-fs/contracts/path.js +8 -0
- package/dist/safe-fs/contracts/portable-path.d.ts +13 -0
- package/dist/safe-fs/contracts/portable-path.js +57 -0
- package/dist/safe-fs/contracts/virtual-path.d.ts +7 -0
- package/dist/safe-fs/contracts/virtual-path.js +66 -0
- package/dist/safe-fs/core.d.ts +12 -0
- package/dist/safe-fs/core.js +12 -0
- package/dist/safe-fs/fs/memory/index.d.ts +58 -0
- package/dist/safe-fs/fs/memory/index.js +532 -0
- package/dist/safe-fs/fs/mount/comparison.d.ts +19 -0
- package/dist/safe-fs/fs/mount/comparison.js +118 -0
- package/dist/safe-fs/fs/mount/identity.d.ts +2 -0
- package/dist/safe-fs/fs/mount/identity.js +12 -0
- package/dist/safe-fs/fs/mount/index.d.ts +44 -0
- package/dist/safe-fs/fs/mount/index.js +546 -0
- package/dist/safe-fs/fs/overlay/index.d.ts +73 -0
- package/dist/safe-fs/fs/overlay/index.js +927 -0
- package/dist/safe-fs/fs/readonly/index.d.ts +31 -0
- package/dist/safe-fs/fs/readonly/index.js +128 -0
- package/dist/safe-fs/fs/real/allocation.d.ts +1 -0
- package/dist/safe-fs/fs/real/allocation.js +8 -0
- package/dist/safe-fs/fs/real/index.d.ts +76 -0
- package/dist/safe-fs/fs/real/index.js +543 -0
- package/dist/safe-fs/fs/s3/authority.d.ts +7 -0
- package/dist/safe-fs/fs/s3/authority.js +31 -0
- package/dist/safe-fs/fs/s3/filesystem.d.ts +93 -0
- package/dist/safe-fs/fs/s3/filesystem.js +1024 -0
- package/dist/safe-fs/fs/s3/http/index.d.ts +2 -0
- package/dist/safe-fs/fs/s3/http/index.js +1 -0
- package/dist/safe-fs/fs/s3/http/request.d.ts +16 -0
- package/dist/safe-fs/fs/s3/http/request.js +196 -0
- package/dist/safe-fs/fs/s3/http/signature.d.ts +20 -0
- package/dist/safe-fs/fs/s3/http/signature.js +74 -0
- package/dist/safe-fs/fs/s3/http/transport.d.ts +3 -0
- package/dist/safe-fs/fs/s3/http/transport.js +455 -0
- package/dist/safe-fs/fs/s3/http/types.d.ts +30 -0
- package/dist/safe-fs/fs/s3/http/types.js +1 -0
- package/dist/safe-fs/fs/s3/http/xml.d.ts +11 -0
- package/dist/safe-fs/fs/s3/http/xml.js +188 -0
- package/dist/safe-fs/fs/s3/index.d.ts +6 -0
- package/dist/safe-fs/fs/s3/index.js +3 -0
- package/dist/safe-fs/fs/s3/mock.d.ts +48 -0
- package/dist/safe-fs/fs/s3/mock.js +233 -0
- package/dist/safe-fs/fs/s3/registry.d.ts +10 -0
- package/dist/safe-fs/fs/s3/registry.js +71 -0
- package/dist/safe-fs/fs/s3/transport.d.ts +103 -0
- package/dist/safe-fs/fs/s3/transport.js +28 -0
- package/dist/safe-fs/fs/webdav/index.d.ts +2 -0
- package/dist/safe-fs/fs/webdav/index.js +1 -0
- package/dist/safe-fs/fs/webdav/resource-id.d.ts +18 -0
- package/dist/safe-fs/fs/webdav/resource-id.js +109 -0
- package/dist/safe-fs/fs/webdav/webdav.d.ts +101 -0
- package/dist/safe-fs/fs/webdav/webdav.js +1226 -0
- package/dist/safe-fs/fs/webdav/xml.d.ts +15 -0
- package/dist/safe-fs/fs/webdav/xml.js +250 -0
- package/dist/safe-fs/index.d.ts +14 -0
- package/dist/safe-fs/index.js +12 -0
- package/dist/safe-fs/node/filesystem.d.ts +11 -0
- package/dist/safe-fs/node/filesystem.js +28 -0
- package/dist/safe-fs/node/index.d.ts +2 -0
- package/dist/safe-fs/node/index.js +1 -0
- package/dist/safe-fs/node/stats.d.ts +1 -0
- package/dist/safe-fs/node/stats.js +1 -0
- package/dist/safe-fs/node/values.d.ts +1 -0
- package/dist/safe-fs/node/values.js +1 -0
- package/dist/safe-fs/node-host.d.ts +1 -0
- package/dist/safe-fs/node-host.js +1 -0
- package/dist/safe-fs/node-unavailable.d.ts +1 -0
- package/dist/safe-fs/node-unavailable.js +1 -0
- package/dist/safe-fs/platform/browser.d.ts +12 -0
- package/dist/safe-fs/platform/browser.js +39 -0
- package/dist/safe-fs/platform/node.d.ts +13 -0
- package/dist/safe-fs/platform/node.js +26 -0
- package/package.json +108 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
import { platform } from "#safe-fs-platform";
|
|
2
|
+
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
3
|
+
import { collectBytes, readBytes } from "../../contracts/io.js";
|
|
4
|
+
import { FsError, toFsError } from "../../contracts/errors.js";
|
|
5
|
+
import { dirname, isPathWithin, normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
6
|
+
import { compareIdentity } from "../mount/identity.js";
|
|
7
|
+
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
8
|
+
function snapshotStat(stat) {
|
|
9
|
+
const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
10
|
+
return {
|
|
11
|
+
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
12
|
+
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
13
|
+
...(birthtimeMs === undefined ? {} : { birthtimeMs }),
|
|
14
|
+
...(identityScope === undefined ? {} : { identityScope }),
|
|
15
|
+
...(ino === undefined ? {} : { ino }),
|
|
16
|
+
...(dev === undefined ? {} : { dev }),
|
|
17
|
+
...(nlink === undefined ? {} : { nlink }),
|
|
18
|
+
...(uid === undefined ? {} : { uid }),
|
|
19
|
+
...(gid === undefined ? {} : { gid }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function hasCode(error, code) {
|
|
23
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
24
|
+
}
|
|
25
|
+
function fail(code, path, message) {
|
|
26
|
+
throw new FsError(code, { syscall: "overlay", path, ...(message === undefined ? {} : { message }) });
|
|
27
|
+
}
|
|
28
|
+
function integer(value, path) {
|
|
29
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
30
|
+
fail("EINVAL", path);
|
|
31
|
+
}
|
|
32
|
+
function mode(value, path) {
|
|
33
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0 || value > 0o7777))
|
|
34
|
+
fail("EINVAL", path);
|
|
35
|
+
}
|
|
36
|
+
function movedPath(path, moves) {
|
|
37
|
+
for (const move of moves) {
|
|
38
|
+
if (isPathWithin(move.source, path))
|
|
39
|
+
path = move.destination + path.slice(move.source.length);
|
|
40
|
+
}
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
async function waitForTurn(previous, signal) {
|
|
44
|
+
signal?.throwIfAborted();
|
|
45
|
+
if (!signal)
|
|
46
|
+
return previous;
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
49
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
50
|
+
void previous.then(() => { signal.removeEventListener("abort", abort); resolve(); });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export class OverlayFileSystem {
|
|
54
|
+
capabilities;
|
|
55
|
+
#upper;
|
|
56
|
+
#lower;
|
|
57
|
+
maxBufferBytes;
|
|
58
|
+
whiteouts = new Set();
|
|
59
|
+
opaque = new Set();
|
|
60
|
+
garbage = new Set();
|
|
61
|
+
activeStages = new Set();
|
|
62
|
+
linkMetadata = new Map();
|
|
63
|
+
linkOrigins = new Map();
|
|
64
|
+
queue = Promise.resolve();
|
|
65
|
+
constructor(options) {
|
|
66
|
+
if (options.upper === options.lower)
|
|
67
|
+
throw new TypeError("Overlay upper and lower must be distinct backends");
|
|
68
|
+
this.#upper = options.upper;
|
|
69
|
+
this.#lower = options.lower;
|
|
70
|
+
registerEntryView(this, (path, options) => this.run(options, async () => {
|
|
71
|
+
const entry = await this.required(path, options);
|
|
72
|
+
return { filesystem: entry.backend, path: entry.path, readOnly: this.capabilities.readOnly === true };
|
|
73
|
+
}, false));
|
|
74
|
+
this.maxBufferBytes = options.maxBufferBytes ?? 64 * 1024 * 1024;
|
|
75
|
+
integer(this.maxBufferBytes, "/");
|
|
76
|
+
const writable = !this.#upper.capabilities.readOnly && this.#upper.capabilities.atomicRename === true;
|
|
77
|
+
const readable = [this.#upper, this.#lower].map((backend) => typeof backend.readStream === "function" ? backend.capabilities.streamingRead : false);
|
|
78
|
+
const streamingRead = readable.every((capability) => capability === true) ? true
|
|
79
|
+
: readable.every((capability) => capability === false) ? false : undefined;
|
|
80
|
+
const streamingWrite = writable && this.#upper.capabilities.streamingWrite === true
|
|
81
|
+
&& this.#upper.capabilities.streamingRead === true
|
|
82
|
+
&& typeof this.#upper.writeStream === "function" && typeof this.#upper.readStream === "function"
|
|
83
|
+
? readable.every((capability) => capability === true) ? true : undefined : false;
|
|
84
|
+
this.capabilities = Object.freeze({
|
|
85
|
+
readOnly: !writable,
|
|
86
|
+
atomicRename: false,
|
|
87
|
+
hardlinks: false,
|
|
88
|
+
symlinks: writable && this.#upper.capabilities.symlinks === true
|
|
89
|
+
&& typeof this.#upper.symlink === "function" && typeof this.#upper.readlink === "function"
|
|
90
|
+
&& (this.#lower.capabilities.symlinks !== true || typeof this.#lower.readlink === "function"),
|
|
91
|
+
permissions: writable && this.#upper.capabilities.permissions === true && typeof this.#upper.chmod === "function",
|
|
92
|
+
timestamps: writable && this.#upper.capabilities.timestamps === true && typeof this.#upper.utimes === "function",
|
|
93
|
+
...(streamingRead === undefined ? {} : { streamingRead }),
|
|
94
|
+
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
95
|
+
});
|
|
96
|
+
Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
|
|
97
|
+
}
|
|
98
|
+
async run(options, operation, cleanup = true) {
|
|
99
|
+
options.signal?.throwIfAborted();
|
|
100
|
+
const previous = this.queue;
|
|
101
|
+
let release;
|
|
102
|
+
this.queue = new Promise((resolve) => { release = resolve; });
|
|
103
|
+
try {
|
|
104
|
+
await waitForTurn(previous, options.signal);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
void previous.then(release);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
options.signal?.throwIfAborted();
|
|
112
|
+
if (cleanup)
|
|
113
|
+
await this.cleanGarbage(false);
|
|
114
|
+
return await operation();
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
release();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
writable(path) {
|
|
121
|
+
if (this.#upper.capabilities.readOnly)
|
|
122
|
+
fail("EROFS", path);
|
|
123
|
+
if (this.#upper.capabilities.atomicRename !== true)
|
|
124
|
+
fail("ENOTSUP", path, "staged mutations require an atomic-rename-capable upper");
|
|
125
|
+
}
|
|
126
|
+
permission(entry, mask) {
|
|
127
|
+
if (entry.backend.capabilities.permissions && ((entry.stat.mode >> 6) & mask) !== mask)
|
|
128
|
+
fail("EACCES", entry.path);
|
|
129
|
+
}
|
|
130
|
+
async maybeStat(backend, path, options) {
|
|
131
|
+
options.signal?.throwIfAborted();
|
|
132
|
+
let stat;
|
|
133
|
+
try {
|
|
134
|
+
stat = await backend.lstat(path, options);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
options.signal?.throwIfAborted();
|
|
138
|
+
if (hasCode(error, "ENOENT"))
|
|
139
|
+
return undefined;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
return snapshotStat(stat);
|
|
143
|
+
}
|
|
144
|
+
hidden(path) {
|
|
145
|
+
return [...this.garbage, ...this.activeStages].some((root) => isPathWithin(root, path));
|
|
146
|
+
}
|
|
147
|
+
async lowerVisible(path, options) {
|
|
148
|
+
for (const root of this.whiteouts)
|
|
149
|
+
if (isPathWithin(root, path))
|
|
150
|
+
return false;
|
|
151
|
+
for (const root of this.opaque)
|
|
152
|
+
if (root !== path && isPathWithin(root, path))
|
|
153
|
+
return false;
|
|
154
|
+
const ancestors = path.split("/").filter(Boolean).slice(0, -1);
|
|
155
|
+
let parent = "";
|
|
156
|
+
for (const component of ancestors) {
|
|
157
|
+
parent += `/${component}`;
|
|
158
|
+
const stat = await this.maybeStat(this.#lower, parent, options);
|
|
159
|
+
if (!stat || stat.type !== "directory")
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
async lookup(path, options) {
|
|
165
|
+
if (this.hidden(path))
|
|
166
|
+
return undefined;
|
|
167
|
+
const upper = await this.maybeStat(this.#upper, path, options);
|
|
168
|
+
if (upper)
|
|
169
|
+
return { path, backend: this.#upper, stat: { ...upper, ...(upper.type === "symlink" ? this.linkMetadata.get(path) : {}) } };
|
|
170
|
+
if (!await this.lowerVisible(path, options))
|
|
171
|
+
return undefined;
|
|
172
|
+
const lower = await this.maybeStat(this.#lower, path, options);
|
|
173
|
+
return lower ? { path, backend: this.#lower, stat: lower } : undefined;
|
|
174
|
+
}
|
|
175
|
+
async resolve(path, options, followFinal = true, allowMissing = false, createMode) {
|
|
176
|
+
validatePath(path);
|
|
177
|
+
if (!path)
|
|
178
|
+
fail("ENOENT", path);
|
|
179
|
+
const pending = path.split("/").filter(Boolean);
|
|
180
|
+
if (path.endsWith("/"))
|
|
181
|
+
pending.push(".");
|
|
182
|
+
const verifications = [];
|
|
183
|
+
const verified = async (location) => {
|
|
184
|
+
for (const verification of verifications) {
|
|
185
|
+
const canonical = await verification.backend.realpath(verification.path, options);
|
|
186
|
+
options.signal?.throwIfAborted();
|
|
187
|
+
validatePath(canonical);
|
|
188
|
+
if (!canonical.startsWith("/"))
|
|
189
|
+
fail("EIO", verification.path, "backend returned a nonabsolute realpath");
|
|
190
|
+
if (verification.finalName !== undefined) {
|
|
191
|
+
try {
|
|
192
|
+
await verification.backend.lstat(`${verification.path}/${verification.finalName}`, options);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (location.entry || !hasCode(error, "ENOENT"))
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const expected = verification.finalName === undefined ? canonical
|
|
200
|
+
: `${canonical === "/" ? "" : canonical}/${verification.finalName}`;
|
|
201
|
+
if (location.path !== movedPath(expected, verification.moves ?? []))
|
|
202
|
+
fail("ENOTSUP", path, "backend cannot confirm the overlay symlink namespace");
|
|
203
|
+
}
|
|
204
|
+
options.signal?.throwIfAborted();
|
|
205
|
+
return location;
|
|
206
|
+
};
|
|
207
|
+
let current = "/";
|
|
208
|
+
let links = 0;
|
|
209
|
+
while (pending.length) {
|
|
210
|
+
options.signal?.throwIfAborted();
|
|
211
|
+
const parent = await this.lookup(current, options);
|
|
212
|
+
if (!parent)
|
|
213
|
+
fail("ENOENT", current);
|
|
214
|
+
if (parent.stat.type !== "directory")
|
|
215
|
+
fail("ENOTDIR", current);
|
|
216
|
+
this.permission(parent, 1);
|
|
217
|
+
const component = pending.shift();
|
|
218
|
+
if (component === ".")
|
|
219
|
+
continue;
|
|
220
|
+
if (component === "..") {
|
|
221
|
+
current = dirname(current);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (new TextEncoder().encode(component).byteLength > 255)
|
|
225
|
+
fail("ENAMETOOLONG", path);
|
|
226
|
+
const candidate = current === "/" ? `/${component}` : `${current}/${component}`;
|
|
227
|
+
if (this.hidden(candidate))
|
|
228
|
+
fail(allowMissing || createMode !== undefined ? "EBUSY" : "ENOENT", candidate);
|
|
229
|
+
let entry = await this.lookup(candidate, options);
|
|
230
|
+
if (!entry && createMode !== undefined) {
|
|
231
|
+
if (verifications.length)
|
|
232
|
+
fail("ENOTSUP", path, "recursive creation through symlinks cannot be verified before mutation");
|
|
233
|
+
await this.makeDirectory(candidate, createMode, options);
|
|
234
|
+
entry = await this.lookup(candidate, options);
|
|
235
|
+
}
|
|
236
|
+
if (!entry) {
|
|
237
|
+
if (allowMissing && pending.length === 0)
|
|
238
|
+
return verified({ path: candidate });
|
|
239
|
+
fail("ENOENT", candidate);
|
|
240
|
+
}
|
|
241
|
+
if (entry.stat.type === "symlink" && (followFinal || pending.length > 0)) {
|
|
242
|
+
if (++links > 40)
|
|
243
|
+
fail("ELOOP", path);
|
|
244
|
+
const suffix = [...pending];
|
|
245
|
+
const last = suffix.at(-1);
|
|
246
|
+
const finalName = !followFinal && last !== undefined && last !== "." && last !== ".." ? suffix.pop() : undefined;
|
|
247
|
+
verifications.push({
|
|
248
|
+
backend: entry.backend,
|
|
249
|
+
path: candidate + (suffix.length ? `/${suffix.join("/")}` : ""),
|
|
250
|
+
finalName,
|
|
251
|
+
});
|
|
252
|
+
const origin = entry.backend === this.#upper ? this.linkOrigins.get(candidate) : undefined;
|
|
253
|
+
if (origin)
|
|
254
|
+
verifications.push({
|
|
255
|
+
backend: this.#lower,
|
|
256
|
+
path: origin.path + (suffix.length ? `/${suffix.join("/")}` : ""),
|
|
257
|
+
finalName,
|
|
258
|
+
moves: origin.moves,
|
|
259
|
+
});
|
|
260
|
+
const target = await this.linkTarget(entry, options);
|
|
261
|
+
const parts = target.split("/").filter(Boolean);
|
|
262
|
+
if (target.endsWith("/"))
|
|
263
|
+
parts.push(".");
|
|
264
|
+
pending.unshift(...parts);
|
|
265
|
+
if (target.startsWith("/"))
|
|
266
|
+
current = "/";
|
|
267
|
+
}
|
|
268
|
+
else
|
|
269
|
+
current = candidate;
|
|
270
|
+
}
|
|
271
|
+
const entry = await this.lookup(current, options);
|
|
272
|
+
if (!entry)
|
|
273
|
+
fail("ENOENT", current);
|
|
274
|
+
return verified({ path: current, entry });
|
|
275
|
+
}
|
|
276
|
+
async linkTarget(entry, options) {
|
|
277
|
+
if (!entry.backend.readlink)
|
|
278
|
+
fail("ENOTSUP", entry.path);
|
|
279
|
+
const target = await entry.backend.readlink(entry.path, options);
|
|
280
|
+
options.signal?.throwIfAborted();
|
|
281
|
+
validatePath(target);
|
|
282
|
+
if (!target)
|
|
283
|
+
fail("ENOENT", entry.path);
|
|
284
|
+
return target;
|
|
285
|
+
}
|
|
286
|
+
async movableLinkTarget(entry, options) {
|
|
287
|
+
const target = await this.linkTarget(entry, options);
|
|
288
|
+
const canonical = await entry.backend.realpath(entry.path, options);
|
|
289
|
+
options.signal?.throwIfAborted();
|
|
290
|
+
const parts = target.split("/").filter(Boolean);
|
|
291
|
+
if (target.endsWith("/"))
|
|
292
|
+
parts.push(".");
|
|
293
|
+
let current = target.startsWith("/") ? "/" : dirname(entry.path);
|
|
294
|
+
let stat = await this.maybeStat(entry.backend, current, options);
|
|
295
|
+
for (const part of parts) {
|
|
296
|
+
if (!stat)
|
|
297
|
+
fail("ENOENT", current);
|
|
298
|
+
if (stat.type !== "directory")
|
|
299
|
+
fail("ENOTSUP", entry.path, "symlink copy-up requires an alias-free target path");
|
|
300
|
+
if (part === ".")
|
|
301
|
+
continue;
|
|
302
|
+
current = part === ".." ? dirname(current) : `${current === "/" ? "" : current}/${part}`;
|
|
303
|
+
stat = await this.maybeStat(entry.backend, current, options);
|
|
304
|
+
if (stat?.type === "symlink")
|
|
305
|
+
fail("ENOTSUP", entry.path, "symlink copy-up cannot preserve an ambiguous target alias");
|
|
306
|
+
}
|
|
307
|
+
if (!stat)
|
|
308
|
+
fail("ENOENT", current);
|
|
309
|
+
if (canonical !== current)
|
|
310
|
+
fail("ENOTSUP", entry.path, "backend cannot prove symlink semantics survive copy-up");
|
|
311
|
+
return target;
|
|
312
|
+
}
|
|
313
|
+
async required(path, options, followFinal = true) {
|
|
314
|
+
return (await this.resolve(path, options, followFinal)).entry;
|
|
315
|
+
}
|
|
316
|
+
async listing(entry, options) {
|
|
317
|
+
if (entry.stat.type !== "directory")
|
|
318
|
+
fail("ENOTDIR", entry.path);
|
|
319
|
+
this.permission(entry, 4);
|
|
320
|
+
const names = new Set();
|
|
321
|
+
const upper = await this.maybeStat(this.#upper, entry.path, options);
|
|
322
|
+
if (upper?.type === "directory") {
|
|
323
|
+
for (const child of await this.#upper.readdir(entry.path, options))
|
|
324
|
+
names.add(child.name);
|
|
325
|
+
}
|
|
326
|
+
if (await this.lowerVisible(entry.path, options) && !this.opaque.has(entry.path)) {
|
|
327
|
+
const lower = await this.maybeStat(this.#lower, entry.path, options);
|
|
328
|
+
if (lower?.type === "directory") {
|
|
329
|
+
for (const child of await this.#lower.readdir(entry.path, options))
|
|
330
|
+
names.add(child.name);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const entries = [];
|
|
334
|
+
for (const name of [...names].sort()) {
|
|
335
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
|
|
336
|
+
fail("EIO", entry.path, "invalid backend directory entry");
|
|
337
|
+
const child = await this.lookup(entry.path === "/" ? `/${name}` : `${entry.path}/${name}`, options);
|
|
338
|
+
if (child)
|
|
339
|
+
entries.push({ name, type: child.stat.type });
|
|
340
|
+
}
|
|
341
|
+
return entries;
|
|
342
|
+
}
|
|
343
|
+
async bytes(entry, options) {
|
|
344
|
+
if (entry.stat.type !== "file")
|
|
345
|
+
fail("EISDIR", entry.path);
|
|
346
|
+
this.permission(entry, 4);
|
|
347
|
+
if (options.maxBytes !== undefined)
|
|
348
|
+
integer(options.maxBytes, entry.path);
|
|
349
|
+
const limit = Math.min(options.maxBytes ?? this.maxBufferBytes, this.maxBufferBytes);
|
|
350
|
+
if (entry.stat.size > limit)
|
|
351
|
+
fail("EFBIG", entry.path);
|
|
352
|
+
const result = await entry.backend.readFile(entry.path, { ...options, maxBytes: limit });
|
|
353
|
+
options.signal?.throwIfAborted();
|
|
354
|
+
if (!(result instanceof Uint8Array))
|
|
355
|
+
fail("EIO", entry.path, "backend returned non-byte data");
|
|
356
|
+
if (result.byteLength > limit)
|
|
357
|
+
fail("EFBIG", entry.path);
|
|
358
|
+
return new Uint8Array(result);
|
|
359
|
+
}
|
|
360
|
+
async cleanGarbage(strict) {
|
|
361
|
+
const failures = [];
|
|
362
|
+
for (const path of this.garbage) {
|
|
363
|
+
try {
|
|
364
|
+
await this.#upper.rm(path, { recursive: true, force: true });
|
|
365
|
+
this.garbage.delete(path);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
failures.push(error);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (strict && failures.length)
|
|
372
|
+
throw new AggregateError(failures, "Overlay staging cleanup failed");
|
|
373
|
+
}
|
|
374
|
+
async cleanup(options = {}) {
|
|
375
|
+
return this.run(options, () => this.cleanGarbage(true));
|
|
376
|
+
}
|
|
377
|
+
async staged(options, operation) {
|
|
378
|
+
const root = `/.virtual-bash-overlay-${platform.randomUUID()}`;
|
|
379
|
+
if (await this.maybeStat(this.#upper, root, options) || await this.maybeStat(this.#lower, root, options))
|
|
380
|
+
fail("EEXIST", root);
|
|
381
|
+
this.activeStages.add(root);
|
|
382
|
+
try {
|
|
383
|
+
await this.#upper.mkdir(root, { ...options, mode: 0o700 });
|
|
384
|
+
return await operation(`${root}/entry`);
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
this.activeStages.delete(root);
|
|
388
|
+
this.garbage.add(root);
|
|
389
|
+
await this.cleanGarbage(false);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async preserve(path, stat, options) {
|
|
393
|
+
if (this.#upper.chmod)
|
|
394
|
+
await this.#upper.chmod(path, stat.mode & 0o7777, options);
|
|
395
|
+
else if (((await this.#upper.lstat(path, options)).mode & 0o7777) !== (stat.mode & 0o7777))
|
|
396
|
+
fail("ENOTSUP", path, "upper cannot preserve modes");
|
|
397
|
+
if (!this.#upper.utimes)
|
|
398
|
+
fail("ENOTSUP", path, "upper cannot preserve timestamps during copy-up");
|
|
399
|
+
await this.#upper.utimes(path, stat.atimeMs, stat.mtimeMs, options);
|
|
400
|
+
}
|
|
401
|
+
async clone(entry, destination, options, streaming = false) {
|
|
402
|
+
if (entry.stat.type !== "directory" && ((entry.stat.nlink ?? 1) > 1
|
|
403
|
+
|| (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined))) {
|
|
404
|
+
fail("ENOTSUP", entry.path, "copy-up cannot preserve hardlink identity");
|
|
405
|
+
}
|
|
406
|
+
if (entry.stat.type === "symlink") {
|
|
407
|
+
if (!this.capabilities.symlinks || !entry.backend.readlink || !this.#upper.symlink)
|
|
408
|
+
fail("ENOTSUP", entry.path);
|
|
409
|
+
await this.#upper.symlink(await this.movableLinkTarget(entry, options), destination, options);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (entry.stat.type === "directory")
|
|
413
|
+
await this.#upper.mkdir(destination, { ...options, mode: entry.stat.mode & 0o7777 });
|
|
414
|
+
else if (streaming && entry.backend.readStream && entry.backend.capabilities.streamingRead !== false && this.#upper.writeStream) {
|
|
415
|
+
this.permission(entry, 4);
|
|
416
|
+
if (entry.stat.size > this.maxBufferBytes)
|
|
417
|
+
fail("EFBIG", entry.path);
|
|
418
|
+
const source = this.bounded(entry.backend.readStream(entry.path, options), entry.path, options);
|
|
419
|
+
await this.streamToUpper(destination, source, { ...options, mode: 0o600, flag: "wx" });
|
|
420
|
+
}
|
|
421
|
+
else
|
|
422
|
+
await this.#upper.writeFile(destination, await this.bytes(entry, options), { ...options, mode: 0o600, flag: "wx" });
|
|
423
|
+
await this.preserve(destination, entry.stat, options);
|
|
424
|
+
}
|
|
425
|
+
rememberLink(entry, destination) {
|
|
426
|
+
this.linkMetadata.delete(destination);
|
|
427
|
+
if (entry.stat.type === "symlink") {
|
|
428
|
+
this.linkMetadata.set(destination, { mode: entry.stat.mode, atimeMs: entry.stat.atimeMs, mtimeMs: entry.stat.mtimeMs });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async ensureDirectory(path, options) {
|
|
432
|
+
const entry = await this.lookup(path, options);
|
|
433
|
+
if (!entry)
|
|
434
|
+
fail("ENOENT", path);
|
|
435
|
+
if (entry.stat.type !== "directory")
|
|
436
|
+
fail("ENOTDIR", path);
|
|
437
|
+
if (entry.backend === this.#upper)
|
|
438
|
+
return;
|
|
439
|
+
await this.copyUp(entry, options);
|
|
440
|
+
}
|
|
441
|
+
async copyUp(entry, options) {
|
|
442
|
+
if (entry.backend === this.#upper)
|
|
443
|
+
return;
|
|
444
|
+
if (entry.path === "/")
|
|
445
|
+
fail("ENOTSUP", entry.path, "upper must have a directory root");
|
|
446
|
+
let origin;
|
|
447
|
+
if (entry.stat.type === "symlink") {
|
|
448
|
+
const target = await this.movableLinkTarget(entry, options);
|
|
449
|
+
origin = { path: entry.path, target, canonical: normalizePath(target, dirname(entry.path)), moves: [] };
|
|
450
|
+
}
|
|
451
|
+
await this.ensureDirectory(dirname(entry.path), options);
|
|
452
|
+
await this.staged(options, async (temporary) => {
|
|
453
|
+
await this.clone(entry, temporary, options);
|
|
454
|
+
options.signal?.throwIfAborted();
|
|
455
|
+
await this.#upper.rename(temporary, entry.path, options);
|
|
456
|
+
this.rememberLink(entry, entry.path);
|
|
457
|
+
if (origin)
|
|
458
|
+
this.linkOrigins.set(entry.path, origin);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async parent(path, options) {
|
|
462
|
+
const parent = await this.lookup(dirname(path), options);
|
|
463
|
+
if (!parent)
|
|
464
|
+
fail("ENOENT", dirname(path));
|
|
465
|
+
if (parent.stat.type !== "directory")
|
|
466
|
+
fail("ENOTDIR", dirname(path));
|
|
467
|
+
this.permission(parent, 3);
|
|
468
|
+
await this.ensureDirectory(parent.path, options);
|
|
469
|
+
}
|
|
470
|
+
async makeDirectory(path, permissions, options) {
|
|
471
|
+
await this.parent(path, options);
|
|
472
|
+
await this.staged(options, async (temporary) => {
|
|
473
|
+
await this.#upper.mkdir(temporary, { ...options, mode: permissions });
|
|
474
|
+
options.signal?.throwIfAborted();
|
|
475
|
+
await this.#upper.rename(temporary, path, options);
|
|
476
|
+
this.opaque.add(path);
|
|
477
|
+
this.linkMetadata.delete(path);
|
|
478
|
+
this.linkOrigins.delete(path);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
writeOptions(path, options) {
|
|
482
|
+
this.writable(path);
|
|
483
|
+
mode(options.mode, path);
|
|
484
|
+
if (!["w", "wx", "a", "ax"].includes(options.flag ?? "w"))
|
|
485
|
+
fail("EINVAL", path);
|
|
486
|
+
}
|
|
487
|
+
async writeLocation(path, options) {
|
|
488
|
+
const exclusive = options.flag === "wx" || options.flag === "ax";
|
|
489
|
+
const location = await this.resolve(path, options, !exclusive, true);
|
|
490
|
+
if (exclusive && location.entry)
|
|
491
|
+
fail("EEXIST", path);
|
|
492
|
+
if (location.entry) {
|
|
493
|
+
if (location.entry.stat.type !== "file")
|
|
494
|
+
fail("EISDIR", path);
|
|
495
|
+
this.permission(location.entry, 2);
|
|
496
|
+
}
|
|
497
|
+
return location;
|
|
498
|
+
}
|
|
499
|
+
async replace(location, options, operation, streaming = false) {
|
|
500
|
+
await this.parent(location.path, options);
|
|
501
|
+
await this.staged(options, async (temporary) => {
|
|
502
|
+
if (location.entry)
|
|
503
|
+
await this.clone(location.entry, temporary, options, streaming);
|
|
504
|
+
await operation(temporary);
|
|
505
|
+
options.signal?.throwIfAborted();
|
|
506
|
+
await this.#upper.rename(temporary, location.path, options);
|
|
507
|
+
this.linkMetadata.delete(location.path);
|
|
508
|
+
this.linkOrigins.delete(location.path);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async readFile(path, options = {}) {
|
|
512
|
+
return this.run(options, async () => this.bytes(await this.required(path, options), options));
|
|
513
|
+
}
|
|
514
|
+
async writeFile(path, data, options = {}) {
|
|
515
|
+
options.signal?.throwIfAborted();
|
|
516
|
+
this.writeOptions(path, options);
|
|
517
|
+
if (!(data instanceof Uint8Array))
|
|
518
|
+
throw new TypeError("Overlay files require Uint8Array data");
|
|
519
|
+
if (data.byteLength > this.maxBufferBytes)
|
|
520
|
+
fail("EFBIG", path);
|
|
521
|
+
const bytes = new Uint8Array(data);
|
|
522
|
+
return this.run(options, async () => {
|
|
523
|
+
const location = await this.writeLocation(path, options);
|
|
524
|
+
const append = options.flag === "a" || options.flag === "ax";
|
|
525
|
+
if (append && (location.entry?.stat.size ?? 0) + bytes.byteLength > this.maxBufferBytes)
|
|
526
|
+
fail("EFBIG", path);
|
|
527
|
+
await this.replace(location, options, async (temporary) => {
|
|
528
|
+
await this.#upper.writeFile(temporary, bytes, { ...options, flag: append ? "a" : "w" });
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
async appendFile(path, data, options = {}) {
|
|
533
|
+
return this.writeFile(path, data, { ...options, flag: "a" });
|
|
534
|
+
}
|
|
535
|
+
async stat(path, options = {}) {
|
|
536
|
+
return this.run(options, async () => snapshotStat((await this.required(path, options)).stat), false);
|
|
537
|
+
}
|
|
538
|
+
async lstat(path, options = {}) {
|
|
539
|
+
return this.run(options, async () => snapshotStat((await this.required(path, options, false)).stat), false);
|
|
540
|
+
}
|
|
541
|
+
compareEntry(path, peer, peerPath, options = {}) {
|
|
542
|
+
return compareEntries(this, path, peer, peerPath, options);
|
|
543
|
+
}
|
|
544
|
+
async readdir(path, options = {}) {
|
|
545
|
+
return this.run(options, async () => this.listing(await this.required(path, options), options), false);
|
|
546
|
+
}
|
|
547
|
+
async realpath(path, options = {}) {
|
|
548
|
+
return this.run(options, async () => (await this.required(path, options)).path, false);
|
|
549
|
+
}
|
|
550
|
+
async access(path, requestedMode = 0, options = {}) {
|
|
551
|
+
return this.run(options, async () => {
|
|
552
|
+
if (!Number.isInteger(requestedMode) || requestedMode < 0 || requestedMode > 7)
|
|
553
|
+
fail("EINVAL", path);
|
|
554
|
+
const entry = await this.required(path, options);
|
|
555
|
+
if (requestedMode & 2)
|
|
556
|
+
this.writable(path);
|
|
557
|
+
this.permission(entry, requestedMode);
|
|
558
|
+
await entry.backend.access(entry.path, requestedMode & ~2, options);
|
|
559
|
+
}, false);
|
|
560
|
+
}
|
|
561
|
+
async mkdir(path, options = {}) {
|
|
562
|
+
return this.run(options, async () => {
|
|
563
|
+
this.writable(path);
|
|
564
|
+
mode(options.mode, path);
|
|
565
|
+
validatePath(path);
|
|
566
|
+
if (options.recursive) {
|
|
567
|
+
const entry = (await this.resolve(path, options, true, false, options.mode ?? 0o777)).entry;
|
|
568
|
+
if (entry.stat.type !== "directory")
|
|
569
|
+
fail("EEXIST", path);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const location = await this.resolve(path.replace(/\/+$/, "") || (path ? "/" : ""), options, false, true);
|
|
573
|
+
if (location.entry)
|
|
574
|
+
fail("EEXIST", path);
|
|
575
|
+
await this.makeDirectory(location.path, options.mode ?? 0o777, options);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async rmdir(path, options = {}) {
|
|
580
|
+
try {
|
|
581
|
+
await this.run(options, async () => {
|
|
582
|
+
this.writable(path);
|
|
583
|
+
validatePath(path);
|
|
584
|
+
const entry = await this.required(path.replace(/\/+$/, "") || (path ? "/" : ""), options, false);
|
|
585
|
+
if (/(?:^|\/)\.{1,2}\/*$/.test(path))
|
|
586
|
+
fail("EINVAL", path);
|
|
587
|
+
if (entry.path === "/")
|
|
588
|
+
fail("EBUSY", path);
|
|
589
|
+
if (entry.stat.type !== "directory")
|
|
590
|
+
fail("ENOTDIR", path);
|
|
591
|
+
const parent = await this.required(dirname(entry.path), options);
|
|
592
|
+
this.permission(parent, 3);
|
|
593
|
+
if ((await this.listing(entry, options)).length)
|
|
594
|
+
fail("ENOTEMPTY", path);
|
|
595
|
+
options.signal?.throwIfAborted();
|
|
596
|
+
if (this.#upper.capabilities.snapshotRmdir === true) {
|
|
597
|
+
fail("ENOTSUP", path, "snapshot-marker upper removal cannot safely publish an overlay whiteout");
|
|
598
|
+
}
|
|
599
|
+
if (entry.backend === this.#upper) {
|
|
600
|
+
if (!this.#upper.rmdir)
|
|
601
|
+
fail("ENOTSUP", path);
|
|
602
|
+
await this.#upper.rmdir(entry.path, options);
|
|
603
|
+
}
|
|
604
|
+
this.whiteouts.add(entry.path);
|
|
605
|
+
for (const key of this.linkMetadata.keys())
|
|
606
|
+
if (isPathWithin(entry.path, key))
|
|
607
|
+
this.linkMetadata.delete(key);
|
|
608
|
+
for (const key of this.linkOrigins.keys())
|
|
609
|
+
if (isPathWithin(entry.path, key))
|
|
610
|
+
this.linkOrigins.delete(key);
|
|
611
|
+
}, false);
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
options.signal?.throwIfAborted();
|
|
615
|
+
throw new FsError(toFsError(error).code, { syscall: "rmdir", path, cause: error });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async rm(path, options = {}) {
|
|
619
|
+
return this.run(options, async () => {
|
|
620
|
+
this.writable(path);
|
|
621
|
+
let entry;
|
|
622
|
+
try {
|
|
623
|
+
entry = await this.required(path, options, false);
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (options.force && hasCode(error, "ENOENT"))
|
|
627
|
+
return;
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
if (entry.path === "/")
|
|
631
|
+
fail("EBUSY", path);
|
|
632
|
+
if (entry.stat.type === "directory" && !options.recursive && (await this.listing(entry, options)).length)
|
|
633
|
+
fail("ENOTEMPTY", path);
|
|
634
|
+
await this.parent(entry.path, options);
|
|
635
|
+
if (entry.backend === this.#upper) {
|
|
636
|
+
await this.staged(options, async (temporary) => {
|
|
637
|
+
options.signal?.throwIfAborted();
|
|
638
|
+
await this.#upper.rename(entry.path, temporary, options);
|
|
639
|
+
this.whiteouts.add(entry.path);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
options.signal?.throwIfAborted();
|
|
644
|
+
this.whiteouts.add(entry.path);
|
|
645
|
+
}
|
|
646
|
+
for (const key of this.linkMetadata.keys())
|
|
647
|
+
if (isPathWithin(entry.path, key))
|
|
648
|
+
this.linkMetadata.delete(key);
|
|
649
|
+
for (const key of this.linkOrigins.keys())
|
|
650
|
+
if (isPathWithin(entry.path, key))
|
|
651
|
+
this.linkOrigins.delete(key);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async materialize(entry, options) {
|
|
655
|
+
if (entry.stat.type === "symlink" && entry.backend === this.#upper)
|
|
656
|
+
await this.movableLinkTarget(entry, options);
|
|
657
|
+
const children = entry.stat.type === "directory" ? await this.listing(entry, options) : [];
|
|
658
|
+
await this.copyUp(entry, options);
|
|
659
|
+
for (const child of children) {
|
|
660
|
+
const path = `${entry.path}/${child.name}`;
|
|
661
|
+
const visible = await this.lookup(path, options);
|
|
662
|
+
if (!visible)
|
|
663
|
+
fail("EIO", path, "visible tree changed during materialization");
|
|
664
|
+
await this.materialize(visible, options);
|
|
665
|
+
}
|
|
666
|
+
if (entry.stat.type === "directory")
|
|
667
|
+
await this.preserve(entry.path, entry.stat, options);
|
|
668
|
+
}
|
|
669
|
+
async rename(source, destination, options = {}) {
|
|
670
|
+
return this.run(options, async () => {
|
|
671
|
+
this.writable(source);
|
|
672
|
+
const original = await this.required(source, options, false);
|
|
673
|
+
const target = await this.resolve(destination, options, false, true);
|
|
674
|
+
if (original.path === target.path)
|
|
675
|
+
return;
|
|
676
|
+
if (original.path === "/" || target.path === "/")
|
|
677
|
+
fail("EBUSY", source);
|
|
678
|
+
if (original.stat.type === "directory" && isPathWithin(original.path, target.path))
|
|
679
|
+
fail("EINVAL", destination);
|
|
680
|
+
if (target.entry) {
|
|
681
|
+
if (original.stat.type === "directory" && target.entry.stat.type !== "directory")
|
|
682
|
+
fail("ENOTDIR", destination);
|
|
683
|
+
if (original.stat.type !== "directory" && target.entry.stat.type === "directory")
|
|
684
|
+
fail("EISDIR", destination);
|
|
685
|
+
if (target.entry.stat.type === "directory" && (await this.listing(target.entry, options)).length)
|
|
686
|
+
fail("ENOTEMPTY", destination);
|
|
687
|
+
}
|
|
688
|
+
await this.parent(original.path, options);
|
|
689
|
+
await this.parent(target.path, options);
|
|
690
|
+
await this.materialize(original, options);
|
|
691
|
+
options.signal?.throwIfAborted();
|
|
692
|
+
const move = { source: original.path, destination: target.path };
|
|
693
|
+
const movedOrigins = [];
|
|
694
|
+
for (const [path, origin] of this.linkOrigins) {
|
|
695
|
+
if (!isPathWithin(original.path, path))
|
|
696
|
+
continue;
|
|
697
|
+
const destination = movedPath(path, [move]);
|
|
698
|
+
const moves = origin.target.startsWith("/") ? origin.moves : [...origin.moves, move];
|
|
699
|
+
if (normalizePath(origin.target, dirname(destination)) !== movedPath(origin.canonical, moves)) {
|
|
700
|
+
fail("ENOTSUP", path, "relocation changes an unprovable relative symlink target");
|
|
701
|
+
}
|
|
702
|
+
movedOrigins.push([destination, { ...origin, moves }]);
|
|
703
|
+
}
|
|
704
|
+
options.signal?.throwIfAborted();
|
|
705
|
+
await this.#upper.rename(original.path, target.path, options);
|
|
706
|
+
this.whiteouts.add(original.path);
|
|
707
|
+
if (original.stat.type === "directory")
|
|
708
|
+
this.opaque.add(target.path);
|
|
709
|
+
const moved = [...this.linkMetadata].filter(([path]) => isPathWithin(original.path, path));
|
|
710
|
+
for (const path of this.linkMetadata.keys()) {
|
|
711
|
+
if (isPathWithin(original.path, path) || isPathWithin(target.path, path))
|
|
712
|
+
this.linkMetadata.delete(path);
|
|
713
|
+
}
|
|
714
|
+
for (const [path, metadata] of moved)
|
|
715
|
+
this.linkMetadata.set(target.path + path.slice(original.path.length), metadata);
|
|
716
|
+
for (const path of this.linkOrigins.keys()) {
|
|
717
|
+
if (isPathWithin(original.path, path) || isPathWithin(target.path, path))
|
|
718
|
+
this.linkOrigins.delete(path);
|
|
719
|
+
}
|
|
720
|
+
for (const [path, origin] of movedOrigins)
|
|
721
|
+
this.linkOrigins.set(path, origin);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
async copyFile(source, destination, options = {}) {
|
|
725
|
+
return this.run(options, async () => {
|
|
726
|
+
this.writable(destination);
|
|
727
|
+
const original = await this.required(source, options);
|
|
728
|
+
const target = await this.writeLocation(destination, { ...options, flag: options.exclusive ? "wx" : "w" });
|
|
729
|
+
if (original.path === target.path)
|
|
730
|
+
fail("EINVAL", destination, "source and destination are the same file");
|
|
731
|
+
if (target.entry) {
|
|
732
|
+
let identity = compareIdentity(original.stat, target.entry.stat);
|
|
733
|
+
if (identity === "unknown")
|
|
734
|
+
identity = await compareEntries(original.backend, original.path, target.entry.backend, target.entry.path, options);
|
|
735
|
+
if (identity === "same")
|
|
736
|
+
fail("EINVAL", destination, "source and destination are the same file");
|
|
737
|
+
if (identity === "unknown")
|
|
738
|
+
fail("ENOTSUP", destination, "backing-entry identity is unknown");
|
|
739
|
+
}
|
|
740
|
+
if (target.entry?.backend !== this.#upper) {
|
|
741
|
+
const upper = await this.maybeStat(this.#upper, target.path, options);
|
|
742
|
+
if (upper) {
|
|
743
|
+
let identity = compareIdentity(original.stat, upper);
|
|
744
|
+
if (identity === "unknown")
|
|
745
|
+
identity = await compareEntries(original.backend, original.path, this.#upper, target.path, options);
|
|
746
|
+
if (identity === "same")
|
|
747
|
+
fail("EINVAL", destination, "source and destination are the same file");
|
|
748
|
+
if (identity === "unknown")
|
|
749
|
+
fail("ENOTSUP", destination, "upper backing-entry identity is unknown");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
await this.cleanGarbage(false);
|
|
753
|
+
const bytes = await this.bytes(original, options);
|
|
754
|
+
await this.replace(target, options, async (temporary) => {
|
|
755
|
+
await this.#upper.writeFile(temporary, bytes, { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
|
|
756
|
+
if (this.#upper.chmod)
|
|
757
|
+
await this.#upper.chmod(temporary, original.stat.mode & 0o7777, options);
|
|
758
|
+
else if (((await this.#upper.lstat(temporary, options)).mode & 0o7777) !== (original.stat.mode & 0o7777))
|
|
759
|
+
fail("ENOTSUP", destination);
|
|
760
|
+
});
|
|
761
|
+
}, false);
|
|
762
|
+
}
|
|
763
|
+
async readlink(path, options = {}) {
|
|
764
|
+
return this.run(options, async () => {
|
|
765
|
+
const entry = await this.required(path, options, false);
|
|
766
|
+
if (entry.stat.type !== "symlink")
|
|
767
|
+
fail("EINVAL", path);
|
|
768
|
+
if (!entry.backend.readlink)
|
|
769
|
+
fail("ENOTSUP", path);
|
|
770
|
+
return entry.backend.readlink(entry.path, options);
|
|
771
|
+
}, false);
|
|
772
|
+
}
|
|
773
|
+
async symlink(target, path, options = {}) {
|
|
774
|
+
return this.run(options, async () => {
|
|
775
|
+
this.writable(path);
|
|
776
|
+
if (!this.capabilities.symlinks || !this.#upper.symlink)
|
|
777
|
+
fail("ENOTSUP", path);
|
|
778
|
+
validatePath(target);
|
|
779
|
+
if (!target)
|
|
780
|
+
fail("ENOENT", path);
|
|
781
|
+
const location = await this.resolve(path, options, false, true);
|
|
782
|
+
if (location.entry)
|
|
783
|
+
fail("EEXIST", path);
|
|
784
|
+
await this.replace(location, options, (temporary) => this.#upper.symlink(target, temporary, options));
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async link(_existingPath, newPath, options = {}) {
|
|
788
|
+
options.signal?.throwIfAborted();
|
|
789
|
+
fail("ENOTSUP", newPath, "overlay hardlinks are not supported");
|
|
790
|
+
}
|
|
791
|
+
async metadata(path, capability, options, operation) {
|
|
792
|
+
return this.run(options, async () => {
|
|
793
|
+
this.writable(path);
|
|
794
|
+
if (!this.capabilities[capability])
|
|
795
|
+
fail("ENOTSUP", path);
|
|
796
|
+
const entry = await this.required(path, options);
|
|
797
|
+
if (entry.stat.type === "directory") {
|
|
798
|
+
await this.copyUp(entry, options);
|
|
799
|
+
await operation(entry.path);
|
|
800
|
+
}
|
|
801
|
+
else
|
|
802
|
+
await this.replace({ path: entry.path, entry }, options, operation);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async chmod(path, permissions, options = {}) {
|
|
806
|
+
options.signal?.throwIfAborted();
|
|
807
|
+
if (permissions === undefined)
|
|
808
|
+
fail("EINVAL", path);
|
|
809
|
+
mode(permissions, path);
|
|
810
|
+
return this.metadata(path, "permissions", options, (target) => this.#upper.chmod(target, permissions, options));
|
|
811
|
+
}
|
|
812
|
+
async utimes(path, atimeMs, mtimeMs, options = {}) {
|
|
813
|
+
options.signal?.throwIfAborted();
|
|
814
|
+
if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs))
|
|
815
|
+
fail("EINVAL", path);
|
|
816
|
+
return this.metadata(path, "timestamps", options, (target) => this.#upper.utimes(target, atimeMs, mtimeMs, options));
|
|
817
|
+
}
|
|
818
|
+
async truncate(path, length = 0, options = {}) {
|
|
819
|
+
integer(length, path);
|
|
820
|
+
return this.run(options, async () => {
|
|
821
|
+
this.writable(path);
|
|
822
|
+
if (length > this.maxBufferBytes)
|
|
823
|
+
fail("EFBIG", path);
|
|
824
|
+
const entry = await this.required(path, options);
|
|
825
|
+
if (entry.stat.type !== "file")
|
|
826
|
+
fail("EISDIR", path);
|
|
827
|
+
this.permission(entry, 2);
|
|
828
|
+
await this.replace({ path: entry.path, entry }, options, async (temporary) => {
|
|
829
|
+
if (this.#upper.truncate)
|
|
830
|
+
await this.#upper.truncate(temporary, length, options);
|
|
831
|
+
else {
|
|
832
|
+
const previous = await this.#upper.readFile(temporary, { ...options, maxBytes: this.maxBufferBytes });
|
|
833
|
+
const bytes = new Uint8Array(length);
|
|
834
|
+
bytes.set(previous.subarray(0, length));
|
|
835
|
+
await this.#upper.writeFile(temporary, bytes, options);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
async *readStream(path, options = {}) {
|
|
841
|
+
options.signal?.throwIfAborted();
|
|
842
|
+
const start = options.start ?? 0;
|
|
843
|
+
const chunkSize = options.chunkSize ?? 64 * 1024;
|
|
844
|
+
integer(start, path);
|
|
845
|
+
integer(chunkSize, path);
|
|
846
|
+
if (!chunkSize)
|
|
847
|
+
fail("EINVAL", path);
|
|
848
|
+
if (options.endExclusive !== undefined) {
|
|
849
|
+
integer(options.endExclusive, path);
|
|
850
|
+
if (options.endExclusive < start)
|
|
851
|
+
fail("EINVAL", path);
|
|
852
|
+
}
|
|
853
|
+
const entry = await this.run(options, async () => {
|
|
854
|
+
const entry = await this.required(path, options);
|
|
855
|
+
if (entry.stat.type !== "file")
|
|
856
|
+
fail("EISDIR", path);
|
|
857
|
+
this.permission(entry, 4);
|
|
858
|
+
return entry;
|
|
859
|
+
});
|
|
860
|
+
if (entry.backend.readStream && entry.backend.capabilities.streamingRead !== false) {
|
|
861
|
+
for await (const chunk of readBytes(entry.backend.readStream(entry.path, options), options.signal)) {
|
|
862
|
+
yield new Uint8Array(chunk);
|
|
863
|
+
}
|
|
864
|
+
options.signal?.throwIfAborted();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const bytes = await this.bytes(entry, options);
|
|
868
|
+
const end = Math.min(bytes.byteLength, options.endExclusive ?? bytes.byteLength);
|
|
869
|
+
for (let offset = start; offset < end; offset += chunkSize) {
|
|
870
|
+
options.signal?.throwIfAborted();
|
|
871
|
+
yield bytes.slice(offset, Math.min(end, offset + chunkSize));
|
|
872
|
+
}
|
|
873
|
+
options.signal?.throwIfAborted();
|
|
874
|
+
}
|
|
875
|
+
async *bounded(source, path, options) {
|
|
876
|
+
let size = 0;
|
|
877
|
+
for await (const chunk of readBytes(source, options.signal)) {
|
|
878
|
+
if (chunk.byteLength > this.maxBufferBytes - size)
|
|
879
|
+
fail("EFBIG", path);
|
|
880
|
+
size += chunk.byteLength;
|
|
881
|
+
yield new Uint8Array(chunk);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async streamToUpper(path, source, options) {
|
|
885
|
+
if (!this.#upper.writeStream)
|
|
886
|
+
fail("ENOTSUP", path);
|
|
887
|
+
const input = readBytes(source, options.signal);
|
|
888
|
+
let failed = false;
|
|
889
|
+
try {
|
|
890
|
+
await this.#upper.writeStream(path, input, options);
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
failed = true;
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
finally {
|
|
897
|
+
await finishCleanup(() => input.return(undefined), failed);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async writeStream(path, source, options = {}) {
|
|
901
|
+
options.signal?.throwIfAborted();
|
|
902
|
+
this.writeOptions(path, options);
|
|
903
|
+
await this.run(options, () => this.writeLocation(path, options));
|
|
904
|
+
if (this.capabilities.streamingWrite !== false) {
|
|
905
|
+
await this.staged(options, async (incoming) => {
|
|
906
|
+
const bounded = this.bounded(source, path, options);
|
|
907
|
+
await this.streamToUpper(incoming, bounded, { ...options, flag: "wx", mode: 0o600 });
|
|
908
|
+
const size = (await this.#upper.stat(incoming, options)).size;
|
|
909
|
+
await this.run(options, async () => {
|
|
910
|
+
const location = await this.writeLocation(path, options);
|
|
911
|
+
const append = options.flag === "a" || options.flag === "ax";
|
|
912
|
+
if (append && (location.entry?.stat.size ?? 0) + size > this.maxBufferBytes)
|
|
913
|
+
fail("EFBIG", path);
|
|
914
|
+
await this.replace(location, options, async (temporary) => {
|
|
915
|
+
await this.streamToUpper(temporary, this.#upper.readStream(incoming, options), { ...options, flag: append ? "a" : "w" });
|
|
916
|
+
}, true);
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const bytes = await collectBytes(source, { maxBytes: this.maxBufferBytes, ...(options.signal ? { signal: options.signal } : {}) });
|
|
922
|
+
await this.writeFile(path, bytes, options);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
export function createOverlayFileSystem(options) {
|
|
926
|
+
return new OverlayFileSystem(options);
|
|
927
|
+
}
|