@peerbit/shared-fs-cli 0.0.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/LICENSE +202 -0
- package/README.md +84 -0
- package/lib/esm/bin.d.ts +2 -0
- package/lib/esm/bin.js +7 -0
- package/lib/esm/bin.js.map +1 -0
- package/lib/esm/cross-os-interop.d.ts +2 -0
- package/lib/esm/cross-os-interop.js +99 -0
- package/lib/esm/cross-os-interop.js.map +1 -0
- package/lib/esm/index.d.ts +2 -0
- package/lib/esm/index.js +576 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/install-native-adapter.d.ts +2 -0
- package/lib/esm/install-native-adapter.js +38 -0
- package/lib/esm/install-native-adapter.js.map +1 -0
- package/lib/esm/native-adapter.d.ts +55 -0
- package/lib/esm/native-adapter.js +268 -0
- package/lib/esm/native-adapter.js.map +1 -0
- package/package.json +53 -0
- package/scripts/postinstall.mjs +38 -0
- package/src/bin.ts +8 -0
- package/src/cross-os-interop.ts +133 -0
- package/src/index.ts +788 -0
- package/src/install-native-adapter.ts +48 -0
- package/src/native-adapter.ts +434 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NativeMountUnavailableError,
|
|
3
|
+
createSharedFsIpcClient,
|
|
4
|
+
createSharedFsIpcServer,
|
|
5
|
+
createSharedFsMountBackend,
|
|
6
|
+
decodePublicSignKey,
|
|
7
|
+
encodePublicSignKey,
|
|
8
|
+
getNativeMountSupport,
|
|
9
|
+
mountNativeSharedFs,
|
|
10
|
+
openSharedFs,
|
|
11
|
+
runSharedFsBenchmark,
|
|
12
|
+
unmountNativeMountpoint,
|
|
13
|
+
} from "@peerbit/shared-fs";
|
|
14
|
+
import { multiaddr } from "@multiformats/multiaddr";
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { Peerbit } from "peerbit";
|
|
21
|
+
import yargs from "yargs";
|
|
22
|
+
import { hideBin } from "yargs/helpers";
|
|
23
|
+
import {
|
|
24
|
+
installNativeAdapter,
|
|
25
|
+
resolveExternalNativeAdapter,
|
|
26
|
+
} from "./native-adapter.js";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_DIRECTORY_NAME = "peerbit-shared-fs";
|
|
29
|
+
const CLI_REPLICATION_ARGS = {
|
|
30
|
+
replicate: {
|
|
31
|
+
limits: {
|
|
32
|
+
cpu: {
|
|
33
|
+
max: 1,
|
|
34
|
+
monitor: undefined,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
type CliProgramArgs =
|
|
41
|
+
| typeof CLI_REPLICATION_ARGS
|
|
42
|
+
| {
|
|
43
|
+
replicate: false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let peerbitRejectionGuardInstalled = false;
|
|
47
|
+
|
|
48
|
+
const isPeerbitSelfReceiverError = (error: unknown) => {
|
|
49
|
+
return (
|
|
50
|
+
error instanceof Error &&
|
|
51
|
+
error.message.includes(
|
|
52
|
+
"Unexpected to create a message with self as the only receiver"
|
|
53
|
+
) &&
|
|
54
|
+
error.stack?.includes("@peerbit/stream")
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isPeerbitFanoutJoinTimeout = (error: unknown) => {
|
|
59
|
+
return (
|
|
60
|
+
error instanceof Error &&
|
|
61
|
+
error.message.includes("fanout join timed out") &&
|
|
62
|
+
error.stack?.includes("@peerbit/pubsub")
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const installPeerbitRejectionGuard = () => {
|
|
67
|
+
if (peerbitRejectionGuardInstalled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
peerbitRejectionGuardInstalled = true;
|
|
71
|
+
process.on("unhandledRejection", (reason) => {
|
|
72
|
+
if (isPeerbitSelfReceiverError(reason)) {
|
|
73
|
+
console.warn(
|
|
74
|
+
chalk.yellow(
|
|
75
|
+
"Peerbit emitted a known self-addressed RPC during local shared-fs operation; continuing."
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (isPeerbitFanoutJoinTimeout(reason)) {
|
|
81
|
+
console.warn(
|
|
82
|
+
chalk.yellow(
|
|
83
|
+
"Peerbit fanout bootstrap timed out during shared-fs operation; continuing while replication retries."
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
throw reason instanceof Error ? reason : new Error(String(reason));
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const resolveDirectory = (directoryArg?: string) => {
|
|
93
|
+
if (directoryArg === undefined) {
|
|
94
|
+
const directory = path.join(os.homedir(), DEFAULT_DIRECTORY_NAME);
|
|
95
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
96
|
+
return directory;
|
|
97
|
+
}
|
|
98
|
+
if (directoryArg === "" || directoryArg === "null") {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
fs.mkdirSync(directoryArg, { recursive: true });
|
|
102
|
+
return directoryArg;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const normalizeNativeMountpoint = (
|
|
106
|
+
mountpoint: string,
|
|
107
|
+
platform: NodeJS.Platform = process.platform
|
|
108
|
+
) => {
|
|
109
|
+
if (platform === "win32") {
|
|
110
|
+
const driveRoot = /^([a-zA-Z]):[\\/]?$/.exec(mountpoint);
|
|
111
|
+
if (driveRoot) {
|
|
112
|
+
return `${driveRoot[1].toUpperCase()}:`;
|
|
113
|
+
}
|
|
114
|
+
return path.win32.resolve(mountpoint);
|
|
115
|
+
}
|
|
116
|
+
return path.resolve(mountpoint);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const coerceAddresses = (addrs: string | string[]) => {
|
|
120
|
+
return (Array.isArray(addrs) ? addrs : [addrs]).map((address) =>
|
|
121
|
+
multiaddr(address)
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const connectToNetwork = async (
|
|
126
|
+
peerbit: Peerbit,
|
|
127
|
+
peer?: string | string[],
|
|
128
|
+
options?: { bootstrap?: boolean }
|
|
129
|
+
) => {
|
|
130
|
+
if (peer) {
|
|
131
|
+
await peerbit.dial(coerceAddresses(peer));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (options?.bootstrap === false) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await peerbit.bootstrap();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const stopPeerbitForCli = async (
|
|
141
|
+
peerbit: Peerbit,
|
|
142
|
+
options?: { timeoutMs?: number }
|
|
143
|
+
) => {
|
|
144
|
+
const timeoutMs = options?.timeoutMs ?? 10_000;
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
console.warn(
|
|
147
|
+
chalk.yellow(
|
|
148
|
+
`Peer shutdown exceeded ${Math.round(timeoutMs / 1000)} seconds, forcing CLI exit.`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
process.exit(process.exitCode ?? 0);
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
timer.unref?.();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await peerbit.stop();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (!isPeerbitIndexCloseError(error)) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
console.warn(
|
|
162
|
+
chalk.yellow(
|
|
163
|
+
"Peer shutdown hit a known document-index close race; continuing after successful CLI work."
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const isPeerbitIndexCloseError = (error: unknown) => {
|
|
172
|
+
if (!(error instanceof TypeError)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return (
|
|
176
|
+
error.message.includes("clearAll") &&
|
|
177
|
+
error.stack?.includes("DocumentIndex.close")
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const waitForTermination = async (stop: () => Promise<void>) => {
|
|
182
|
+
await new Promise<void>((resolve, reject) => {
|
|
183
|
+
let settled = false;
|
|
184
|
+
const keepAlive = setInterval(() => {}, 1 << 30);
|
|
185
|
+
const finish = async () => {
|
|
186
|
+
if (settled) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
settled = true;
|
|
190
|
+
clearInterval(keepAlive);
|
|
191
|
+
try {
|
|
192
|
+
await stop();
|
|
193
|
+
resolve();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
reject(error);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
process.once("SIGINT", finish);
|
|
199
|
+
process.once("SIGTERM", finish);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const waitForChildExit = async (child: ChildProcess, timeoutMs = 5_000) => {
|
|
204
|
+
if (child.exitCode != null || child.signalCode != null) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
await new Promise<void>((resolve) => {
|
|
208
|
+
const timeout = setTimeout(resolve, timeoutMs);
|
|
209
|
+
child.once("exit", () => {
|
|
210
|
+
clearTimeout(timeout);
|
|
211
|
+
resolve();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const mountExternalNativeAdapter = async (
|
|
217
|
+
command: string,
|
|
218
|
+
endpoint: string,
|
|
219
|
+
mountpoint: string
|
|
220
|
+
) => {
|
|
221
|
+
const args = ["--endpoint", endpoint, "--mountpoint", mountpoint];
|
|
222
|
+
if (process.env.PEERBIT_SHARED_FS_NATIVE_ADAPTER_DEBUG === "1") {
|
|
223
|
+
args.push("--debug");
|
|
224
|
+
}
|
|
225
|
+
const child = spawn(command, args, {
|
|
226
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
227
|
+
});
|
|
228
|
+
child.stderr.on("data", (chunk) => process.stderr.write(chunk));
|
|
229
|
+
|
|
230
|
+
await new Promise<void>((resolve, reject) => {
|
|
231
|
+
let output = "";
|
|
232
|
+
const timeout = setTimeout(() => {
|
|
233
|
+
cleanup();
|
|
234
|
+
reject(
|
|
235
|
+
new Error(
|
|
236
|
+
`Native adapter did not report readiness within 15 seconds: ${command}`
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}, 15_000);
|
|
240
|
+
const cleanup = () => {
|
|
241
|
+
clearTimeout(timeout);
|
|
242
|
+
child.stdout.off("data", onStdout);
|
|
243
|
+
child.off("error", onError);
|
|
244
|
+
child.off("exit", onExit);
|
|
245
|
+
};
|
|
246
|
+
const onStdout = (chunk: Buffer) => {
|
|
247
|
+
output += chunk.toString("utf8");
|
|
248
|
+
process.stdout.write(chunk);
|
|
249
|
+
if (output.includes("peerbit-shared-fs-native ready")) {
|
|
250
|
+
cleanup();
|
|
251
|
+
resolve();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const onError = (error: Error) => {
|
|
255
|
+
cleanup();
|
|
256
|
+
reject(error);
|
|
257
|
+
};
|
|
258
|
+
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
259
|
+
cleanup();
|
|
260
|
+
reject(
|
|
261
|
+
new Error(
|
|
262
|
+
`Native adapter exited before mount readiness: code=${code} signal=${signal}`
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
child.stdout.on("data", onStdout);
|
|
267
|
+
child.once("error", onError);
|
|
268
|
+
child.once("exit", onExit);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
mountpoint,
|
|
273
|
+
async unmount() {
|
|
274
|
+
if (child.exitCode != null || child.signalCode != null) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
child.kill("SIGINT");
|
|
278
|
+
await waitForChildExit(child);
|
|
279
|
+
if (child.exitCode == null && child.signalCode == null) {
|
|
280
|
+
child.kill("SIGTERM");
|
|
281
|
+
await waitForChildExit(child);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const configureExternalNativeAdapterEnv = async () => {
|
|
288
|
+
const adapter = await resolveExternalNativeAdapter();
|
|
289
|
+
if (adapter && !process.env.PEERBIT_SHARED_FS_NATIVE_ADAPTER) {
|
|
290
|
+
process.env.PEERBIT_SHARED_FS_NATIVE_ADAPTER = adapter;
|
|
291
|
+
}
|
|
292
|
+
return adapter;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const printNativeRequirements = async () => {
|
|
296
|
+
const externalAdapter = await configureExternalNativeAdapterEnv();
|
|
297
|
+
const support = await getNativeMountSupport();
|
|
298
|
+
console.log(chalk.bold("Native mount status"));
|
|
299
|
+
console.log(`platform: ${support.platform}`);
|
|
300
|
+
console.log(`adapter: ${support.adapter}`);
|
|
301
|
+
console.log(`external adapter: ${externalAdapter ?? "not found"}`);
|
|
302
|
+
console.log(`available: ${support.available ? "yes" : "no"}`);
|
|
303
|
+
if (support.missing.length > 0) {
|
|
304
|
+
console.log("missing:");
|
|
305
|
+
for (const item of support.missing) {
|
|
306
|
+
console.log(` - ${item}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const note of support.notes) {
|
|
310
|
+
console.log(`note: ${note}`);
|
|
311
|
+
}
|
|
312
|
+
console.log("");
|
|
313
|
+
console.log(chalk.bold("Native mount requirements"));
|
|
314
|
+
console.log(
|
|
315
|
+
"linux: libfuse/FUSE plus fuse-native or the peerbit-shared-fs-native adapter"
|
|
316
|
+
);
|
|
317
|
+
console.log(
|
|
318
|
+
"macOS: macFUSE plus fuse-native or the peerbit-shared-fs-native adapter"
|
|
319
|
+
);
|
|
320
|
+
console.log(
|
|
321
|
+
"windows: WinFsp runtime plus the peerbit-shared-fs-native adapter"
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const printBenchmarkResult = (
|
|
326
|
+
result: Awaited<ReturnType<typeof runSharedFsBenchmark>>
|
|
327
|
+
) => {
|
|
328
|
+
console.log(chalk.bold(`benchmark root: ${result.root}`));
|
|
329
|
+
console.log(
|
|
330
|
+
`large write: ${result.largeFile.writeMs}ms ${result.largeFile.writeMbps.toFixed(2)} Mbps`
|
|
331
|
+
);
|
|
332
|
+
console.log(
|
|
333
|
+
`large read: ${result.largeFile.readMs}ms ${result.largeFile.readMbps.toFixed(2)} Mbps`
|
|
334
|
+
);
|
|
335
|
+
console.log(
|
|
336
|
+
`small write: ${result.smallFiles.writeMs}ms ${result.smallFiles.filesPerSecondWrite.toFixed(2)} files/s`
|
|
337
|
+
);
|
|
338
|
+
console.log(`small list: ${result.smallFiles.listMs}ms`);
|
|
339
|
+
console.log(
|
|
340
|
+
`small read: ${result.smallFiles.readMs}ms ${result.smallFiles.filesPerSecondRead.toFixed(2)} files/s`
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const openCliFs = async (
|
|
345
|
+
peerbit: Peerbit,
|
|
346
|
+
options: {
|
|
347
|
+
address?: string;
|
|
348
|
+
machineLabel?: string;
|
|
349
|
+
replicate?: boolean;
|
|
350
|
+
rootKey?: Peerbit["identity"]["publicKey"];
|
|
351
|
+
}
|
|
352
|
+
) => {
|
|
353
|
+
const programArgs: CliProgramArgs =
|
|
354
|
+
options.replicate === false
|
|
355
|
+
? { replicate: false }
|
|
356
|
+
: CLI_REPLICATION_ARGS;
|
|
357
|
+
return openSharedFs({
|
|
358
|
+
peerbit,
|
|
359
|
+
address: options.address,
|
|
360
|
+
machineLabel: options.machineLabel || os.hostname(),
|
|
361
|
+
rootKey: options.rootKey,
|
|
362
|
+
...programArgs,
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
export const runCli = async (args = hideBin(process.argv)) => {
|
|
367
|
+
installPeerbitRejectionGuard();
|
|
368
|
+
await yargs(args)
|
|
369
|
+
.scriptName("peerbit-fs")
|
|
370
|
+
.option("directory", {
|
|
371
|
+
alias: "d",
|
|
372
|
+
type: "string",
|
|
373
|
+
description:
|
|
374
|
+
"Peerbit state directory. Use an empty string for in-memory state.",
|
|
375
|
+
})
|
|
376
|
+
.option("machine", {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "Machine label stored on every signed file version.",
|
|
379
|
+
})
|
|
380
|
+
.option("peer", {
|
|
381
|
+
type: "string",
|
|
382
|
+
array: true,
|
|
383
|
+
description:
|
|
384
|
+
"Multiaddr peer to dial before opening a shared filesystem.",
|
|
385
|
+
})
|
|
386
|
+
.option("replicate", {
|
|
387
|
+
type: "boolean",
|
|
388
|
+
default: true,
|
|
389
|
+
description:
|
|
390
|
+
"Help replicate remote data. Disable with --no-replicate.",
|
|
391
|
+
})
|
|
392
|
+
.command(
|
|
393
|
+
"create",
|
|
394
|
+
"create a new experimental shared filesystem",
|
|
395
|
+
(command) =>
|
|
396
|
+
command.option("auth", {
|
|
397
|
+
type: "boolean",
|
|
398
|
+
default: true,
|
|
399
|
+
description:
|
|
400
|
+
"Create with trusted-writer access control rooted at this peer identity. Disable with --no-auth.",
|
|
401
|
+
}),
|
|
402
|
+
async (argv) => {
|
|
403
|
+
const directory = resolveDirectory(argv.directory);
|
|
404
|
+
const peerbit = await Peerbit.create({ directory });
|
|
405
|
+
try {
|
|
406
|
+
const fsHandle = await openCliFs(peerbit, {
|
|
407
|
+
machineLabel: argv.machine,
|
|
408
|
+
replicate: false,
|
|
409
|
+
rootKey: argv.auth
|
|
410
|
+
? peerbit.identity.publicKey
|
|
411
|
+
: undefined,
|
|
412
|
+
});
|
|
413
|
+
console.log(fsHandle.address);
|
|
414
|
+
} finally {
|
|
415
|
+
await stopPeerbitForCli(peerbit);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
.command(
|
|
420
|
+
"whoami",
|
|
421
|
+
"print the local Peerbit writer public key",
|
|
422
|
+
(command) => command,
|
|
423
|
+
async (argv) => {
|
|
424
|
+
const directory = resolveDirectory(argv.directory);
|
|
425
|
+
const peerbit = await Peerbit.create({ directory });
|
|
426
|
+
try {
|
|
427
|
+
console.log(
|
|
428
|
+
encodePublicSignKey(peerbit.identity.publicKey)
|
|
429
|
+
);
|
|
430
|
+
} finally {
|
|
431
|
+
await stopPeerbitForCli(peerbit);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
.command(
|
|
436
|
+
"trust <address> <public-key>",
|
|
437
|
+
"authorize a writer key on an access-controlled shared filesystem",
|
|
438
|
+
(command) =>
|
|
439
|
+
command
|
|
440
|
+
.positional("address", {
|
|
441
|
+
type: "string",
|
|
442
|
+
demandOption: true,
|
|
443
|
+
})
|
|
444
|
+
.positional("public-key", {
|
|
445
|
+
type: "string",
|
|
446
|
+
demandOption: true,
|
|
447
|
+
description:
|
|
448
|
+
"Base64 public key printed by peerbit-fs whoami.",
|
|
449
|
+
}),
|
|
450
|
+
async (argv) => {
|
|
451
|
+
const directory = resolveDirectory(argv.directory);
|
|
452
|
+
const peerbit = await Peerbit.create({ directory });
|
|
453
|
+
try {
|
|
454
|
+
await connectToNetwork(peerbit, argv.peer, {
|
|
455
|
+
bootstrap: argv.replicate !== false,
|
|
456
|
+
});
|
|
457
|
+
const fsHandle = await openCliFs(peerbit, {
|
|
458
|
+
address: argv.address,
|
|
459
|
+
machineLabel: argv.machine,
|
|
460
|
+
replicate: argv.replicate,
|
|
461
|
+
});
|
|
462
|
+
await fsHandle.authorizeWriter(
|
|
463
|
+
decodePublicSignKey(String(argv.publicKey))
|
|
464
|
+
);
|
|
465
|
+
console.log(chalk.green("Writer trusted"));
|
|
466
|
+
} finally {
|
|
467
|
+
await stopPeerbitForCli(peerbit);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
.command(
|
|
472
|
+
"install-adapter",
|
|
473
|
+
"download and install the prebuilt native mount adapter",
|
|
474
|
+
(command) =>
|
|
475
|
+
command
|
|
476
|
+
.option("prefix", {
|
|
477
|
+
type: "string",
|
|
478
|
+
description:
|
|
479
|
+
"Install directory. Defaults to ~/.peerbit/shared-fs/bin.",
|
|
480
|
+
})
|
|
481
|
+
.option("adapter-version", {
|
|
482
|
+
type: "string",
|
|
483
|
+
description:
|
|
484
|
+
"Adapter release version. Defaults to this CLI package version.",
|
|
485
|
+
})
|
|
486
|
+
.option("base-url", {
|
|
487
|
+
type: "string",
|
|
488
|
+
description:
|
|
489
|
+
"Release asset base URL override for mirrors or test builds.",
|
|
490
|
+
})
|
|
491
|
+
.option("force", {
|
|
492
|
+
type: "boolean",
|
|
493
|
+
default: false,
|
|
494
|
+
description: "Replace an existing installed adapter.",
|
|
495
|
+
})
|
|
496
|
+
.option("print-path", {
|
|
497
|
+
type: "boolean",
|
|
498
|
+
default: false,
|
|
499
|
+
description:
|
|
500
|
+
"Print the installed adapter path after resolving/installing.",
|
|
501
|
+
})
|
|
502
|
+
.option("if-needed", {
|
|
503
|
+
type: "boolean",
|
|
504
|
+
default: false,
|
|
505
|
+
hidden: true,
|
|
506
|
+
}),
|
|
507
|
+
async (argv) => {
|
|
508
|
+
const result = await installNativeAdapter({
|
|
509
|
+
installDir: argv.prefix,
|
|
510
|
+
version: argv.adapterVersion,
|
|
511
|
+
baseUrl: argv.baseUrl,
|
|
512
|
+
force: argv.force,
|
|
513
|
+
ifNeeded: argv.ifNeeded,
|
|
514
|
+
});
|
|
515
|
+
if (argv.printPath) {
|
|
516
|
+
console.log(result.binaryPath);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (result.installed) {
|
|
520
|
+
console.log(
|
|
521
|
+
chalk.green(
|
|
522
|
+
`Installed native adapter ${result.assetName} at ${result.binaryPath}`
|
|
523
|
+
)
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
console.log(
|
|
528
|
+
chalk.gray(
|
|
529
|
+
`Native adapter already installed at ${result.binaryPath}`
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
.command(
|
|
535
|
+
"mount <address> <mountpoint>",
|
|
536
|
+
"mount a shared filesystem using the native adapter",
|
|
537
|
+
(command) =>
|
|
538
|
+
command
|
|
539
|
+
.positional("address", {
|
|
540
|
+
type: "string",
|
|
541
|
+
demandOption: true,
|
|
542
|
+
})
|
|
543
|
+
.positional("mountpoint", {
|
|
544
|
+
type: "string",
|
|
545
|
+
demandOption: true,
|
|
546
|
+
})
|
|
547
|
+
.option("native-adapter", {
|
|
548
|
+
type: "string",
|
|
549
|
+
description:
|
|
550
|
+
"External native adapter command. Can also be set with PEERBIT_SHARED_FS_NATIVE_ADAPTER.",
|
|
551
|
+
}),
|
|
552
|
+
async (argv) => {
|
|
553
|
+
const directory = resolveDirectory(argv.directory);
|
|
554
|
+
const peerbit = await Peerbit.create({ directory });
|
|
555
|
+
let ipc:
|
|
556
|
+
| Awaited<ReturnType<typeof createSharedFsIpcServer>>
|
|
557
|
+
| undefined;
|
|
558
|
+
let mounted:
|
|
559
|
+
| Awaited<ReturnType<typeof mountNativeSharedFs>>
|
|
560
|
+
| Awaited<ReturnType<typeof mountExternalNativeAdapter>>
|
|
561
|
+
| undefined;
|
|
562
|
+
try {
|
|
563
|
+
await connectToNetwork(peerbit, argv.peer, {
|
|
564
|
+
bootstrap: argv.replicate !== false,
|
|
565
|
+
});
|
|
566
|
+
const fsHandle = await openCliFs(peerbit, {
|
|
567
|
+
address: argv.address,
|
|
568
|
+
machineLabel: argv.machine,
|
|
569
|
+
replicate: argv.replicate,
|
|
570
|
+
});
|
|
571
|
+
const backend = createSharedFsMountBackend(fsHandle);
|
|
572
|
+
const externalAdapter = await resolveExternalNativeAdapter(
|
|
573
|
+
argv.nativeAdapter
|
|
574
|
+
);
|
|
575
|
+
const mountpoint = normalizeNativeMountpoint(
|
|
576
|
+
String(argv.mountpoint)
|
|
577
|
+
);
|
|
578
|
+
ipc = await createSharedFsIpcServer(
|
|
579
|
+
backend,
|
|
580
|
+
externalAdapter ? "tcp://127.0.0.1:0" : undefined
|
|
581
|
+
);
|
|
582
|
+
mounted = externalAdapter
|
|
583
|
+
? await mountExternalNativeAdapter(
|
|
584
|
+
externalAdapter,
|
|
585
|
+
ipc.endpoint,
|
|
586
|
+
mountpoint
|
|
587
|
+
)
|
|
588
|
+
: await mountNativeSharedFs(
|
|
589
|
+
createSharedFsIpcClient(ipc.endpoint),
|
|
590
|
+
{
|
|
591
|
+
mountpoint,
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
console.log(
|
|
595
|
+
chalk.green(
|
|
596
|
+
`Mounted ${fsHandle.address} at ${mounted.mountpoint}`
|
|
597
|
+
)
|
|
598
|
+
);
|
|
599
|
+
console.log(`IPC endpoint: ${ipc.endpoint}`);
|
|
600
|
+
await waitForTermination(async () => {
|
|
601
|
+
await mounted?.unmount();
|
|
602
|
+
await ipc?.close();
|
|
603
|
+
await stopPeerbitForCli(peerbit);
|
|
604
|
+
});
|
|
605
|
+
} catch (error) {
|
|
606
|
+
await mounted?.unmount().catch(() => {});
|
|
607
|
+
await ipc?.close().catch(() => {});
|
|
608
|
+
await stopPeerbitForCli(peerbit).catch(() => {});
|
|
609
|
+
if (error instanceof NativeMountUnavailableError) {
|
|
610
|
+
console.error(chalk.red(error.message));
|
|
611
|
+
await printNativeRequirements();
|
|
612
|
+
process.exitCode = 1;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
.command(
|
|
620
|
+
"status [address]",
|
|
621
|
+
"show local adapter requirements and optional filesystem status",
|
|
622
|
+
(command) =>
|
|
623
|
+
command.positional("address", {
|
|
624
|
+
type: "string",
|
|
625
|
+
}),
|
|
626
|
+
async (argv) => {
|
|
627
|
+
await printNativeRequirements();
|
|
628
|
+
if (!argv.address) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const directory = resolveDirectory(argv.directory);
|
|
632
|
+
const peerbit = await Peerbit.create({ directory });
|
|
633
|
+
try {
|
|
634
|
+
await connectToNetwork(peerbit, argv.peer, {
|
|
635
|
+
bootstrap: argv.replicate !== false,
|
|
636
|
+
});
|
|
637
|
+
const fsHandle = await openCliFs(peerbit, {
|
|
638
|
+
address: argv.address,
|
|
639
|
+
machineLabel: argv.machine,
|
|
640
|
+
replicate: argv.replicate,
|
|
641
|
+
});
|
|
642
|
+
const rootEntries = await fsHandle.list("/");
|
|
643
|
+
const conflicts = await fsHandle.conflicts();
|
|
644
|
+
console.log(`address: ${fsHandle.address}`);
|
|
645
|
+
console.log(`local public key: ${fsHandle.localPublicKey}`);
|
|
646
|
+
console.log(
|
|
647
|
+
`access controlled: ${
|
|
648
|
+
fsHandle.accessControlled ? "yes" : "no"
|
|
649
|
+
}`
|
|
650
|
+
);
|
|
651
|
+
if (fsHandle.rootKey) {
|
|
652
|
+
console.log(`root key: ${fsHandle.rootKey}`);
|
|
653
|
+
}
|
|
654
|
+
console.log(`root entries: ${rootEntries.length}`);
|
|
655
|
+
console.log(`conflicts: ${conflicts.length}`);
|
|
656
|
+
} finally {
|
|
657
|
+
await stopPeerbitForCli(peerbit);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
.command(
|
|
662
|
+
"conflicts <address>",
|
|
663
|
+
"list visible conflict versions",
|
|
664
|
+
(command) =>
|
|
665
|
+
command.positional("address", {
|
|
666
|
+
type: "string",
|
|
667
|
+
demandOption: true,
|
|
668
|
+
}),
|
|
669
|
+
async (argv) => {
|
|
670
|
+
const directory = resolveDirectory(argv.directory);
|
|
671
|
+
const peerbit = await Peerbit.create({ directory });
|
|
672
|
+
try {
|
|
673
|
+
await connectToNetwork(peerbit, argv.peer, {
|
|
674
|
+
bootstrap: argv.replicate !== false,
|
|
675
|
+
});
|
|
676
|
+
const fsHandle = await openCliFs(peerbit, {
|
|
677
|
+
address: argv.address,
|
|
678
|
+
machineLabel: argv.machine,
|
|
679
|
+
replicate: argv.replicate,
|
|
680
|
+
});
|
|
681
|
+
const conflicts = await fsHandle.conflicts();
|
|
682
|
+
if (conflicts.length === 0) {
|
|
683
|
+
console.log("No conflicts");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
for (const conflict of conflicts) {
|
|
687
|
+
console.log(chalk.bold(conflict.path));
|
|
688
|
+
for (const version of conflict.versions) {
|
|
689
|
+
console.log(
|
|
690
|
+
` ${version.id} ${version.size} bytes ${version.machineLabel} ${version.authorKey}`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} finally {
|
|
695
|
+
await stopPeerbitForCli(peerbit);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
.command(
|
|
700
|
+
"benchmark [address]",
|
|
701
|
+
"run a baseline large-file and many-small-files workload",
|
|
702
|
+
(command) =>
|
|
703
|
+
command
|
|
704
|
+
.positional("address", {
|
|
705
|
+
type: "string",
|
|
706
|
+
})
|
|
707
|
+
.option("large-size", {
|
|
708
|
+
type: "number",
|
|
709
|
+
default: 16 * 1024 * 1024,
|
|
710
|
+
description: "Large file size in bytes.",
|
|
711
|
+
})
|
|
712
|
+
.option("small-files", {
|
|
713
|
+
type: "number",
|
|
714
|
+
default: 200,
|
|
715
|
+
description: "Number of small files to write and read.",
|
|
716
|
+
})
|
|
717
|
+
.option("small-size", {
|
|
718
|
+
type: "number",
|
|
719
|
+
default: 1024,
|
|
720
|
+
description: "Small file size in bytes.",
|
|
721
|
+
})
|
|
722
|
+
.option("root", {
|
|
723
|
+
type: "string",
|
|
724
|
+
description:
|
|
725
|
+
"Benchmark root path inside the shared filesystem.",
|
|
726
|
+
})
|
|
727
|
+
.option("cleanup", {
|
|
728
|
+
type: "boolean",
|
|
729
|
+
default: false,
|
|
730
|
+
description:
|
|
731
|
+
"Delete benchmark files after metrics are collected.",
|
|
732
|
+
})
|
|
733
|
+
.option("json", {
|
|
734
|
+
type: "boolean",
|
|
735
|
+
default: false,
|
|
736
|
+
description: "Print machine-readable JSON.",
|
|
737
|
+
}),
|
|
738
|
+
async (argv) => {
|
|
739
|
+
const directory = resolveDirectory(argv.directory);
|
|
740
|
+
const peerbit = await Peerbit.create({ directory });
|
|
741
|
+
try {
|
|
742
|
+
if (argv.address) {
|
|
743
|
+
await connectToNetwork(peerbit, argv.peer, {
|
|
744
|
+
bootstrap: argv.replicate !== false,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const fsHandle = await openCliFs(peerbit, {
|
|
748
|
+
address: argv.address,
|
|
749
|
+
machineLabel: argv.machine,
|
|
750
|
+
replicate: argv.replicate,
|
|
751
|
+
});
|
|
752
|
+
const result = await runSharedFsBenchmark(fsHandle, {
|
|
753
|
+
root: argv.root,
|
|
754
|
+
largeFileSize: argv.largeSize,
|
|
755
|
+
smallFileCount: argv.smallFiles,
|
|
756
|
+
smallFileSize: argv.smallSize,
|
|
757
|
+
cleanup: argv.cleanup,
|
|
758
|
+
});
|
|
759
|
+
if (argv.json) {
|
|
760
|
+
console.log(JSON.stringify(result, null, 2));
|
|
761
|
+
} else {
|
|
762
|
+
printBenchmarkResult(result);
|
|
763
|
+
}
|
|
764
|
+
} finally {
|
|
765
|
+
await stopPeerbitForCli(peerbit);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
)
|
|
769
|
+
.command(
|
|
770
|
+
"unmount <mountpoint>",
|
|
771
|
+
"unmount a native shared filesystem mountpoint",
|
|
772
|
+
(command) =>
|
|
773
|
+
command.positional("mountpoint", {
|
|
774
|
+
type: "string",
|
|
775
|
+
demandOption: true,
|
|
776
|
+
}),
|
|
777
|
+
async (argv) => {
|
|
778
|
+
await unmountNativeMountpoint(
|
|
779
|
+
normalizeNativeMountpoint(String(argv.mountpoint))
|
|
780
|
+
);
|
|
781
|
+
console.log(chalk.green(`Unmounted ${argv.mountpoint}`));
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
.demandCommand(1)
|
|
785
|
+
.strict()
|
|
786
|
+
.help()
|
|
787
|
+
.parseAsync();
|
|
788
|
+
};
|