@rome-os/node-core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/dist/actions.d.ts +4 -0
- package/dist/client.d.ts +4 -1
- package/dist/client.js +30 -1
- package/dist/connect.js +2 -3
- package/dist/connector.d.ts +10 -1
- package/dist/connector.js +129 -7
- package/dist/daemon-client.d.ts +8 -4
- package/dist/daemon-client.js +86 -2
- package/dist/daemon-control.js +13 -2
- package/dist/daemon-protocol.d.ts +22 -1
- package/dist/daemon-protocol.js +22 -1
- package/dist/daemon-rpc.d.ts +7 -2
- package/dist/daemon-rpc.js +49 -17
- package/dist/daemon.js +167 -21
- package/dist/device-authorization.d.ts +8 -0
- package/dist/device-authorization.js +156 -0
- package/dist/device-session.d.ts +7 -0
- package/dist/device-session.js +16 -0
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +180 -66
- package/dist/frame.d.ts +24 -0
- package/dist/frame.js +64 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/login.d.ts +2 -5
- package/dist/login.js +5 -8
- package/dist/transfer.d.ts +102 -0
- package/dist/transfer.js +518 -0
- package/package.json +1 -1
package/dist/transfer.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { open, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { actionError, isRecord, parseResponse } from "./actions.js";
|
|
6
|
+
import { FRAME_HEADER_BYTES, MAX_FRAME_BYTES } from "./frame.js";
|
|
7
|
+
export const TRANSFER_VERSION = 1;
|
|
8
|
+
export const TRANSFER_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
export const TRANSFER_WINDOW_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
export const TRANSFER_IDLE_MS = 60_000;
|
|
11
|
+
export const MAX_HOST_TRANSFERS = 4;
|
|
12
|
+
export const PART_SUFFIX = ".rome-part";
|
|
13
|
+
const isSize = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
const isDigest = (value) => typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
15
|
+
export function parseTransferMessage(value) {
|
|
16
|
+
if (!isRecord(value) || value.type !== "transfer")
|
|
17
|
+
return null;
|
|
18
|
+
switch (value.kind) {
|
|
19
|
+
case "data":
|
|
20
|
+
case "ack":
|
|
21
|
+
return isSize(value.offset)
|
|
22
|
+
? { type: "transfer", kind: value.kind, offset: value.offset }
|
|
23
|
+
: null;
|
|
24
|
+
case "end":
|
|
25
|
+
case "done":
|
|
26
|
+
return isSize(value.size) && isDigest(value.sha256)
|
|
27
|
+
? { type: "transfer", kind: value.kind, size: value.size, sha256: value.sha256 }
|
|
28
|
+
: null;
|
|
29
|
+
case "abort":
|
|
30
|
+
return typeof value.code === "string" && typeof value.message === "string"
|
|
31
|
+
? { type: "transfer", kind: "abort", code: value.code, message: value.message }
|
|
32
|
+
: null;
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class TransferError extends Error {
|
|
38
|
+
code;
|
|
39
|
+
constructor(code, message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.code = code;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function fsError(error) {
|
|
45
|
+
const code = error?.code;
|
|
46
|
+
return new TransferError(code === "ENOENT"
|
|
47
|
+
? "not_found"
|
|
48
|
+
: code === "EACCES" || code === "EPERM"
|
|
49
|
+
? "permission_denied"
|
|
50
|
+
: code === "EISDIR"
|
|
51
|
+
? "is_directory"
|
|
52
|
+
: "io_error", error instanceof Error ? error.message : "The file operation failed.");
|
|
53
|
+
}
|
|
54
|
+
const closed = (stream) => stream.closed
|
|
55
|
+
? Promise.resolve()
|
|
56
|
+
: new Promise((resolve) => {
|
|
57
|
+
stream.once("close", () => resolve());
|
|
58
|
+
stream.destroy();
|
|
59
|
+
});
|
|
60
|
+
function lifecycle(send, idleMs, cleanup) {
|
|
61
|
+
let settled = false;
|
|
62
|
+
let timer;
|
|
63
|
+
let resolve;
|
|
64
|
+
let reject;
|
|
65
|
+
const result = new Promise((done, fail) => {
|
|
66
|
+
resolve = done;
|
|
67
|
+
reject = fail;
|
|
68
|
+
});
|
|
69
|
+
result.catch(() => { });
|
|
70
|
+
const abort = (code, message, notify = true) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
if (notify)
|
|
76
|
+
send({ type: "transfer", kind: "abort", code, message });
|
|
77
|
+
void cleanup()
|
|
78
|
+
.catch(() => { })
|
|
79
|
+
.finally(() => reject(new TransferError(code, message)));
|
|
80
|
+
};
|
|
81
|
+
const touch = () => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
timer = setTimeout(() => abort("timeout", `No transfer progress for ${idleMs / 1000} seconds.`), idleMs);
|
|
84
|
+
timer.unref();
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
result,
|
|
88
|
+
abort,
|
|
89
|
+
touch,
|
|
90
|
+
finish(summary) {
|
|
91
|
+
if (settled)
|
|
92
|
+
return;
|
|
93
|
+
settled = true;
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
resolve(summary);
|
|
96
|
+
},
|
|
97
|
+
get settled() {
|
|
98
|
+
return settled;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const notAFile = (path) => new TransferError("not_a_file", `${path} is not a regular file.`);
|
|
103
|
+
export async function openSource(path) {
|
|
104
|
+
const info = await stat(path).catch((error) => {
|
|
105
|
+
throw fsError(error);
|
|
106
|
+
});
|
|
107
|
+
if (!info.isFile())
|
|
108
|
+
throw notAFile(path);
|
|
109
|
+
let handle;
|
|
110
|
+
try {
|
|
111
|
+
handle = await open(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw fsError(error);
|
|
115
|
+
}
|
|
116
|
+
let size;
|
|
117
|
+
try {
|
|
118
|
+
const opened = await handle.stat();
|
|
119
|
+
if (!opened.isFile())
|
|
120
|
+
throw notAFile(path);
|
|
121
|
+
size = opened.size;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
await handle.close().catch(() => { });
|
|
125
|
+
throw error instanceof TransferError ? error : fsError(error);
|
|
126
|
+
}
|
|
127
|
+
let started = false;
|
|
128
|
+
return {
|
|
129
|
+
size,
|
|
130
|
+
start(send, options) {
|
|
131
|
+
started = true;
|
|
132
|
+
return startSender(handle, send, options);
|
|
133
|
+
},
|
|
134
|
+
async close() {
|
|
135
|
+
if (!started)
|
|
136
|
+
await handle.close().catch(() => { });
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function startSender(handle, send, options = {}) {
|
|
141
|
+
const chunkBytes = options.chunkBytes ?? TRANSFER_CHUNK_BYTES;
|
|
142
|
+
const windowBytes = options.windowBytes ?? TRANSFER_WINDOW_BYTES;
|
|
143
|
+
if (windowBytes < chunkBytes)
|
|
144
|
+
throw new RangeError("The window must hold at least one chunk.");
|
|
145
|
+
if (chunkBytes > MAX_FRAME_BYTES - FRAME_HEADER_BYTES - 1024)
|
|
146
|
+
throw new RangeError("A chunk must fit in one frame.");
|
|
147
|
+
const stream = handle.createReadStream({ highWaterMark: chunkBytes });
|
|
148
|
+
const hash = createHash("sha256");
|
|
149
|
+
let sent = 0;
|
|
150
|
+
let acked = 0;
|
|
151
|
+
let sha256;
|
|
152
|
+
const life = lifecycle(send, options.idleMs ?? TRANSFER_IDLE_MS, () => closed(stream));
|
|
153
|
+
const full = () => sent - acked + chunkBytes > windowBytes;
|
|
154
|
+
const lost = () => life.abort("connection_lost", "The device connection was lost. Run the copy again.", false);
|
|
155
|
+
stream.on("data", (chunk) => {
|
|
156
|
+
if (life.settled)
|
|
157
|
+
return;
|
|
158
|
+
const bytes = chunk;
|
|
159
|
+
hash.update(bytes);
|
|
160
|
+
if (!send({ type: "transfer", kind: "data", offset: sent }, bytes))
|
|
161
|
+
return lost();
|
|
162
|
+
sent += bytes.byteLength;
|
|
163
|
+
life.touch();
|
|
164
|
+
if (full())
|
|
165
|
+
stream.pause();
|
|
166
|
+
});
|
|
167
|
+
stream.once("end", () => {
|
|
168
|
+
if (life.settled)
|
|
169
|
+
return;
|
|
170
|
+
sha256 = hash.digest("hex");
|
|
171
|
+
if (!send({ type: "transfer", kind: "end", size: sent, sha256 }))
|
|
172
|
+
return lost();
|
|
173
|
+
life.touch();
|
|
174
|
+
});
|
|
175
|
+
stream.once("error", (error) => life.abort("read_failed", error.message));
|
|
176
|
+
life.touch();
|
|
177
|
+
return {
|
|
178
|
+
result: life.result,
|
|
179
|
+
abort: life.abort,
|
|
180
|
+
receive(message) {
|
|
181
|
+
if (life.settled)
|
|
182
|
+
return;
|
|
183
|
+
if (message.kind === "ack") {
|
|
184
|
+
if (message.offset <= acked || message.offset > sent)
|
|
185
|
+
return;
|
|
186
|
+
acked = message.offset;
|
|
187
|
+
life.touch();
|
|
188
|
+
options.onProgress?.(acked);
|
|
189
|
+
if (stream.isPaused() && !full())
|
|
190
|
+
stream.resume();
|
|
191
|
+
}
|
|
192
|
+
else if (message.kind === "done") {
|
|
193
|
+
if (sha256 === undefined || message.size !== sent || message.sha256 !== sha256)
|
|
194
|
+
life.abort("checksum_mismatch", "The receiver reported a different size or checksum.");
|
|
195
|
+
else
|
|
196
|
+
life.finish({ size: sent, sha256 });
|
|
197
|
+
}
|
|
198
|
+
else if (message.kind === "abort")
|
|
199
|
+
life.abort(message.code, message.message, false);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export async function createPartFile(path, id = randomUUID()) {
|
|
204
|
+
const name = Buffer.from(basename(path)).subarray(0, 160).toString("utf8");
|
|
205
|
+
const part = join(dirname(path), `.${name}.${id}${PART_SUFFIX}`);
|
|
206
|
+
try {
|
|
207
|
+
return { part, handle: await open(part, "wx", 0o600) };
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw fsError(error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export async function openDestination(path) {
|
|
214
|
+
const existing = await stat(path).catch((error) => {
|
|
215
|
+
if (error.code === "ENOENT")
|
|
216
|
+
return null;
|
|
217
|
+
throw fsError(error);
|
|
218
|
+
});
|
|
219
|
+
if (existing?.isDirectory())
|
|
220
|
+
throw new TransferError("is_directory", `${path} is a directory.`);
|
|
221
|
+
const { part, handle } = await createPartFile(path);
|
|
222
|
+
let started = false;
|
|
223
|
+
return {
|
|
224
|
+
part,
|
|
225
|
+
start(send, options) {
|
|
226
|
+
started = true;
|
|
227
|
+
return startReceiver(handle, path, part, send, options);
|
|
228
|
+
},
|
|
229
|
+
async cancel() {
|
|
230
|
+
if (started)
|
|
231
|
+
return;
|
|
232
|
+
await handle.close().catch(() => { });
|
|
233
|
+
await rm(part, { force: true });
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function finalMode(path) {
|
|
238
|
+
const current = await stat(path).catch(() => null);
|
|
239
|
+
if (current)
|
|
240
|
+
return current.mode & 0o777;
|
|
241
|
+
const probe = join(dirname(path), `.${randomUUID()}.rome-mode`);
|
|
242
|
+
const handle = await open(probe, "wx");
|
|
243
|
+
try {
|
|
244
|
+
return (await handle.stat()).mode & 0o777;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await handle.close();
|
|
248
|
+
await rm(probe, { force: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function startReceiver(handle, path, part, send, options = {}) {
|
|
252
|
+
const stream = handle.createWriteStream({ flush: true });
|
|
253
|
+
const hash = createHash("sha256");
|
|
254
|
+
let expected = options.size;
|
|
255
|
+
let received = 0;
|
|
256
|
+
let written = 0;
|
|
257
|
+
let ending = false;
|
|
258
|
+
const life = lifecycle(send, options.idleMs ?? TRANSFER_IDLE_MS, async () => {
|
|
259
|
+
await closed(stream);
|
|
260
|
+
await rm(part, { force: true });
|
|
261
|
+
});
|
|
262
|
+
stream.on("error", (error) => life.abort("write_failed", error.message));
|
|
263
|
+
async function finish(size, sha256) {
|
|
264
|
+
const actual = hash.digest("hex");
|
|
265
|
+
if (process.platform !== "win32")
|
|
266
|
+
await handle.chmod(await finalMode(path));
|
|
267
|
+
if (life.settled)
|
|
268
|
+
return;
|
|
269
|
+
await new Promise((resolve, reject) => {
|
|
270
|
+
stream.once("close", resolve);
|
|
271
|
+
stream.once("error", reject);
|
|
272
|
+
stream.end();
|
|
273
|
+
});
|
|
274
|
+
if (life.settled)
|
|
275
|
+
return;
|
|
276
|
+
if (received !== size || (expected !== undefined && expected !== size))
|
|
277
|
+
return life.abort("size_mismatch", `Received ${received} bytes, expected ${expected ?? size}. ${path} was not changed.`);
|
|
278
|
+
if (actual !== sha256)
|
|
279
|
+
return life.abort("checksum_mismatch", `The SHA-256 checksum does not match. ${path} was not changed.`);
|
|
280
|
+
try {
|
|
281
|
+
await rename(part, path);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
const failure = fsError(error);
|
|
285
|
+
return life.abort(failure.code, failure.message);
|
|
286
|
+
}
|
|
287
|
+
send({ type: "transfer", kind: "done", size, sha256 });
|
|
288
|
+
life.finish({ size, sha256 });
|
|
289
|
+
}
|
|
290
|
+
life.touch();
|
|
291
|
+
return {
|
|
292
|
+
result: life.result,
|
|
293
|
+
abort: life.abort,
|
|
294
|
+
expect(size) {
|
|
295
|
+
expected = size;
|
|
296
|
+
},
|
|
297
|
+
receive(message, body) {
|
|
298
|
+
if (life.settled)
|
|
299
|
+
return;
|
|
300
|
+
if (message.kind === "data") {
|
|
301
|
+
if (ending || message.offset !== received)
|
|
302
|
+
return life.abort("out_of_order", "Transfer data arrived out of order.");
|
|
303
|
+
const length = body.byteLength;
|
|
304
|
+
if (expected !== undefined && received + length > expected)
|
|
305
|
+
return life.abort("size_mismatch", `Received more than ${expected} bytes.`);
|
|
306
|
+
received += length;
|
|
307
|
+
hash.update(body);
|
|
308
|
+
life.touch();
|
|
309
|
+
stream.write(body, (error) => {
|
|
310
|
+
if (error || life.settled)
|
|
311
|
+
return;
|
|
312
|
+
written += length;
|
|
313
|
+
options.onProgress?.(written);
|
|
314
|
+
if (!send({ type: "transfer", kind: "ack", offset: written }))
|
|
315
|
+
life.abort("connection_lost", "The device connection was lost. Run the copy again.", false);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
else if (message.kind === "end") {
|
|
319
|
+
if (ending)
|
|
320
|
+
return;
|
|
321
|
+
ending = true;
|
|
322
|
+
life.touch();
|
|
323
|
+
finish(message.size, message.sha256).catch((error) => life.abort("write_failed", error instanceof Error ? error.message : "Write failed."));
|
|
324
|
+
}
|
|
325
|
+
else if (message.kind === "abort")
|
|
326
|
+
life.abort(message.code, message.message, false);
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function parseOpen(args) {
|
|
331
|
+
if (!isRecord(args) ||
|
|
332
|
+
(args.direction !== "push" && args.direction !== "pull") ||
|
|
333
|
+
typeof args.path !== "string" ||
|
|
334
|
+
!args.path ||
|
|
335
|
+
args.path.includes("\0") ||
|
|
336
|
+
(args.direction === "push" && !isSize(args.size)))
|
|
337
|
+
return null;
|
|
338
|
+
return {
|
|
339
|
+
direction: args.direction,
|
|
340
|
+
path: args.path,
|
|
341
|
+
...(args.direction === "push" ? { size: args.size } : {}),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export class TransferHost {
|
|
345
|
+
options;
|
|
346
|
+
transfers = new Map();
|
|
347
|
+
constructor(options = {}) {
|
|
348
|
+
this.options = options;
|
|
349
|
+
}
|
|
350
|
+
async open(id, peer, args, send) {
|
|
351
|
+
if (this.transfers.has(id))
|
|
352
|
+
return;
|
|
353
|
+
const request = parseOpen(args);
|
|
354
|
+
if (!request) {
|
|
355
|
+
send(actionError("invalid_args", "transfer.open requires direction push or pull, a path, and a size for push."));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (this.transfers.size >= (this.options.limit ?? MAX_HOST_TRANSFERS)) {
|
|
359
|
+
send(actionError("busy", "The computer is already running its maximum number of transfers."));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const entry = { peer, canceled: false };
|
|
363
|
+
this.transfers.set(id, entry);
|
|
364
|
+
try {
|
|
365
|
+
if (request.direction === "push") {
|
|
366
|
+
const destination = await openDestination(request.path);
|
|
367
|
+
if (entry.canceled)
|
|
368
|
+
return void (await destination.cancel());
|
|
369
|
+
entry.endpoint = destination.start(send, { ...this.options, size: request.size });
|
|
370
|
+
send({ type: "response", ok: true, result: {} });
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const source = await openSource(request.path);
|
|
374
|
+
if (entry.canceled)
|
|
375
|
+
return void (await source.close());
|
|
376
|
+
send({
|
|
377
|
+
type: "response",
|
|
378
|
+
ok: true,
|
|
379
|
+
result: { size: source.size },
|
|
380
|
+
});
|
|
381
|
+
entry.endpoint = source.start(send, this.options);
|
|
382
|
+
}
|
|
383
|
+
await entry.endpoint.result.catch(() => { });
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
if (!entry.canceled)
|
|
387
|
+
send(error instanceof TransferError
|
|
388
|
+
? actionError(error.code, error.message)
|
|
389
|
+
: actionError("io_error", "The transfer failed."));
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
this.transfers.delete(id);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
receive(id, peer, message, body) {
|
|
396
|
+
const entry = this.transfers.get(id);
|
|
397
|
+
if (!entry || entry.peer !== peer)
|
|
398
|
+
return;
|
|
399
|
+
if (entry.endpoint)
|
|
400
|
+
entry.endpoint.receive(message, body);
|
|
401
|
+
else if (message.kind === "abort")
|
|
402
|
+
entry.canceled = true;
|
|
403
|
+
}
|
|
404
|
+
abortAll() {
|
|
405
|
+
for (const entry of this.transfers.values()) {
|
|
406
|
+
entry.canceled = true;
|
|
407
|
+
entry.endpoint?.abort("connection_lost", "The host connection was lost.", false);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
export async function copyWithDevice(openChannel, request, options = {}) {
|
|
412
|
+
const started = Date.now();
|
|
413
|
+
const { signal, onProgress, ...transfer } = options;
|
|
414
|
+
const canceled = () => new TransferError("canceled", "The copy was canceled.");
|
|
415
|
+
if (signal?.aborted)
|
|
416
|
+
throw canceled();
|
|
417
|
+
const idleMs = transfer.idleMs ?? TRANSFER_IDLE_MS;
|
|
418
|
+
const source = request.direction === "push" ? await openSource(request.localPath) : undefined;
|
|
419
|
+
let destination;
|
|
420
|
+
try {
|
|
421
|
+
if (request.direction === "pull")
|
|
422
|
+
destination = await openDestination(request.localPath);
|
|
423
|
+
if (signal?.aborted)
|
|
424
|
+
throw canceled();
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
await source?.close();
|
|
428
|
+
await destination?.cancel();
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
let total = source?.size ?? 0;
|
|
432
|
+
let endpoint;
|
|
433
|
+
let receiver;
|
|
434
|
+
let channel;
|
|
435
|
+
let opened = false;
|
|
436
|
+
let settle;
|
|
437
|
+
const outcome = new Promise((resolve) => {
|
|
438
|
+
settle = resolve;
|
|
439
|
+
});
|
|
440
|
+
const attach = (next) => {
|
|
441
|
+
endpoint = next;
|
|
442
|
+
next.result.then(settle, (error) => settle(error instanceof TransferError ? error : new TransferError("io_error", String(error))));
|
|
443
|
+
};
|
|
444
|
+
const fail = (error, notify) => {
|
|
445
|
+
if (endpoint)
|
|
446
|
+
return endpoint.abort(error.code, error.message, notify);
|
|
447
|
+
if (notify)
|
|
448
|
+
channel?.send({ type: "transfer", kind: "abort", code: error.code, message: error.message });
|
|
449
|
+
void Promise.resolve(source?.close() ?? destination?.cancel()).finally(() => settle(error));
|
|
450
|
+
};
|
|
451
|
+
const progress = (bytes) => onProgress?.({ bytes, total });
|
|
452
|
+
const opening = source
|
|
453
|
+
? setTimeout(() => fail(new TransferError("timeout", "The device did not answer the copy."), true), idleMs)
|
|
454
|
+
: undefined;
|
|
455
|
+
const result = await openChannel({
|
|
456
|
+
frame(meta, body) {
|
|
457
|
+
const response = parseResponse(meta);
|
|
458
|
+
if (response) {
|
|
459
|
+
if (!response.ok)
|
|
460
|
+
return fail(new TransferError(response.error.code, response.error.message), false);
|
|
461
|
+
if (opened || !channel)
|
|
462
|
+
return;
|
|
463
|
+
opened = true;
|
|
464
|
+
clearTimeout(opening);
|
|
465
|
+
if (source)
|
|
466
|
+
return attach(source.start(channel.send, { ...transfer, onProgress: progress }));
|
|
467
|
+
const size = isRecord(response.result) ? response.result.size : undefined;
|
|
468
|
+
if (!isSize(size))
|
|
469
|
+
return fail(new TransferError("invalid_response", "The device sent no file size."), true);
|
|
470
|
+
total = size;
|
|
471
|
+
receiver?.expect(size);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const message = parseTransferMessage(meta);
|
|
475
|
+
if (message && endpoint)
|
|
476
|
+
endpoint.receive(message, body);
|
|
477
|
+
else if (message?.kind === "abort")
|
|
478
|
+
fail(new TransferError(message.code, message.message), false);
|
|
479
|
+
},
|
|
480
|
+
lost() {
|
|
481
|
+
fail(new TransferError("connection_lost", "The device connection was lost. Run the copy again."), false);
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
if (!("send" in result)) {
|
|
485
|
+
clearTimeout(opening);
|
|
486
|
+
await (source?.close() ?? destination?.cancel());
|
|
487
|
+
throw result.ok
|
|
488
|
+
? new TransferError("invalid_response", "The device channel could not be opened.")
|
|
489
|
+
: new TransferError(result.error.code, result.error.message);
|
|
490
|
+
}
|
|
491
|
+
channel = result;
|
|
492
|
+
if (signal?.aborted) {
|
|
493
|
+
clearTimeout(opening);
|
|
494
|
+
channel.close();
|
|
495
|
+
await (source?.close() ?? destination?.cancel());
|
|
496
|
+
throw canceled();
|
|
497
|
+
}
|
|
498
|
+
if (destination) {
|
|
499
|
+
receiver = destination.start(channel.send, { ...transfer, onProgress: progress });
|
|
500
|
+
attach(receiver);
|
|
501
|
+
}
|
|
502
|
+
const cancel = () => fail(canceled(), true);
|
|
503
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
504
|
+
const args = {
|
|
505
|
+
direction: request.direction,
|
|
506
|
+
path: request.remotePath,
|
|
507
|
+
...(source ? { size: source.size } : {}),
|
|
508
|
+
};
|
|
509
|
+
if (!channel.send({ type: "request", action: "transfer.open", args }))
|
|
510
|
+
fail(new TransferError("connection_lost", "The copy could not be sent to the device."), false);
|
|
511
|
+
const value = await outcome;
|
|
512
|
+
clearTimeout(opening);
|
|
513
|
+
signal?.removeEventListener("abort", cancel);
|
|
514
|
+
channel.close();
|
|
515
|
+
if (value instanceof TransferError)
|
|
516
|
+
throw value;
|
|
517
|
+
return { bytes: value.size, sha256: value.sha256, ms: Date.now() - started };
|
|
518
|
+
}
|