@dropgate/core 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -128
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +2814 -1436
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +424 -283
- package/dist/index.d.ts +424 -283
- package/dist/index.js +2816 -1389
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +495 -190
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +366 -5
- package/dist/p2p/index.d.ts +366 -5
- package/dist/p2p/index.js +497 -178
- package/dist/p2p/index.js.map +1 -1
- package/package.json +15 -12
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
1
5
|
// src/constants.ts
|
|
2
6
|
var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
3
7
|
var AES_GCM_IV_BYTES = 12;
|
|
@@ -8,18 +12,12 @@ var MAX_IN_MEMORY_DOWNLOAD_BYTES = 100 * 1024 * 1024;
|
|
|
8
12
|
// src/errors.ts
|
|
9
13
|
var DropgateError = class extends Error {
|
|
10
14
|
constructor(message, opts = {}) {
|
|
11
|
-
super(message);
|
|
15
|
+
super(message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
16
|
+
__publicField(this, "code");
|
|
17
|
+
__publicField(this, "details");
|
|
12
18
|
this.name = this.constructor.name;
|
|
13
19
|
this.code = opts.code || "DROPGATE_ERROR";
|
|
14
20
|
this.details = opts.details;
|
|
15
|
-
if (opts.cause !== void 0) {
|
|
16
|
-
Object.defineProperty(this, "cause", {
|
|
17
|
-
value: opts.cause,
|
|
18
|
-
writable: false,
|
|
19
|
-
enumerable: false,
|
|
20
|
-
configurable: true
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
21
|
}
|
|
24
22
|
};
|
|
25
23
|
var DropgateValidationError = class extends DropgateError {
|
|
@@ -235,6 +233,144 @@ async function fetchJson(fetchFn, url, opts = {}) {
|
|
|
235
233
|
}
|
|
236
234
|
}
|
|
237
235
|
|
|
236
|
+
// src/crypto/sha256-fallback.ts
|
|
237
|
+
var K = new Uint32Array([
|
|
238
|
+
1116352408,
|
|
239
|
+
1899447441,
|
|
240
|
+
3049323471,
|
|
241
|
+
3921009573,
|
|
242
|
+
961987163,
|
|
243
|
+
1508970993,
|
|
244
|
+
2453635748,
|
|
245
|
+
2870763221,
|
|
246
|
+
3624381080,
|
|
247
|
+
310598401,
|
|
248
|
+
607225278,
|
|
249
|
+
1426881987,
|
|
250
|
+
1925078388,
|
|
251
|
+
2162078206,
|
|
252
|
+
2614888103,
|
|
253
|
+
3248222580,
|
|
254
|
+
3835390401,
|
|
255
|
+
4022224774,
|
|
256
|
+
264347078,
|
|
257
|
+
604807628,
|
|
258
|
+
770255983,
|
|
259
|
+
1249150122,
|
|
260
|
+
1555081692,
|
|
261
|
+
1996064986,
|
|
262
|
+
2554220882,
|
|
263
|
+
2821834349,
|
|
264
|
+
2952996808,
|
|
265
|
+
3210313671,
|
|
266
|
+
3336571891,
|
|
267
|
+
3584528711,
|
|
268
|
+
113926993,
|
|
269
|
+
338241895,
|
|
270
|
+
666307205,
|
|
271
|
+
773529912,
|
|
272
|
+
1294757372,
|
|
273
|
+
1396182291,
|
|
274
|
+
1695183700,
|
|
275
|
+
1986661051,
|
|
276
|
+
2177026350,
|
|
277
|
+
2456956037,
|
|
278
|
+
2730485921,
|
|
279
|
+
2820302411,
|
|
280
|
+
3259730800,
|
|
281
|
+
3345764771,
|
|
282
|
+
3516065817,
|
|
283
|
+
3600352804,
|
|
284
|
+
4094571909,
|
|
285
|
+
275423344,
|
|
286
|
+
430227734,
|
|
287
|
+
506948616,
|
|
288
|
+
659060556,
|
|
289
|
+
883997877,
|
|
290
|
+
958139571,
|
|
291
|
+
1322822218,
|
|
292
|
+
1537002063,
|
|
293
|
+
1747873779,
|
|
294
|
+
1955562222,
|
|
295
|
+
2024104815,
|
|
296
|
+
2227730452,
|
|
297
|
+
2361852424,
|
|
298
|
+
2428436474,
|
|
299
|
+
2756734187,
|
|
300
|
+
3204031479,
|
|
301
|
+
3329325298
|
|
302
|
+
]);
|
|
303
|
+
function rotr(x, n) {
|
|
304
|
+
return x >>> n | x << 32 - n;
|
|
305
|
+
}
|
|
306
|
+
function sha256Fallback(data) {
|
|
307
|
+
const bytes = new Uint8Array(data);
|
|
308
|
+
const bitLen = bytes.length * 8;
|
|
309
|
+
const padded = new Uint8Array(
|
|
310
|
+
Math.ceil((bytes.length + 9) / 64) * 64
|
|
311
|
+
);
|
|
312
|
+
padded.set(bytes);
|
|
313
|
+
padded[bytes.length] = 128;
|
|
314
|
+
const view = new DataView(padded.buffer);
|
|
315
|
+
view.setUint32(padded.length - 8, bitLen / 4294967296 >>> 0, false);
|
|
316
|
+
view.setUint32(padded.length - 4, bitLen >>> 0, false);
|
|
317
|
+
let h0 = 1779033703;
|
|
318
|
+
let h1 = 3144134277;
|
|
319
|
+
let h2 = 1013904242;
|
|
320
|
+
let h3 = 2773480762;
|
|
321
|
+
let h4 = 1359893119;
|
|
322
|
+
let h5 = 2600822924;
|
|
323
|
+
let h6 = 528734635;
|
|
324
|
+
let h7 = 1541459225;
|
|
325
|
+
const W = new Uint32Array(64);
|
|
326
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
327
|
+
for (let i = 0; i < 16; i++) {
|
|
328
|
+
W[i] = view.getUint32(offset + i * 4, false);
|
|
329
|
+
}
|
|
330
|
+
for (let i = 16; i < 64; i++) {
|
|
331
|
+
const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ W[i - 15] >>> 3;
|
|
332
|
+
const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ W[i - 2] >>> 10;
|
|
333
|
+
W[i] = W[i - 16] + s0 + W[i - 7] + s1 | 0;
|
|
334
|
+
}
|
|
335
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
|
336
|
+
for (let i = 0; i < 64; i++) {
|
|
337
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
338
|
+
const ch = e & f ^ ~e & g;
|
|
339
|
+
const temp1 = h + S1 + ch + K[i] + W[i] | 0;
|
|
340
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
341
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
342
|
+
const temp2 = S0 + maj | 0;
|
|
343
|
+
h = g;
|
|
344
|
+
g = f;
|
|
345
|
+
f = e;
|
|
346
|
+
e = d + temp1 | 0;
|
|
347
|
+
d = c;
|
|
348
|
+
c = b;
|
|
349
|
+
b = a;
|
|
350
|
+
a = temp1 + temp2 | 0;
|
|
351
|
+
}
|
|
352
|
+
h0 = h0 + a | 0;
|
|
353
|
+
h1 = h1 + b | 0;
|
|
354
|
+
h2 = h2 + c | 0;
|
|
355
|
+
h3 = h3 + d | 0;
|
|
356
|
+
h4 = h4 + e | 0;
|
|
357
|
+
h5 = h5 + f | 0;
|
|
358
|
+
h6 = h6 + g | 0;
|
|
359
|
+
h7 = h7 + h | 0;
|
|
360
|
+
}
|
|
361
|
+
const result = new ArrayBuffer(32);
|
|
362
|
+
const out = new DataView(result);
|
|
363
|
+
out.setUint32(0, h0, false);
|
|
364
|
+
out.setUint32(4, h1, false);
|
|
365
|
+
out.setUint32(8, h2, false);
|
|
366
|
+
out.setUint32(12, h3, false);
|
|
367
|
+
out.setUint32(16, h4, false);
|
|
368
|
+
out.setUint32(20, h5, false);
|
|
369
|
+
out.setUint32(24, h6, false);
|
|
370
|
+
out.setUint32(28, h7, false);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
238
374
|
// src/crypto/decrypt.ts
|
|
239
375
|
async function importKeyFromBase64(cryptoObj, keyB64, base64) {
|
|
240
376
|
const adapter = base64 || getDefaultBase64();
|
|
@@ -265,8 +401,7 @@ async function decryptFilenameFromBase64(cryptoObj, encryptedFilenameB64, key, b
|
|
|
265
401
|
}
|
|
266
402
|
|
|
267
403
|
// src/crypto/index.ts
|
|
268
|
-
|
|
269
|
-
const hashBuffer = await cryptoObj.subtle.digest("SHA-256", data);
|
|
404
|
+
function digestToHex(hashBuffer) {
|
|
270
405
|
const arr = new Uint8Array(hashBuffer);
|
|
271
406
|
let hex = "";
|
|
272
407
|
for (let i = 0; i < arr.length; i++) {
|
|
@@ -274,6 +409,13 @@ async function sha256Hex(cryptoObj, data) {
|
|
|
274
409
|
}
|
|
275
410
|
return hex;
|
|
276
411
|
}
|
|
412
|
+
async function sha256Hex(cryptoObj, data) {
|
|
413
|
+
if (cryptoObj?.subtle) {
|
|
414
|
+
const hashBuffer = await cryptoObj.subtle.digest("SHA-256", data);
|
|
415
|
+
return digestToHex(hashBuffer);
|
|
416
|
+
}
|
|
417
|
+
return digestToHex(sha256Fallback(data));
|
|
418
|
+
}
|
|
277
419
|
async function generateAesGcmKey(cryptoObj) {
|
|
278
420
|
return cryptoObj.subtle.generateKey(
|
|
279
421
|
{ name: "AES-GCM", length: 256 },
|
|
@@ -303,1501 +445,2790 @@ async function encryptFilenameToBase64(cryptoObj, filename, key) {
|
|
|
303
445
|
return arrayBufferToBase64(buf);
|
|
304
446
|
}
|
|
305
447
|
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
448
|
+
// node_modules/fflate/esm/browser.js
|
|
449
|
+
var u8 = Uint8Array;
|
|
450
|
+
var u16 = Uint16Array;
|
|
451
|
+
var i32 = Int32Array;
|
|
452
|
+
var fleb = new u8([
|
|
453
|
+
0,
|
|
454
|
+
0,
|
|
455
|
+
0,
|
|
456
|
+
0,
|
|
457
|
+
0,
|
|
458
|
+
0,
|
|
459
|
+
0,
|
|
460
|
+
0,
|
|
461
|
+
1,
|
|
462
|
+
1,
|
|
463
|
+
1,
|
|
464
|
+
1,
|
|
465
|
+
2,
|
|
466
|
+
2,
|
|
467
|
+
2,
|
|
468
|
+
2,
|
|
469
|
+
3,
|
|
470
|
+
3,
|
|
471
|
+
3,
|
|
472
|
+
3,
|
|
473
|
+
4,
|
|
474
|
+
4,
|
|
475
|
+
4,
|
|
476
|
+
4,
|
|
477
|
+
5,
|
|
478
|
+
5,
|
|
479
|
+
5,
|
|
480
|
+
5,
|
|
481
|
+
0,
|
|
482
|
+
/* unused */
|
|
483
|
+
0,
|
|
484
|
+
0,
|
|
485
|
+
/* impossible */
|
|
486
|
+
0
|
|
487
|
+
]);
|
|
488
|
+
var fdeb = new u8([
|
|
489
|
+
0,
|
|
490
|
+
0,
|
|
491
|
+
0,
|
|
492
|
+
0,
|
|
493
|
+
1,
|
|
494
|
+
1,
|
|
495
|
+
2,
|
|
496
|
+
2,
|
|
497
|
+
3,
|
|
498
|
+
3,
|
|
499
|
+
4,
|
|
500
|
+
4,
|
|
501
|
+
5,
|
|
502
|
+
5,
|
|
503
|
+
6,
|
|
504
|
+
6,
|
|
505
|
+
7,
|
|
506
|
+
7,
|
|
507
|
+
8,
|
|
508
|
+
8,
|
|
509
|
+
9,
|
|
510
|
+
9,
|
|
511
|
+
10,
|
|
512
|
+
10,
|
|
513
|
+
11,
|
|
514
|
+
11,
|
|
515
|
+
12,
|
|
516
|
+
12,
|
|
517
|
+
13,
|
|
518
|
+
13,
|
|
519
|
+
/* unused */
|
|
520
|
+
0,
|
|
521
|
+
0
|
|
522
|
+
]);
|
|
523
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
524
|
+
var freb = function(eb, start) {
|
|
525
|
+
var b = new u16(31);
|
|
526
|
+
for (var i = 0; i < 31; ++i) {
|
|
527
|
+
b[i] = start += 1 << eb[i - 1];
|
|
317
528
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
`${baseUrl}/api/info`,
|
|
323
|
-
{
|
|
324
|
-
method: "GET",
|
|
325
|
-
timeoutMs,
|
|
326
|
-
signal,
|
|
327
|
-
headers: { Accept: "application/json" }
|
|
328
|
-
}
|
|
329
|
-
);
|
|
330
|
-
if (res.ok && json && typeof json === "object" && "version" in json) {
|
|
331
|
-
return { baseUrl, serverInfo: json };
|
|
529
|
+
var r = new i32(b[30]);
|
|
530
|
+
for (var i = 1; i < 30; ++i) {
|
|
531
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
532
|
+
r[j] = j - b[i] << 5 | i;
|
|
332
533
|
}
|
|
333
|
-
throw new DropgateProtocolError(
|
|
334
|
-
`Server info request failed (status ${res.status}).`
|
|
335
|
-
);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
if (err instanceof DropgateError) throw err;
|
|
338
|
-
throw new DropgateNetworkError("Could not reach server /api/info.", {
|
|
339
|
-
cause: err
|
|
340
|
-
});
|
|
341
534
|
}
|
|
535
|
+
return { b, r };
|
|
536
|
+
};
|
|
537
|
+
var _a = freb(fleb, 2);
|
|
538
|
+
var fl = _a.b;
|
|
539
|
+
var revfl = _a.r;
|
|
540
|
+
fl[28] = 258, revfl[258] = 28;
|
|
541
|
+
var _b = freb(fdeb, 0);
|
|
542
|
+
var fd = _b.b;
|
|
543
|
+
var revfd = _b.r;
|
|
544
|
+
var rev = new u16(32768);
|
|
545
|
+
for (i = 0; i < 32768; ++i) {
|
|
546
|
+
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
547
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
548
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
549
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
342
550
|
}
|
|
343
|
-
var
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
551
|
+
var x;
|
|
552
|
+
var i;
|
|
553
|
+
var flt = new u8(288);
|
|
554
|
+
for (i = 0; i < 144; ++i)
|
|
555
|
+
flt[i] = 8;
|
|
556
|
+
var i;
|
|
557
|
+
for (i = 144; i < 256; ++i)
|
|
558
|
+
flt[i] = 9;
|
|
559
|
+
var i;
|
|
560
|
+
for (i = 256; i < 280; ++i)
|
|
561
|
+
flt[i] = 7;
|
|
562
|
+
var i;
|
|
563
|
+
for (i = 280; i < 288; ++i)
|
|
564
|
+
flt[i] = 8;
|
|
565
|
+
var i;
|
|
566
|
+
var fdt = new u8(32);
|
|
567
|
+
for (i = 0; i < 32; ++i)
|
|
568
|
+
fdt[i] = 5;
|
|
569
|
+
var i;
|
|
570
|
+
var slc = function(v, s, e) {
|
|
571
|
+
if (s == null || s < 0)
|
|
572
|
+
s = 0;
|
|
573
|
+
if (e == null || e > v.length)
|
|
574
|
+
e = v.length;
|
|
575
|
+
return new u8(v.subarray(s, e));
|
|
576
|
+
};
|
|
577
|
+
var ec = [
|
|
578
|
+
"unexpected EOF",
|
|
579
|
+
"invalid block type",
|
|
580
|
+
"invalid length/literal",
|
|
581
|
+
"invalid distance",
|
|
582
|
+
"stream finished",
|
|
583
|
+
"no stream handler",
|
|
584
|
+
,
|
|
585
|
+
"no callback",
|
|
586
|
+
"invalid UTF-8 data",
|
|
587
|
+
"extra field too long",
|
|
588
|
+
"date not in range 1980-2099",
|
|
589
|
+
"filename too long",
|
|
590
|
+
"stream finishing",
|
|
591
|
+
"invalid zip data"
|
|
592
|
+
// determined by unknown compression method
|
|
593
|
+
];
|
|
594
|
+
var err = function(ind, msg, nt) {
|
|
595
|
+
var e = new Error(msg || ec[ind]);
|
|
596
|
+
e.code = ind;
|
|
597
|
+
if (Error.captureStackTrace)
|
|
598
|
+
Error.captureStackTrace(e, err);
|
|
599
|
+
if (!nt)
|
|
600
|
+
throw e;
|
|
601
|
+
return e;
|
|
602
|
+
};
|
|
603
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
604
|
+
var crct = /* @__PURE__ */ (function() {
|
|
605
|
+
var t = new Int32Array(256);
|
|
606
|
+
for (var i = 0; i < 256; ++i) {
|
|
607
|
+
var c = i, k = 9;
|
|
608
|
+
while (--k)
|
|
609
|
+
c = (c & 1 && -306674912) ^ c >>> 1;
|
|
610
|
+
t[i] = c;
|
|
611
|
+
}
|
|
612
|
+
return t;
|
|
613
|
+
})();
|
|
614
|
+
var crc = function() {
|
|
615
|
+
var c = -1;
|
|
616
|
+
return {
|
|
617
|
+
p: function(d) {
|
|
618
|
+
var cr = c;
|
|
619
|
+
for (var i = 0; i < d.length; ++i)
|
|
620
|
+
cr = crct[cr & 255 ^ d[i]] ^ cr >>> 8;
|
|
621
|
+
c = cr;
|
|
622
|
+
},
|
|
623
|
+
d: function() {
|
|
624
|
+
return ~c;
|
|
354
625
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
626
|
+
};
|
|
627
|
+
};
|
|
628
|
+
var mrg = function(a, b) {
|
|
629
|
+
var o = {};
|
|
630
|
+
for (var k in a)
|
|
631
|
+
o[k] = a[k];
|
|
632
|
+
for (var k in b)
|
|
633
|
+
o[k] = b[k];
|
|
634
|
+
return o;
|
|
635
|
+
};
|
|
636
|
+
var wbytes = function(d, b, v) {
|
|
637
|
+
for (; v; ++b)
|
|
638
|
+
d[b] = v, v >>>= 8;
|
|
639
|
+
};
|
|
640
|
+
var te = typeof TextEncoder != "undefined" && /* @__PURE__ */ new TextEncoder();
|
|
641
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
642
|
+
var tds = 0;
|
|
643
|
+
try {
|
|
644
|
+
td.decode(et, { stream: true });
|
|
645
|
+
tds = 1;
|
|
646
|
+
} catch (e) {
|
|
647
|
+
}
|
|
648
|
+
function strToU8(str, latin1) {
|
|
649
|
+
if (latin1) {
|
|
650
|
+
var ar_1 = new u8(str.length);
|
|
651
|
+
for (var i = 0; i < str.length; ++i)
|
|
652
|
+
ar_1[i] = str.charCodeAt(i);
|
|
653
|
+
return ar_1;
|
|
654
|
+
}
|
|
655
|
+
if (te)
|
|
656
|
+
return te.encode(str);
|
|
657
|
+
var l = str.length;
|
|
658
|
+
var ar = new u8(str.length + (str.length >> 1));
|
|
659
|
+
var ai = 0;
|
|
660
|
+
var w = function(v) {
|
|
661
|
+
ar[ai++] = v;
|
|
662
|
+
};
|
|
663
|
+
for (var i = 0; i < l; ++i) {
|
|
664
|
+
if (ai + 5 > ar.length) {
|
|
665
|
+
var n = new u8(ai + 8 + (l - i << 1));
|
|
666
|
+
n.set(ar);
|
|
667
|
+
ar = n;
|
|
360
668
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
669
|
+
var c = str.charCodeAt(i);
|
|
670
|
+
if (c < 128 || latin1)
|
|
671
|
+
w(c);
|
|
672
|
+
else if (c < 2048)
|
|
673
|
+
w(192 | c >> 6), w(128 | c & 63);
|
|
674
|
+
else if (c > 55295 && c < 57344)
|
|
675
|
+
c = 65536 + (c & 1023 << 10) | str.charCodeAt(++i) & 1023, w(240 | c >> 18), w(128 | c >> 12 & 63), w(128 | c >> 6 & 63), w(128 | c & 63);
|
|
676
|
+
else
|
|
677
|
+
w(224 | c >> 12), w(128 | c >> 6 & 63), w(128 | c & 63);
|
|
678
|
+
}
|
|
679
|
+
return slc(ar, 0, ai);
|
|
680
|
+
}
|
|
681
|
+
var exfl = function(ex) {
|
|
682
|
+
var le = 0;
|
|
683
|
+
if (ex) {
|
|
684
|
+
for (var k in ex) {
|
|
685
|
+
var l = ex[k].length;
|
|
686
|
+
if (l > 65535)
|
|
687
|
+
err(9);
|
|
688
|
+
le += l + 4;
|
|
365
689
|
}
|
|
366
|
-
this.cryptoObj = cryptoObj;
|
|
367
|
-
this.base64 = opts.base64 || getDefaultBase64();
|
|
368
|
-
this.logger = opts.logger || null;
|
|
369
690
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
691
|
+
return le;
|
|
692
|
+
};
|
|
693
|
+
var wzh = function(d, b, f, fn, u, c, ce, co) {
|
|
694
|
+
var fl2 = fn.length, ex = f.extra, col = co && co.length;
|
|
695
|
+
var exl = exfl(ex);
|
|
696
|
+
wbytes(d, b, ce != null ? 33639248 : 67324752), b += 4;
|
|
697
|
+
if (ce != null)
|
|
698
|
+
d[b++] = 20, d[b++] = f.os;
|
|
699
|
+
d[b] = 20, b += 2;
|
|
700
|
+
d[b++] = f.flag << 1 | (c < 0 && 8), d[b++] = u && 8;
|
|
701
|
+
d[b++] = f.compression & 255, d[b++] = f.compression >> 8;
|
|
702
|
+
var dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980;
|
|
703
|
+
if (y < 0 || y > 119)
|
|
704
|
+
err(10);
|
|
705
|
+
wbytes(d, b, y << 25 | dt.getMonth() + 1 << 21 | dt.getDate() << 16 | dt.getHours() << 11 | dt.getMinutes() << 5 | dt.getSeconds() >> 1), b += 4;
|
|
706
|
+
if (c != -1) {
|
|
707
|
+
wbytes(d, b, f.crc);
|
|
708
|
+
wbytes(d, b + 4, c < 0 ? -c - 2 : c);
|
|
709
|
+
wbytes(d, b + 8, f.size);
|
|
710
|
+
}
|
|
711
|
+
wbytes(d, b + 12, fl2);
|
|
712
|
+
wbytes(d, b + 14, exl), b += 16;
|
|
713
|
+
if (ce != null) {
|
|
714
|
+
wbytes(d, b, col);
|
|
715
|
+
wbytes(d, b + 6, f.attrs);
|
|
716
|
+
wbytes(d, b + 10, ce), b += 14;
|
|
717
|
+
}
|
|
718
|
+
d.set(fn, b);
|
|
719
|
+
b += fl2;
|
|
720
|
+
if (exl) {
|
|
721
|
+
for (var k in ex) {
|
|
722
|
+
var exf = ex[k], l = exf.length;
|
|
723
|
+
wbytes(d, b, +k);
|
|
724
|
+
wbytes(d, b + 2, l);
|
|
725
|
+
d.set(exf, b + 4), b += 4 + l;
|
|
382
726
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
727
|
+
}
|
|
728
|
+
if (col)
|
|
729
|
+
d.set(co, b), b += col;
|
|
730
|
+
return b;
|
|
731
|
+
};
|
|
732
|
+
var wzf = function(o, b, c, d, e) {
|
|
733
|
+
wbytes(o, b, 101010256);
|
|
734
|
+
wbytes(o, b + 8, c);
|
|
735
|
+
wbytes(o, b + 10, c);
|
|
736
|
+
wbytes(o, b + 12, d);
|
|
737
|
+
wbytes(o, b + 16, e);
|
|
738
|
+
};
|
|
739
|
+
var ZipPassThrough = /* @__PURE__ */ (function() {
|
|
740
|
+
function ZipPassThrough2(filename) {
|
|
741
|
+
this.filename = filename;
|
|
742
|
+
this.c = crc();
|
|
743
|
+
this.size = 0;
|
|
744
|
+
this.compression = 0;
|
|
745
|
+
}
|
|
746
|
+
ZipPassThrough2.prototype.process = function(chunk, final) {
|
|
747
|
+
this.ondata(null, chunk, final);
|
|
748
|
+
};
|
|
749
|
+
ZipPassThrough2.prototype.push = function(chunk, final) {
|
|
750
|
+
if (!this.ondata)
|
|
751
|
+
err(5);
|
|
752
|
+
this.c.p(chunk);
|
|
753
|
+
this.size += chunk.length;
|
|
754
|
+
if (final)
|
|
755
|
+
this.crc = this.c.d();
|
|
756
|
+
this.process(chunk, final || false);
|
|
757
|
+
};
|
|
758
|
+
return ZipPassThrough2;
|
|
759
|
+
})();
|
|
760
|
+
var Zip = /* @__PURE__ */ (function() {
|
|
761
|
+
function Zip2(cb) {
|
|
762
|
+
this.ondata = cb;
|
|
763
|
+
this.u = [];
|
|
764
|
+
this.d = 1;
|
|
765
|
+
}
|
|
766
|
+
Zip2.prototype.add = function(file) {
|
|
767
|
+
var _this = this;
|
|
768
|
+
if (!this.ondata)
|
|
769
|
+
err(5);
|
|
770
|
+
if (this.d & 2)
|
|
771
|
+
this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, false);
|
|
772
|
+
else {
|
|
773
|
+
var f = strToU8(file.filename), fl_1 = f.length;
|
|
774
|
+
var com = file.comment, o = com && strToU8(com);
|
|
775
|
+
var u = fl_1 != file.filename.length || o && com.length != o.length;
|
|
776
|
+
var hl_1 = fl_1 + exfl(file.extra) + 30;
|
|
777
|
+
if (fl_1 > 65535)
|
|
778
|
+
this.ondata(err(11, 0, 1), null, false);
|
|
779
|
+
var header = new u8(hl_1);
|
|
780
|
+
wzh(header, 0, file, f, u, -1);
|
|
781
|
+
var chks_1 = [header];
|
|
782
|
+
var pAll_1 = function() {
|
|
783
|
+
for (var _i = 0, chks_2 = chks_1; _i < chks_2.length; _i++) {
|
|
784
|
+
var chk = chks_2[_i];
|
|
785
|
+
_this.ondata(null, chk, false);
|
|
786
|
+
}
|
|
787
|
+
chks_1 = [];
|
|
788
|
+
};
|
|
789
|
+
var tr_1 = this.d;
|
|
790
|
+
this.d = 0;
|
|
791
|
+
var ind_1 = this.u.length;
|
|
792
|
+
var uf_1 = mrg(file, {
|
|
793
|
+
f,
|
|
794
|
+
u,
|
|
795
|
+
o,
|
|
796
|
+
t: function() {
|
|
797
|
+
if (file.terminate)
|
|
798
|
+
file.terminate();
|
|
394
799
|
},
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
800
|
+
r: function() {
|
|
801
|
+
pAll_1();
|
|
802
|
+
if (tr_1) {
|
|
803
|
+
var nxt = _this.u[ind_1 + 1];
|
|
804
|
+
if (nxt)
|
|
805
|
+
nxt.r();
|
|
806
|
+
else
|
|
807
|
+
_this.d = 1;
|
|
808
|
+
}
|
|
809
|
+
tr_1 = 1;
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
var cl_1 = 0;
|
|
813
|
+
file.ondata = function(err2, dat, final) {
|
|
814
|
+
if (err2) {
|
|
815
|
+
_this.ondata(err2, dat, final);
|
|
816
|
+
_this.terminate();
|
|
817
|
+
} else {
|
|
818
|
+
cl_1 += dat.length;
|
|
819
|
+
chks_1.push(dat);
|
|
820
|
+
if (final) {
|
|
821
|
+
var dd = new u8(16);
|
|
822
|
+
wbytes(dd, 0, 134695760);
|
|
823
|
+
wbytes(dd, 4, file.crc);
|
|
824
|
+
wbytes(dd, 8, cl_1);
|
|
825
|
+
wbytes(dd, 12, file.size);
|
|
826
|
+
chks_1.push(dd);
|
|
827
|
+
uf_1.c = cl_1, uf_1.b = hl_1 + cl_1 + 16, uf_1.crc = file.crc, uf_1.size = file.size;
|
|
828
|
+
if (tr_1)
|
|
829
|
+
uf_1.r();
|
|
830
|
+
tr_1 = 1;
|
|
831
|
+
} else if (tr_1)
|
|
832
|
+
pAll_1();
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
this.u.push(uf_1);
|
|
401
836
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
throw new DropgateNetworkError("Could not connect to the server.", {
|
|
422
|
-
cause: err
|
|
837
|
+
};
|
|
838
|
+
Zip2.prototype.end = function() {
|
|
839
|
+
var _this = this;
|
|
840
|
+
if (this.d & 2) {
|
|
841
|
+
this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, true);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (this.d)
|
|
845
|
+
this.e();
|
|
846
|
+
else
|
|
847
|
+
this.u.push({
|
|
848
|
+
r: function() {
|
|
849
|
+
if (!(_this.d & 1))
|
|
850
|
+
return;
|
|
851
|
+
_this.u.splice(-1, 1);
|
|
852
|
+
_this.e();
|
|
853
|
+
},
|
|
854
|
+
t: function() {
|
|
855
|
+
}
|
|
423
856
|
});
|
|
857
|
+
this.d = 3;
|
|
858
|
+
};
|
|
859
|
+
Zip2.prototype.e = function() {
|
|
860
|
+
var bt = 0, l = 0, tl = 0;
|
|
861
|
+
for (var _i = 0, _a2 = this.u; _i < _a2.length; _i++) {
|
|
862
|
+
var f = _a2[_i];
|
|
863
|
+
tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0);
|
|
424
864
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
compatible: false,
|
|
432
|
-
clientVersion,
|
|
433
|
-
serverVersion,
|
|
434
|
-
message: `Incompatible versions. Client v${clientVersion}, Server v${serverVersion}${serverInfo?.name ? ` (${serverInfo.name})` : ""}.`,
|
|
435
|
-
serverInfo,
|
|
436
|
-
baseUrl
|
|
437
|
-
};
|
|
865
|
+
var out = new u8(tl + 22);
|
|
866
|
+
for (var _b2 = 0, _c = this.u; _b2 < _c.length; _b2++) {
|
|
867
|
+
var f = _c[_b2];
|
|
868
|
+
wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o);
|
|
869
|
+
bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b;
|
|
438
870
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
};
|
|
871
|
+
wzf(out, bt, this.u.length, tl, l);
|
|
872
|
+
this.ondata(null, out, true);
|
|
873
|
+
this.d = 2;
|
|
874
|
+
};
|
|
875
|
+
Zip2.prototype.terminate = function() {
|
|
876
|
+
for (var _i = 0, _a2 = this.u; _i < _a2.length; _i++) {
|
|
877
|
+
var f = _a2[_i];
|
|
878
|
+
f.t();
|
|
448
879
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
880
|
+
this.d = 2;
|
|
881
|
+
};
|
|
882
|
+
return Zip2;
|
|
883
|
+
})();
|
|
884
|
+
|
|
885
|
+
// src/zip/stream-zip.ts
|
|
886
|
+
var StreamingZipWriter = class {
|
|
887
|
+
constructor(onData) {
|
|
888
|
+
__publicField(this, "zip");
|
|
889
|
+
__publicField(this, "currentFile", null);
|
|
890
|
+
__publicField(this, "onData");
|
|
891
|
+
__publicField(this, "finalized", false);
|
|
892
|
+
__publicField(this, "pendingWrites", Promise.resolve());
|
|
893
|
+
this.onData = onData;
|
|
894
|
+
this.zip = new Zip((err2, data, _final) => {
|
|
895
|
+
if (err2) throw err2;
|
|
896
|
+
this.pendingWrites = this.pendingWrites.then(() => this.onData(data));
|
|
897
|
+
});
|
|
457
898
|
}
|
|
458
899
|
/**
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* @
|
|
462
|
-
* @throws {DropgateValidationError} If any validation check fails.
|
|
900
|
+
* Begin a new file entry in the ZIP.
|
|
901
|
+
* Must call endFile() before starting another file.
|
|
902
|
+
* @param name - Filename within the ZIP archive.
|
|
463
903
|
*/
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (!caps || !caps.enabled) {
|
|
468
|
-
throw new DropgateValidationError("Server does not support file uploads.");
|
|
469
|
-
}
|
|
470
|
-
const fileSize = Number(file?.size || 0);
|
|
471
|
-
if (!file || !Number.isFinite(fileSize) || fileSize <= 0) {
|
|
472
|
-
throw new DropgateValidationError("File is missing or invalid.");
|
|
473
|
-
}
|
|
474
|
-
const maxMB = Number(caps.maxSizeMB);
|
|
475
|
-
if (Number.isFinite(maxMB) && maxMB > 0) {
|
|
476
|
-
const limitBytes = maxMB * 1e3 * 1e3;
|
|
477
|
-
const totalChunks = Math.ceil(fileSize / this.chunkSize);
|
|
478
|
-
const estimatedBytes = estimateTotalUploadSizeBytes(
|
|
479
|
-
fileSize,
|
|
480
|
-
totalChunks,
|
|
481
|
-
Boolean(encrypt)
|
|
482
|
-
);
|
|
483
|
-
if (estimatedBytes > limitBytes) {
|
|
484
|
-
const msg = encrypt ? `File too large once encryption overhead is included. Server limit: ${maxMB} MB.` : `File too large. Server limit: ${maxMB} MB.`;
|
|
485
|
-
throw new DropgateValidationError(msg);
|
|
486
|
-
}
|
|
904
|
+
startFile(name) {
|
|
905
|
+
if (this.currentFile) {
|
|
906
|
+
throw new Error("Must call endFile() before starting a new file.");
|
|
487
907
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (!Number.isFinite(lt) || lt < 0 || !Number.isInteger(lt)) {
|
|
491
|
-
throw new DropgateValidationError(
|
|
492
|
-
"Invalid lifetime. Must be a non-negative integer (milliseconds)."
|
|
493
|
-
);
|
|
908
|
+
if (this.finalized) {
|
|
909
|
+
throw new Error("ZIP has already been finalized.");
|
|
494
910
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
911
|
+
const entry = new ZipPassThrough(name);
|
|
912
|
+
this.zip.add(entry);
|
|
913
|
+
this.currentFile = entry;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Write a chunk of data to the current file entry.
|
|
917
|
+
* @param data - The data chunk to write.
|
|
918
|
+
*/
|
|
919
|
+
writeChunk(data) {
|
|
920
|
+
if (!this.currentFile) {
|
|
921
|
+
throw new Error("No file started. Call startFile() first.");
|
|
507
922
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
923
|
+
this.currentFile.push(data, false);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* End the current file entry.
|
|
927
|
+
*/
|
|
928
|
+
endFile() {
|
|
929
|
+
if (!this.currentFile) {
|
|
930
|
+
throw new Error("No file to end.");
|
|
512
931
|
}
|
|
513
|
-
|
|
932
|
+
this.currentFile.push(new Uint8Array(0), true);
|
|
933
|
+
this.currentFile = null;
|
|
514
934
|
}
|
|
515
935
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* @returns Upload result containing the download URL and file identifiers.
|
|
519
|
-
* @throws {DropgateValidationError} If input validation fails.
|
|
520
|
-
* @throws {DropgateNetworkError} If the server cannot be reached.
|
|
521
|
-
* @throws {DropgateProtocolError} If the server returns an error.
|
|
522
|
-
* @throws {DropgateAbortError} If the upload is cancelled.
|
|
936
|
+
* Finalize the ZIP archive. Must be called after all files are written.
|
|
937
|
+
* Waits for all pending async writes to complete before resolving.
|
|
523
938
|
*/
|
|
524
|
-
async
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
totalChunks,
|
|
612
|
-
...maxDownloads !== void 0 ? { maxDownloads } : {}
|
|
613
|
-
};
|
|
614
|
-
const initRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init`, {
|
|
615
|
-
method: "POST",
|
|
616
|
-
timeoutMs: timeouts.initMs ?? 15e3,
|
|
617
|
-
signal: effectiveSignal,
|
|
618
|
-
headers: {
|
|
619
|
-
"Content-Type": "application/json",
|
|
620
|
-
Accept: "application/json"
|
|
621
|
-
},
|
|
622
|
-
body: JSON.stringify(initPayload)
|
|
939
|
+
async finalize() {
|
|
940
|
+
if (this.currentFile) {
|
|
941
|
+
throw new Error("Cannot finalize with an open file. Call endFile() first.");
|
|
942
|
+
}
|
|
943
|
+
if (this.finalized) return;
|
|
944
|
+
this.finalized = true;
|
|
945
|
+
this.zip.end();
|
|
946
|
+
await this.pendingWrites;
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// src/p2p/utils.ts
|
|
951
|
+
function isLocalhostHostname(hostname) {
|
|
952
|
+
const host = String(hostname || "").toLowerCase();
|
|
953
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
954
|
+
}
|
|
955
|
+
function isSecureContextForP2P(hostname, isSecureContext) {
|
|
956
|
+
return Boolean(isSecureContext) || isLocalhostHostname(hostname || "");
|
|
957
|
+
}
|
|
958
|
+
function generateP2PCode(cryptoObj) {
|
|
959
|
+
const crypto2 = cryptoObj || getDefaultCrypto();
|
|
960
|
+
const letters = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
961
|
+
if (crypto2) {
|
|
962
|
+
const randomBytes = new Uint8Array(8);
|
|
963
|
+
crypto2.getRandomValues(randomBytes);
|
|
964
|
+
let letterPart = "";
|
|
965
|
+
for (let i = 0; i < 4; i++) {
|
|
966
|
+
letterPart += letters[randomBytes[i] % letters.length];
|
|
967
|
+
}
|
|
968
|
+
let numberPart = "";
|
|
969
|
+
for (let i = 4; i < 8; i++) {
|
|
970
|
+
numberPart += (randomBytes[i] % 10).toString();
|
|
971
|
+
}
|
|
972
|
+
return `${letterPart}-${numberPart}`;
|
|
973
|
+
}
|
|
974
|
+
let a = "";
|
|
975
|
+
for (let i = 0; i < 4; i++) {
|
|
976
|
+
a += letters[Math.floor(Math.random() * letters.length)];
|
|
977
|
+
}
|
|
978
|
+
let b = "";
|
|
979
|
+
for (let i = 0; i < 4; i++) {
|
|
980
|
+
b += Math.floor(Math.random() * 10);
|
|
981
|
+
}
|
|
982
|
+
return `${a}-${b}`;
|
|
983
|
+
}
|
|
984
|
+
function isP2PCodeLike(code) {
|
|
985
|
+
return /^[A-Z]{4}-\d{4}$/.test(String(code || "").trim());
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/p2p/helpers.ts
|
|
989
|
+
function resolvePeerConfig(userConfig, serverCaps) {
|
|
990
|
+
return {
|
|
991
|
+
path: userConfig.peerjsPath ?? serverCaps?.peerjsPath ?? "/peerjs",
|
|
992
|
+
iceServers: userConfig.iceServers ?? serverCaps?.iceServers ?? []
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function buildPeerOptions(config = {}) {
|
|
996
|
+
const { host, port, peerjsPath = "/peerjs", secure = false, iceServers = [] } = config;
|
|
997
|
+
const peerOpts = {
|
|
998
|
+
host,
|
|
999
|
+
path: peerjsPath,
|
|
1000
|
+
secure,
|
|
1001
|
+
config: { iceServers },
|
|
1002
|
+
debug: 0
|
|
1003
|
+
};
|
|
1004
|
+
if (port) {
|
|
1005
|
+
peerOpts.port = port;
|
|
1006
|
+
}
|
|
1007
|
+
return peerOpts;
|
|
1008
|
+
}
|
|
1009
|
+
async function createPeerWithRetries(opts) {
|
|
1010
|
+
const { code, codeGenerator, maxAttempts, buildPeer, onCode } = opts;
|
|
1011
|
+
let nextCode = code || codeGenerator();
|
|
1012
|
+
let peer = null;
|
|
1013
|
+
let lastError = null;
|
|
1014
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1015
|
+
onCode?.(nextCode, attempt);
|
|
1016
|
+
try {
|
|
1017
|
+
peer = await new Promise((resolve, reject) => {
|
|
1018
|
+
const instance = buildPeer(nextCode);
|
|
1019
|
+
instance.on("open", () => resolve(instance));
|
|
1020
|
+
instance.on("error", (err2) => {
|
|
1021
|
+
try {
|
|
1022
|
+
instance.destroy();
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
reject(err2);
|
|
623
1026
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1027
|
+
});
|
|
1028
|
+
return { peer, code: nextCode };
|
|
1029
|
+
} catch (err2) {
|
|
1030
|
+
lastError = err2;
|
|
1031
|
+
nextCode = codeGenerator();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
throw lastError || new DropgateNetworkError("Could not establish PeerJS connection.");
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/p2p/protocol.ts
|
|
1038
|
+
var P2P_PROTOCOL_VERSION = 3;
|
|
1039
|
+
function isP2PMessage(value) {
|
|
1040
|
+
if (!value || typeof value !== "object") return false;
|
|
1041
|
+
const msg = value;
|
|
1042
|
+
return typeof msg.t === "string" && [
|
|
1043
|
+
"hello",
|
|
1044
|
+
"file_list",
|
|
1045
|
+
"meta",
|
|
1046
|
+
"ready",
|
|
1047
|
+
"chunk",
|
|
1048
|
+
"chunk_ack",
|
|
1049
|
+
"file_end",
|
|
1050
|
+
"file_end_ack",
|
|
1051
|
+
"end",
|
|
1052
|
+
"end_ack",
|
|
1053
|
+
"ping",
|
|
1054
|
+
"pong",
|
|
1055
|
+
"error",
|
|
1056
|
+
"cancelled",
|
|
1057
|
+
"resume",
|
|
1058
|
+
"resume_ack"
|
|
1059
|
+
].includes(msg.t);
|
|
1060
|
+
}
|
|
1061
|
+
var P2P_CHUNK_SIZE = 64 * 1024;
|
|
1062
|
+
var P2P_MAX_UNACKED_CHUNKS = 32;
|
|
1063
|
+
var P2P_END_ACK_TIMEOUT_MS = 15e3;
|
|
1064
|
+
var P2P_END_ACK_RETRIES = 3;
|
|
1065
|
+
var P2P_END_ACK_RETRY_DELAY_MS = 100;
|
|
1066
|
+
var P2P_CLOSE_GRACE_PERIOD_MS = 2e3;
|
|
1067
|
+
|
|
1068
|
+
// src/p2p/send.ts
|
|
1069
|
+
function generateSessionId() {
|
|
1070
|
+
return crypto.randomUUID();
|
|
1071
|
+
}
|
|
1072
|
+
var ALLOWED_TRANSITIONS = {
|
|
1073
|
+
initializing: ["listening", "closed"],
|
|
1074
|
+
listening: ["handshaking", "closed", "cancelled"],
|
|
1075
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
1076
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
1077
|
+
transferring: ["finishing", "closed", "cancelled"],
|
|
1078
|
+
finishing: ["awaiting_ack", "closed", "cancelled"],
|
|
1079
|
+
awaiting_ack: ["completed", "closed", "cancelled"],
|
|
1080
|
+
completed: ["closed"],
|
|
1081
|
+
cancelled: ["closed"],
|
|
1082
|
+
closed: []
|
|
1083
|
+
};
|
|
1084
|
+
async function startP2PSend(opts) {
|
|
1085
|
+
const {
|
|
1086
|
+
file,
|
|
1087
|
+
Peer,
|
|
1088
|
+
serverInfo,
|
|
1089
|
+
host,
|
|
1090
|
+
port,
|
|
1091
|
+
peerjsPath,
|
|
1092
|
+
secure = false,
|
|
1093
|
+
iceServers,
|
|
1094
|
+
codeGenerator,
|
|
1095
|
+
cryptoObj,
|
|
1096
|
+
maxAttempts = 4,
|
|
1097
|
+
chunkSize = P2P_CHUNK_SIZE,
|
|
1098
|
+
endAckTimeoutMs = P2P_END_ACK_TIMEOUT_MS,
|
|
1099
|
+
bufferHighWaterMark = 8 * 1024 * 1024,
|
|
1100
|
+
bufferLowWaterMark = 2 * 1024 * 1024,
|
|
1101
|
+
heartbeatIntervalMs = 5e3,
|
|
1102
|
+
chunkAcknowledgments = true,
|
|
1103
|
+
maxUnackedChunks = P2P_MAX_UNACKED_CHUNKS,
|
|
1104
|
+
onCode,
|
|
1105
|
+
onStatus,
|
|
1106
|
+
onProgress,
|
|
1107
|
+
onComplete,
|
|
1108
|
+
onError,
|
|
1109
|
+
onDisconnect,
|
|
1110
|
+
onCancel,
|
|
1111
|
+
onConnectionHealth
|
|
1112
|
+
} = opts;
|
|
1113
|
+
const files = Array.isArray(file) ? file : [file];
|
|
1114
|
+
const isMultiFile = files.length > 1;
|
|
1115
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
1116
|
+
if (!files.length) {
|
|
1117
|
+
throw new DropgateValidationError("At least one file is required.");
|
|
1118
|
+
}
|
|
1119
|
+
if (!Peer) {
|
|
1120
|
+
throw new DropgateValidationError(
|
|
1121
|
+
"PeerJS Peer constructor is required. Install peerjs and pass it as the Peer option."
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
const p2pCaps = serverInfo?.capabilities?.p2p;
|
|
1125
|
+
if (serverInfo && !p2pCaps?.enabled) {
|
|
1126
|
+
throw new DropgateValidationError("Direct transfer is disabled on this server.");
|
|
1127
|
+
}
|
|
1128
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
1129
|
+
{ peerjsPath, iceServers },
|
|
1130
|
+
p2pCaps
|
|
1131
|
+
);
|
|
1132
|
+
const peerOpts = buildPeerOptions({
|
|
1133
|
+
host,
|
|
1134
|
+
port,
|
|
1135
|
+
peerjsPath: finalPath,
|
|
1136
|
+
secure,
|
|
1137
|
+
iceServers: finalIceServers
|
|
1138
|
+
});
|
|
1139
|
+
const finalCodeGenerator = codeGenerator || (() => generateP2PCode(cryptoObj));
|
|
1140
|
+
const buildPeer = (id) => new Peer(id, peerOpts);
|
|
1141
|
+
const { peer, code } = await createPeerWithRetries({
|
|
1142
|
+
code: null,
|
|
1143
|
+
codeGenerator: finalCodeGenerator,
|
|
1144
|
+
maxAttempts,
|
|
1145
|
+
buildPeer,
|
|
1146
|
+
onCode
|
|
1147
|
+
});
|
|
1148
|
+
const sessionId = generateSessionId();
|
|
1149
|
+
let state = "listening";
|
|
1150
|
+
let activeConn = null;
|
|
1151
|
+
let sentBytes = 0;
|
|
1152
|
+
let heartbeatTimer = null;
|
|
1153
|
+
let healthCheckTimer = null;
|
|
1154
|
+
let lastActivityTime = Date.now();
|
|
1155
|
+
const unackedChunks = /* @__PURE__ */ new Map();
|
|
1156
|
+
let nextSeq = 0;
|
|
1157
|
+
let ackResolvers = [];
|
|
1158
|
+
const transitionTo = (newState) => {
|
|
1159
|
+
if (!ALLOWED_TRANSITIONS[state].includes(newState)) {
|
|
1160
|
+
console.warn(`[P2P Send] Invalid state transition: ${state} -> ${newState}`);
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
state = newState;
|
|
1164
|
+
return true;
|
|
1165
|
+
};
|
|
1166
|
+
const reportProgress = (data) => {
|
|
1167
|
+
if (isStopped()) return;
|
|
1168
|
+
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : totalSize;
|
|
1169
|
+
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
1170
|
+
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
1171
|
+
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
1172
|
+
};
|
|
1173
|
+
const safeError = (err2) => {
|
|
1174
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1175
|
+
transitionTo("closed");
|
|
1176
|
+
onError?.(err2);
|
|
1177
|
+
cleanup();
|
|
1178
|
+
};
|
|
1179
|
+
const safeComplete = () => {
|
|
1180
|
+
if (state !== "awaiting_ack" && state !== "finishing") return;
|
|
1181
|
+
transitionTo("completed");
|
|
1182
|
+
onComplete?.();
|
|
1183
|
+
cleanup();
|
|
1184
|
+
};
|
|
1185
|
+
const cleanup = () => {
|
|
1186
|
+
if (heartbeatTimer) {
|
|
1187
|
+
clearInterval(heartbeatTimer);
|
|
1188
|
+
heartbeatTimer = null;
|
|
1189
|
+
}
|
|
1190
|
+
if (healthCheckTimer) {
|
|
1191
|
+
clearInterval(healthCheckTimer);
|
|
1192
|
+
healthCheckTimer = null;
|
|
1193
|
+
}
|
|
1194
|
+
ackResolvers.forEach((resolve) => resolve());
|
|
1195
|
+
ackResolvers = [];
|
|
1196
|
+
unackedChunks.clear();
|
|
1197
|
+
if (typeof window !== "undefined") {
|
|
1198
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
1199
|
+
}
|
|
1200
|
+
try {
|
|
1201
|
+
activeConn?.close();
|
|
1202
|
+
} catch {
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
peer.destroy();
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
const handleUnload = () => {
|
|
1210
|
+
try {
|
|
1211
|
+
activeConn?.send({ t: "error", message: "Sender closed the connection." });
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
stop();
|
|
1215
|
+
};
|
|
1216
|
+
if (typeof window !== "undefined") {
|
|
1217
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
1218
|
+
}
|
|
1219
|
+
const stop = () => {
|
|
1220
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1221
|
+
if (state === "completed") {
|
|
1222
|
+
cleanup();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const wasActive = state === "transferring" || state === "finishing" || state === "awaiting_ack";
|
|
1226
|
+
transitionTo("cancelled");
|
|
1227
|
+
try {
|
|
1228
|
+
if (activeConn && activeConn.open) {
|
|
1229
|
+
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
1230
|
+
}
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
if (wasActive && onCancel) {
|
|
1234
|
+
onCancel({ cancelledBy: "sender" });
|
|
1235
|
+
}
|
|
1236
|
+
cleanup();
|
|
1237
|
+
};
|
|
1238
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
1239
|
+
const startHealthMonitoring = (conn) => {
|
|
1240
|
+
if (!onConnectionHealth) return;
|
|
1241
|
+
healthCheckTimer = setInterval(() => {
|
|
1242
|
+
if (isStopped()) return;
|
|
1243
|
+
const dc = conn._dc;
|
|
1244
|
+
if (!dc) return;
|
|
1245
|
+
const health = {
|
|
1246
|
+
iceConnectionState: dc.readyState === "open" ? "connected" : "disconnected",
|
|
1247
|
+
bufferedAmount: dc.bufferedAmount,
|
|
1248
|
+
lastActivityMs: Date.now() - lastActivityTime
|
|
1249
|
+
};
|
|
1250
|
+
onConnectionHealth(health);
|
|
1251
|
+
}, 2e3);
|
|
1252
|
+
};
|
|
1253
|
+
const handleChunkAck = (msg) => {
|
|
1254
|
+
lastActivityTime = Date.now();
|
|
1255
|
+
unackedChunks.delete(msg.seq);
|
|
1256
|
+
reportProgress({ received: msg.received, total: totalSize });
|
|
1257
|
+
const resolver = ackResolvers.shift();
|
|
1258
|
+
if (resolver) resolver();
|
|
1259
|
+
};
|
|
1260
|
+
const waitForAck = () => {
|
|
1261
|
+
return new Promise((resolve) => {
|
|
1262
|
+
ackResolvers.push(resolve);
|
|
1263
|
+
});
|
|
1264
|
+
};
|
|
1265
|
+
const sendChunk = async (conn, data, offset, fileTotal) => {
|
|
1266
|
+
if (chunkAcknowledgments) {
|
|
1267
|
+
while (unackedChunks.size >= maxUnackedChunks) {
|
|
1268
|
+
await Promise.race([
|
|
1269
|
+
waitForAck(),
|
|
1270
|
+
sleep(1e3)
|
|
1271
|
+
// Timeout to prevent deadlock
|
|
1272
|
+
]);
|
|
1273
|
+
if (isStopped()) return;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const seq = nextSeq++;
|
|
1277
|
+
if (chunkAcknowledgments) {
|
|
1278
|
+
unackedChunks.set(seq, { offset, size: data.byteLength, sentAt: Date.now() });
|
|
1279
|
+
}
|
|
1280
|
+
conn.send({ t: "chunk", seq, offset, size: data.byteLength, total: fileTotal ?? totalSize });
|
|
1281
|
+
conn.send(data);
|
|
1282
|
+
sentBytes += data.byteLength;
|
|
1283
|
+
const dc = conn._dc;
|
|
1284
|
+
if (dc && bufferHighWaterMark > 0) {
|
|
1285
|
+
while (dc.bufferedAmount > bufferHighWaterMark) {
|
|
1286
|
+
await new Promise((resolve) => {
|
|
1287
|
+
const fallback = setTimeout(resolve, 60);
|
|
1288
|
+
try {
|
|
1289
|
+
dc.addEventListener(
|
|
1290
|
+
"bufferedamountlow",
|
|
1291
|
+
() => {
|
|
1292
|
+
clearTimeout(fallback);
|
|
1293
|
+
resolve();
|
|
1294
|
+
},
|
|
1295
|
+
{ once: true }
|
|
1296
|
+
);
|
|
1297
|
+
} catch {
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
if (isStopped()) return;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
const waitForEndAck = async (conn, ackPromise) => {
|
|
1305
|
+
const baseTimeout = endAckTimeoutMs;
|
|
1306
|
+
for (let attempt = 0; attempt < P2P_END_ACK_RETRIES; attempt++) {
|
|
1307
|
+
conn.send({ t: "end", attempt });
|
|
1308
|
+
const timeout = baseTimeout * Math.pow(1.5, attempt);
|
|
1309
|
+
const result = await Promise.race([
|
|
1310
|
+
ackPromise,
|
|
1311
|
+
sleep(timeout).then(() => null)
|
|
1312
|
+
]);
|
|
1313
|
+
if (result && result.t === "end_ack") {
|
|
1314
|
+
return result;
|
|
1315
|
+
}
|
|
1316
|
+
if (isStopped()) {
|
|
1317
|
+
throw new DropgateNetworkError("Connection closed during completion.");
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
throw new DropgateNetworkError("Receiver did not confirm completion after retries.");
|
|
1321
|
+
};
|
|
1322
|
+
peer.on("connection", (conn) => {
|
|
1323
|
+
if (isStopped()) return;
|
|
1324
|
+
if (activeConn) {
|
|
1325
|
+
const isOldConnOpen = activeConn.open !== false;
|
|
1326
|
+
if (isOldConnOpen && state === "transferring") {
|
|
1327
|
+
try {
|
|
1328
|
+
conn.send({ t: "error", message: "Transfer already in progress." });
|
|
1329
|
+
} catch {
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
conn.close();
|
|
1333
|
+
} catch {
|
|
1334
|
+
}
|
|
1335
|
+
return;
|
|
1336
|
+
} else if (!isOldConnOpen) {
|
|
1337
|
+
try {
|
|
1338
|
+
activeConn.close();
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
activeConn = null;
|
|
1342
|
+
state = "listening";
|
|
1343
|
+
sentBytes = 0;
|
|
1344
|
+
nextSeq = 0;
|
|
1345
|
+
unackedChunks.clear();
|
|
1346
|
+
} else {
|
|
1347
|
+
try {
|
|
1348
|
+
conn.send({ t: "error", message: "Another receiver is already connected." });
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
try {
|
|
1352
|
+
conn.close();
|
|
1353
|
+
} catch {
|
|
630
1354
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
activeConn = conn;
|
|
1359
|
+
transitionTo("handshaking");
|
|
1360
|
+
if (!isStopped()) onStatus?.({ phase: "connected", message: "Receiver connected." });
|
|
1361
|
+
lastActivityTime = Date.now();
|
|
1362
|
+
let helloResolve = null;
|
|
1363
|
+
let readyResolve = null;
|
|
1364
|
+
let endAckResolve = null;
|
|
1365
|
+
let fileEndAckResolve = null;
|
|
1366
|
+
const helloPromise = new Promise((resolve) => {
|
|
1367
|
+
helloResolve = resolve;
|
|
1368
|
+
});
|
|
1369
|
+
const readyPromise = new Promise((resolve) => {
|
|
1370
|
+
readyResolve = resolve;
|
|
1371
|
+
});
|
|
1372
|
+
const endAckPromise = new Promise((resolve) => {
|
|
1373
|
+
endAckResolve = resolve;
|
|
1374
|
+
});
|
|
1375
|
+
conn.on("data", (data) => {
|
|
1376
|
+
lastActivityTime = Date.now();
|
|
1377
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (!isP2PMessage(data)) return;
|
|
1381
|
+
const msg = data;
|
|
1382
|
+
switch (msg.t) {
|
|
1383
|
+
case "hello":
|
|
1384
|
+
helloResolve?.(msg.protocolVersion);
|
|
1385
|
+
break;
|
|
1386
|
+
case "ready":
|
|
1387
|
+
if (!isStopped()) onStatus?.({ phase: "transferring", message: "Receiver accepted. Starting transfer..." });
|
|
1388
|
+
readyResolve?.();
|
|
1389
|
+
break;
|
|
1390
|
+
case "chunk_ack":
|
|
1391
|
+
handleChunkAck(msg);
|
|
1392
|
+
break;
|
|
1393
|
+
case "file_end_ack":
|
|
1394
|
+
fileEndAckResolve?.(msg);
|
|
1395
|
+
break;
|
|
1396
|
+
case "end_ack":
|
|
1397
|
+
endAckResolve?.(msg);
|
|
1398
|
+
break;
|
|
1399
|
+
case "pong":
|
|
1400
|
+
break;
|
|
1401
|
+
case "error":
|
|
1402
|
+
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
1403
|
+
break;
|
|
1404
|
+
case "cancelled":
|
|
1405
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1406
|
+
transitionTo("cancelled");
|
|
1407
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.reason });
|
|
1408
|
+
cleanup();
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
conn.on("open", async () => {
|
|
1413
|
+
try {
|
|
1414
|
+
if (isStopped()) return;
|
|
1415
|
+
startHealthMonitoring(conn);
|
|
1416
|
+
conn.send({
|
|
1417
|
+
t: "hello",
|
|
1418
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
1419
|
+
sessionId
|
|
1420
|
+
});
|
|
1421
|
+
const receiverVersion = await Promise.race([
|
|
1422
|
+
helloPromise,
|
|
1423
|
+
sleep(1e4).then(() => null)
|
|
1424
|
+
]);
|
|
1425
|
+
if (isStopped()) return;
|
|
1426
|
+
if (receiverVersion === null) {
|
|
1427
|
+
throw new DropgateNetworkError("Receiver did not respond to handshake.");
|
|
1428
|
+
} else if (receiverVersion !== P2P_PROTOCOL_VERSION) {
|
|
1429
|
+
throw new DropgateNetworkError(
|
|
1430
|
+
`Protocol version mismatch: sender v${P2P_PROTOCOL_VERSION}, receiver v${receiverVersion}`
|
|
636
1431
|
);
|
|
637
1432
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
throw effectiveSignal.reason || new DropgateAbortError();
|
|
647
|
-
}
|
|
648
|
-
const start = i * this.chunkSize;
|
|
649
|
-
const end = Math.min(start + this.chunkSize, file.size);
|
|
650
|
-
let chunkBlob = file.slice(start, end);
|
|
651
|
-
const percentComplete = i / totalChunks * 100;
|
|
652
|
-
const processedBytes = i * this.chunkSize;
|
|
653
|
-
progress({
|
|
654
|
-
phase: "chunk",
|
|
655
|
-
text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
|
|
656
|
-
percent: percentComplete,
|
|
657
|
-
processedBytes,
|
|
658
|
-
totalBytes: fileSizeBytes,
|
|
659
|
-
chunkIndex: i,
|
|
660
|
-
totalChunks
|
|
1433
|
+
transitionTo("negotiating");
|
|
1434
|
+
if (!isStopped()) onStatus?.({ phase: "waiting", message: "Connected. Waiting for receiver to accept..." });
|
|
1435
|
+
if (isMultiFile) {
|
|
1436
|
+
conn.send({
|
|
1437
|
+
t: "file_list",
|
|
1438
|
+
fileCount: files.length,
|
|
1439
|
+
files: files.map((f) => ({ name: f.name, size: f.size, mime: f.type || "application/octet-stream" })),
|
|
1440
|
+
totalSize
|
|
661
1441
|
});
|
|
662
|
-
const chunkBuffer = await chunkBlob.arrayBuffer();
|
|
663
|
-
let uploadBlob;
|
|
664
|
-
if (effectiveEncrypt && cryptoKey) {
|
|
665
|
-
uploadBlob = await encryptToBlob(this.cryptoObj, chunkBuffer, cryptoKey);
|
|
666
|
-
} else {
|
|
667
|
-
uploadBlob = new Blob([chunkBuffer]);
|
|
668
|
-
}
|
|
669
|
-
if (uploadBlob.size > DEFAULT_CHUNK_SIZE + 1024) {
|
|
670
|
-
throw new DropgateValidationError(
|
|
671
|
-
"Chunk too large (client-side). Check chunk size settings."
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
const toHash = await uploadBlob.arrayBuffer();
|
|
675
|
-
const hashHex = await sha256Hex(this.cryptoObj, toHash);
|
|
676
|
-
const headers = {
|
|
677
|
-
"Content-Type": "application/octet-stream",
|
|
678
|
-
"X-Upload-ID": uploadId,
|
|
679
|
-
"X-Chunk-Index": String(i),
|
|
680
|
-
"X-Chunk-Hash": hashHex
|
|
681
|
-
};
|
|
682
|
-
const chunkUrl = `${baseUrl}/upload/chunk`;
|
|
683
|
-
await this.attemptChunkUpload(
|
|
684
|
-
chunkUrl,
|
|
685
|
-
{
|
|
686
|
-
method: "POST",
|
|
687
|
-
headers,
|
|
688
|
-
body: uploadBlob
|
|
689
|
-
},
|
|
690
|
-
{
|
|
691
|
-
retries,
|
|
692
|
-
backoffMs: baseBackoffMs,
|
|
693
|
-
maxBackoffMs,
|
|
694
|
-
timeoutMs: timeouts.chunkMs ?? 6e4,
|
|
695
|
-
signal: effectiveSignal,
|
|
696
|
-
progress,
|
|
697
|
-
chunkIndex: i,
|
|
698
|
-
totalChunks,
|
|
699
|
-
chunkSize: this.chunkSize,
|
|
700
|
-
fileSizeBytes
|
|
701
|
-
}
|
|
702
|
-
);
|
|
703
1442
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
body: JSON.stringify({ uploadId })
|
|
1443
|
+
conn.send({
|
|
1444
|
+
t: "meta",
|
|
1445
|
+
sessionId,
|
|
1446
|
+
name: files[0].name,
|
|
1447
|
+
size: files[0].size,
|
|
1448
|
+
mime: files[0].type || "application/octet-stream",
|
|
1449
|
+
...isMultiFile ? { fileIndex: 0 } : {}
|
|
1450
|
+
});
|
|
1451
|
+
const dc = conn._dc;
|
|
1452
|
+
if (dc && Number.isFinite(bufferLowWaterMark)) {
|
|
1453
|
+
try {
|
|
1454
|
+
dc.bufferedAmountLowThreshold = bufferLowWaterMark;
|
|
1455
|
+
} catch {
|
|
718
1456
|
}
|
|
719
|
-
);
|
|
720
|
-
if (!completeRes.res.ok) {
|
|
721
|
-
const errorJson = completeRes.json;
|
|
722
|
-
const msg = errorJson?.error || "Finalisation failed.";
|
|
723
|
-
throw new DropgateProtocolError(msg, {
|
|
724
|
-
details: completeRes.json || completeRes.text
|
|
725
|
-
});
|
|
726
1457
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
"
|
|
732
|
-
|
|
1458
|
+
await readyPromise;
|
|
1459
|
+
if (isStopped()) return;
|
|
1460
|
+
if (heartbeatIntervalMs > 0) {
|
|
1461
|
+
heartbeatTimer = setInterval(() => {
|
|
1462
|
+
if (state === "transferring" || state === "finishing" || state === "awaiting_ack") {
|
|
1463
|
+
try {
|
|
1464
|
+
conn.send({ t: "ping", timestamp: Date.now() });
|
|
1465
|
+
} catch {
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}, heartbeatIntervalMs);
|
|
733
1469
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1470
|
+
transitionTo("transferring");
|
|
1471
|
+
let overallSentBytes = 0;
|
|
1472
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
1473
|
+
const currentFile = files[fi];
|
|
1474
|
+
if (isMultiFile && fi > 0) {
|
|
1475
|
+
conn.send({
|
|
1476
|
+
t: "meta",
|
|
1477
|
+
sessionId,
|
|
1478
|
+
name: currentFile.name,
|
|
1479
|
+
size: currentFile.size,
|
|
1480
|
+
mime: currentFile.type || "application/octet-stream",
|
|
1481
|
+
fileIndex: fi
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
for (let offset = 0; offset < currentFile.size; offset += chunkSize) {
|
|
1485
|
+
if (isStopped()) return;
|
|
1486
|
+
const slice = currentFile.slice(offset, offset + chunkSize);
|
|
1487
|
+
const buf = await slice.arrayBuffer();
|
|
1488
|
+
if (isStopped()) return;
|
|
1489
|
+
await sendChunk(conn, buf, offset, currentFile.size);
|
|
1490
|
+
overallSentBytes += buf.byteLength;
|
|
1491
|
+
reportProgress({ received: overallSentBytes, total: totalSize });
|
|
1492
|
+
}
|
|
1493
|
+
if (isStopped()) return;
|
|
1494
|
+
if (isMultiFile) {
|
|
1495
|
+
const fileEndAckPromise = new Promise((resolve) => {
|
|
1496
|
+
fileEndAckResolve = resolve;
|
|
1497
|
+
});
|
|
1498
|
+
conn.send({ t: "file_end", fileIndex: fi });
|
|
1499
|
+
const feAck = await Promise.race([
|
|
1500
|
+
fileEndAckPromise,
|
|
1501
|
+
sleep(endAckTimeoutMs).then(() => null)
|
|
1502
|
+
]);
|
|
1503
|
+
if (isStopped()) return;
|
|
1504
|
+
if (!feAck) {
|
|
1505
|
+
throw new DropgateNetworkError(`Receiver did not confirm receipt of file ${fi + 1}/${files.length}.`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
737
1508
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
} catch (err) {
|
|
748
|
-
if (err instanceof Error && (err.name === "AbortError" || err.message?.includes("abort"))) {
|
|
749
|
-
uploadState = "cancelled";
|
|
750
|
-
onCancel?.();
|
|
751
|
-
} else {
|
|
752
|
-
uploadState = "error";
|
|
1509
|
+
if (isStopped()) return;
|
|
1510
|
+
transitionTo("finishing");
|
|
1511
|
+
transitionTo("awaiting_ack");
|
|
1512
|
+
const ackResult = await waitForEndAck(conn, endAckPromise);
|
|
1513
|
+
if (isStopped()) return;
|
|
1514
|
+
const ackTotal = Number(ackResult.total) || totalSize;
|
|
1515
|
+
const ackReceived = Number(ackResult.received) || 0;
|
|
1516
|
+
if (ackTotal && ackReceived < ackTotal) {
|
|
1517
|
+
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
753
1518
|
}
|
|
754
|
-
|
|
1519
|
+
reportProgress({ received: ackReceived || ackTotal, total: ackTotal });
|
|
1520
|
+
safeComplete();
|
|
1521
|
+
} catch (err2) {
|
|
1522
|
+
safeError(err2);
|
|
755
1523
|
}
|
|
756
|
-
})
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1524
|
+
});
|
|
1525
|
+
conn.on("error", (err2) => {
|
|
1526
|
+
safeError(err2);
|
|
1527
|
+
});
|
|
1528
|
+
conn.on("close", () => {
|
|
1529
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1530
|
+
cleanup();
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (state === "awaiting_ack") {
|
|
1534
|
+
setTimeout(() => {
|
|
1535
|
+
if (state === "awaiting_ack") {
|
|
1536
|
+
safeError(new DropgateNetworkError("Connection closed while awaiting confirmation."));
|
|
1537
|
+
}
|
|
1538
|
+
}, P2P_CLOSE_GRACE_PERIOD_MS);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (state === "transferring" || state === "finishing") {
|
|
1542
|
+
transitionTo("cancelled");
|
|
1543
|
+
onCancel?.({ cancelledBy: "receiver" });
|
|
1544
|
+
cleanup();
|
|
1545
|
+
} else {
|
|
1546
|
+
activeConn = null;
|
|
1547
|
+
state = "listening";
|
|
1548
|
+
sentBytes = 0;
|
|
1549
|
+
nextSeq = 0;
|
|
1550
|
+
unackedChunks.clear();
|
|
1551
|
+
onDisconnect?.();
|
|
769
1552
|
}
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1553
|
+
});
|
|
1554
|
+
});
|
|
1555
|
+
return {
|
|
1556
|
+
peer,
|
|
1557
|
+
code,
|
|
1558
|
+
sessionId,
|
|
1559
|
+
stop,
|
|
1560
|
+
getStatus: () => state,
|
|
1561
|
+
getBytesSent: () => sentBytes,
|
|
1562
|
+
getConnectedPeerId: () => {
|
|
1563
|
+
if (!activeConn) return null;
|
|
1564
|
+
return activeConn.peer || null;
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/p2p/receive.ts
|
|
1570
|
+
var ALLOWED_TRANSITIONS2 = {
|
|
1571
|
+
initializing: ["connecting", "closed"],
|
|
1572
|
+
connecting: ["handshaking", "closed", "cancelled"],
|
|
1573
|
+
handshaking: ["negotiating", "closed", "cancelled"],
|
|
1574
|
+
negotiating: ["transferring", "closed", "cancelled"],
|
|
1575
|
+
transferring: ["completed", "closed", "cancelled"],
|
|
1576
|
+
completed: ["closed"],
|
|
1577
|
+
cancelled: ["closed"],
|
|
1578
|
+
closed: []
|
|
1579
|
+
};
|
|
1580
|
+
async function startP2PReceive(opts) {
|
|
1581
|
+
const {
|
|
1582
|
+
code,
|
|
1583
|
+
Peer,
|
|
1584
|
+
serverInfo,
|
|
1585
|
+
host,
|
|
1586
|
+
port,
|
|
1587
|
+
peerjsPath,
|
|
1588
|
+
secure = false,
|
|
1589
|
+
iceServers,
|
|
1590
|
+
autoReady = true,
|
|
1591
|
+
watchdogTimeoutMs = 15e3,
|
|
1592
|
+
onStatus,
|
|
1593
|
+
onMeta,
|
|
1594
|
+
onData,
|
|
1595
|
+
onProgress,
|
|
1596
|
+
onFileStart,
|
|
1597
|
+
onFileEnd,
|
|
1598
|
+
onComplete,
|
|
1599
|
+
onError,
|
|
1600
|
+
onDisconnect,
|
|
1601
|
+
onCancel
|
|
1602
|
+
} = opts;
|
|
1603
|
+
if (!code) {
|
|
1604
|
+
throw new DropgateValidationError("No sharing code was provided.");
|
|
784
1605
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1606
|
+
if (!Peer) {
|
|
1607
|
+
throw new DropgateValidationError(
|
|
1608
|
+
"PeerJS Peer constructor is required. Install peerjs and pass it as the Peer option."
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
const p2pCaps = serverInfo?.capabilities?.p2p;
|
|
1612
|
+
if (serverInfo && !p2pCaps?.enabled) {
|
|
1613
|
+
throw new DropgateValidationError("Direct transfer is disabled on this server.");
|
|
1614
|
+
}
|
|
1615
|
+
const normalizedCode = String(code).trim().replace(/\s+/g, "").toUpperCase();
|
|
1616
|
+
if (!isP2PCodeLike(normalizedCode)) {
|
|
1617
|
+
throw new DropgateValidationError("Invalid direct transfer code.");
|
|
1618
|
+
}
|
|
1619
|
+
const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(
|
|
1620
|
+
{ peerjsPath, iceServers },
|
|
1621
|
+
p2pCaps
|
|
1622
|
+
);
|
|
1623
|
+
const peerOpts = buildPeerOptions({
|
|
1624
|
+
host,
|
|
1625
|
+
port,
|
|
1626
|
+
peerjsPath: finalPath,
|
|
1627
|
+
secure,
|
|
1628
|
+
iceServers: finalIceServers
|
|
1629
|
+
});
|
|
1630
|
+
const peer = new Peer(void 0, peerOpts);
|
|
1631
|
+
let state = "initializing";
|
|
1632
|
+
let total = 0;
|
|
1633
|
+
let received = 0;
|
|
1634
|
+
let currentSessionId = null;
|
|
1635
|
+
let writeQueue = Promise.resolve();
|
|
1636
|
+
let watchdogTimer = null;
|
|
1637
|
+
let activeConn = null;
|
|
1638
|
+
let pendingChunk = null;
|
|
1639
|
+
let fileList = null;
|
|
1640
|
+
let currentFileReceived = 0;
|
|
1641
|
+
let totalReceivedAllFiles = 0;
|
|
1642
|
+
const transitionTo = (newState) => {
|
|
1643
|
+
if (!ALLOWED_TRANSITIONS2[state].includes(newState)) {
|
|
1644
|
+
console.warn(`[P2P Receive] Invalid state transition: ${state} -> ${newState}`);
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
state = newState;
|
|
1648
|
+
return true;
|
|
1649
|
+
};
|
|
1650
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
1651
|
+
const resetWatchdog = () => {
|
|
1652
|
+
if (watchdogTimeoutMs <= 0) return;
|
|
1653
|
+
if (watchdogTimer) {
|
|
1654
|
+
clearTimeout(watchdogTimer);
|
|
1655
|
+
}
|
|
1656
|
+
watchdogTimer = setTimeout(() => {
|
|
1657
|
+
if (state === "transferring") {
|
|
1658
|
+
safeError(new DropgateNetworkError("Connection timed out (no data received)."));
|
|
817
1659
|
}
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
|
|
1660
|
+
}, watchdogTimeoutMs);
|
|
1661
|
+
};
|
|
1662
|
+
const clearWatchdog = () => {
|
|
1663
|
+
if (watchdogTimer) {
|
|
1664
|
+
clearTimeout(watchdogTimer);
|
|
1665
|
+
watchdogTimer = null;
|
|
821
1666
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1667
|
+
};
|
|
1668
|
+
const safeError = (err2) => {
|
|
1669
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1670
|
+
transitionTo("closed");
|
|
1671
|
+
onError?.(err2);
|
|
1672
|
+
cleanup();
|
|
1673
|
+
};
|
|
1674
|
+
const safeComplete = (completeData) => {
|
|
1675
|
+
if (state !== "transferring") return;
|
|
1676
|
+
transitionTo("completed");
|
|
1677
|
+
onComplete?.(completeData);
|
|
1678
|
+
};
|
|
1679
|
+
const cleanup = () => {
|
|
1680
|
+
clearWatchdog();
|
|
1681
|
+
if (typeof window !== "undefined") {
|
|
1682
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
834
1683
|
}
|
|
835
|
-
progress({ phase: "metadata", text: "Fetching file info...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
836
|
-
const { signal: metaSignal, cleanup: metaCleanup } = makeAbortSignal(signal, timeoutMs);
|
|
837
|
-
let metadata;
|
|
838
1684
|
try {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
headers: { Accept: "application/json" },
|
|
842
|
-
signal: metaSignal
|
|
843
|
-
});
|
|
844
|
-
if (!metaRes.ok) {
|
|
845
|
-
if (metaRes.status === 404) {
|
|
846
|
-
throw new DropgateProtocolError("File not found or has expired.");
|
|
847
|
-
}
|
|
848
|
-
throw new DropgateProtocolError(`Failed to fetch file metadata (status ${metaRes.status}).`);
|
|
849
|
-
}
|
|
850
|
-
metadata = await metaRes.json();
|
|
851
|
-
} catch (err) {
|
|
852
|
-
if (err instanceof DropgateError) throw err;
|
|
853
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
854
|
-
throw new DropgateAbortError("Download cancelled.");
|
|
855
|
-
}
|
|
856
|
-
throw new DropgateNetworkError("Could not fetch file metadata.", { cause: err });
|
|
857
|
-
} finally {
|
|
858
|
-
metaCleanup();
|
|
1685
|
+
peer.destroy();
|
|
1686
|
+
} catch {
|
|
859
1687
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
throw new DropgateValidationError(
|
|
866
|
-
`File is too large (${sizeMB}MB) to download without streaming. Provide an onData callback to stream files larger than ${limitMB}MB.`
|
|
867
|
-
);
|
|
1688
|
+
};
|
|
1689
|
+
const handleUnload = () => {
|
|
1690
|
+
try {
|
|
1691
|
+
activeConn?.send({ t: "error", message: "Receiver closed the connection." });
|
|
1692
|
+
} catch {
|
|
868
1693
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
this.base64
|
|
886
|
-
);
|
|
887
|
-
} catch (err) {
|
|
888
|
-
throw new DropgateError("Failed to decrypt filename. Invalid key or corrupted data.", {
|
|
889
|
-
code: "DECRYPT_FILENAME_FAILED",
|
|
890
|
-
cause: err
|
|
891
|
-
});
|
|
1694
|
+
stop();
|
|
1695
|
+
};
|
|
1696
|
+
if (typeof window !== "undefined") {
|
|
1697
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
1698
|
+
}
|
|
1699
|
+
const stop = () => {
|
|
1700
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1701
|
+
if (state === "completed") {
|
|
1702
|
+
cleanup();
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const wasActive = state === "transferring";
|
|
1706
|
+
transitionTo("cancelled");
|
|
1707
|
+
try {
|
|
1708
|
+
if (activeConn && activeConn.open) {
|
|
1709
|
+
activeConn.send({ t: "cancelled", reason: "Receiver cancelled the transfer." });
|
|
892
1710
|
}
|
|
893
|
-
}
|
|
894
|
-
filename = metadata.filename || "file";
|
|
1711
|
+
} catch {
|
|
895
1712
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1713
|
+
if (wasActive && onCancel) {
|
|
1714
|
+
onCancel({ cancelledBy: "receiver" });
|
|
1715
|
+
}
|
|
1716
|
+
cleanup();
|
|
1717
|
+
};
|
|
1718
|
+
const sendChunkAck = (conn, seq) => {
|
|
901
1719
|
try {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1720
|
+
conn.send({ t: "chunk_ack", seq, received });
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
peer.on("error", (err2) => {
|
|
1725
|
+
safeError(err2);
|
|
1726
|
+
});
|
|
1727
|
+
peer.on("open", () => {
|
|
1728
|
+
transitionTo("connecting");
|
|
1729
|
+
const conn = peer.connect(normalizedCode, { reliable: true });
|
|
1730
|
+
activeConn = conn;
|
|
1731
|
+
conn.on("open", () => {
|
|
1732
|
+
transitionTo("handshaking");
|
|
1733
|
+
onStatus?.({ phase: "connected", message: "Connected." });
|
|
1734
|
+
conn.send({
|
|
1735
|
+
t: "hello",
|
|
1736
|
+
protocolVersion: P2P_PROTOCOL_VERSION,
|
|
1737
|
+
sessionId: ""
|
|
905
1738
|
});
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
offset += chunk.length;
|
|
930
|
-
}
|
|
931
|
-
pendingChunks.length = 0;
|
|
932
|
-
pendingLength = 0;
|
|
933
|
-
return result;
|
|
934
|
-
};
|
|
935
|
-
while (true) {
|
|
936
|
-
if (signal?.aborted) {
|
|
937
|
-
throw new DropgateAbortError("Download cancelled.");
|
|
938
|
-
}
|
|
939
|
-
const { done, value } = await reader.read();
|
|
940
|
-
if (done) break;
|
|
941
|
-
pendingChunks.push(value);
|
|
942
|
-
pendingLength += value.length;
|
|
943
|
-
while (pendingLength >= ENCRYPTED_CHUNK_SIZE) {
|
|
944
|
-
const buffer = flushPending();
|
|
945
|
-
const encryptedChunk = buffer.subarray(0, ENCRYPTED_CHUNK_SIZE);
|
|
946
|
-
if (buffer.length > ENCRYPTED_CHUNK_SIZE) {
|
|
947
|
-
const remainder = buffer.subarray(ENCRYPTED_CHUNK_SIZE);
|
|
948
|
-
pendingChunks.push(remainder);
|
|
949
|
-
pendingLength = remainder.length;
|
|
1739
|
+
});
|
|
1740
|
+
conn.on("data", async (data) => {
|
|
1741
|
+
try {
|
|
1742
|
+
resetWatchdog();
|
|
1743
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || typeof Blob !== "undefined" && data instanceof Blob) {
|
|
1744
|
+
let bufPromise;
|
|
1745
|
+
if (data instanceof ArrayBuffer) {
|
|
1746
|
+
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
1747
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
1748
|
+
bufPromise = Promise.resolve(
|
|
1749
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
1750
|
+
);
|
|
1751
|
+
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
1752
|
+
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
1753
|
+
} else {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
const chunkSeq = pendingChunk?.seq ?? -1;
|
|
1757
|
+
pendingChunk = null;
|
|
1758
|
+
writeQueue = writeQueue.then(async () => {
|
|
1759
|
+
const buf = await bufPromise;
|
|
1760
|
+
if (onData) {
|
|
1761
|
+
await onData(buf);
|
|
950
1762
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1763
|
+
received += buf.byteLength;
|
|
1764
|
+
currentFileReceived += buf.byteLength;
|
|
1765
|
+
const progressReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
1766
|
+
const progressTotal = fileList ? fileList.totalSize : total;
|
|
1767
|
+
const percent = progressTotal ? Math.min(100, progressReceived / progressTotal * 100) : 0;
|
|
1768
|
+
if (!isStopped()) onProgress?.({ processedBytes: progressReceived, totalBytes: progressTotal, percent });
|
|
1769
|
+
if (chunkSeq >= 0) {
|
|
1770
|
+
sendChunkAck(conn, chunkSeq);
|
|
957
1771
|
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1772
|
+
}).catch((err2) => {
|
|
1773
|
+
try {
|
|
1774
|
+
conn.send({
|
|
1775
|
+
t: "error",
|
|
1776
|
+
message: err2?.message || "Receiver write failed."
|
|
1777
|
+
});
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
safeError(err2);
|
|
967
1781
|
});
|
|
1782
|
+
return;
|
|
968
1783
|
}
|
|
969
|
-
if (
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1784
|
+
if (!isP2PMessage(data)) return;
|
|
1785
|
+
const msg = data;
|
|
1786
|
+
switch (msg.t) {
|
|
1787
|
+
case "hello":
|
|
1788
|
+
currentSessionId = msg.sessionId || null;
|
|
1789
|
+
transitionTo("negotiating");
|
|
1790
|
+
onStatus?.({ phase: "waiting", message: "Waiting for file details..." });
|
|
1791
|
+
break;
|
|
1792
|
+
case "file_list":
|
|
1793
|
+
fileList = msg;
|
|
1794
|
+
total = fileList.totalSize;
|
|
1795
|
+
break;
|
|
1796
|
+
case "meta": {
|
|
1797
|
+
if (state !== "negotiating" && !(state === "transferring" && fileList)) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {
|
|
1801
|
+
try {
|
|
1802
|
+
conn.send({ t: "error", message: "Busy with another session." });
|
|
1803
|
+
} catch {
|
|
1804
|
+
}
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (msg.sessionId) {
|
|
1808
|
+
currentSessionId = msg.sessionId;
|
|
1809
|
+
}
|
|
1810
|
+
const name = String(msg.name || "file");
|
|
1811
|
+
const fileSize = Number(msg.size) || 0;
|
|
1812
|
+
const fi = msg.fileIndex;
|
|
1813
|
+
if (fileList && typeof fi === "number" && fi > 0) {
|
|
1814
|
+
currentFileReceived = 0;
|
|
1815
|
+
onFileStart?.({ fileIndex: fi, name, size: fileSize });
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
received = 0;
|
|
1819
|
+
currentFileReceived = 0;
|
|
1820
|
+
totalReceivedAllFiles = 0;
|
|
1821
|
+
if (!fileList) {
|
|
1822
|
+
total = fileSize;
|
|
1823
|
+
}
|
|
1824
|
+
writeQueue = Promise.resolve();
|
|
1825
|
+
const sendReady = () => {
|
|
1826
|
+
transitionTo("transferring");
|
|
1827
|
+
resetWatchdog();
|
|
1828
|
+
if (fileList) {
|
|
1829
|
+
onFileStart?.({ fileIndex: 0, name, size: fileSize });
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
conn.send({ t: "ready" });
|
|
1833
|
+
} catch {
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
const metaEvt = { name, total };
|
|
1837
|
+
if (fileList) {
|
|
1838
|
+
metaEvt.fileCount = fileList.fileCount;
|
|
1839
|
+
metaEvt.files = fileList.files.map((f) => ({ name: f.name, size: f.size }));
|
|
1840
|
+
metaEvt.totalSize = fileList.totalSize;
|
|
1841
|
+
}
|
|
1842
|
+
if (autoReady) {
|
|
1843
|
+
if (!isStopped()) {
|
|
1844
|
+
onMeta?.(metaEvt);
|
|
1845
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1846
|
+
}
|
|
1847
|
+
sendReady();
|
|
1848
|
+
} else {
|
|
1849
|
+
metaEvt.sendReady = sendReady;
|
|
1850
|
+
if (!isStopped()) {
|
|
1851
|
+
onMeta?.(metaEvt);
|
|
1852
|
+
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
break;
|
|
983
1856
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1857
|
+
case "chunk":
|
|
1858
|
+
pendingChunk = msg;
|
|
1859
|
+
break;
|
|
1860
|
+
case "ping":
|
|
1861
|
+
try {
|
|
1862
|
+
conn.send({ t: "pong", timestamp: Date.now() });
|
|
1863
|
+
} catch {
|
|
1864
|
+
}
|
|
1865
|
+
break;
|
|
1866
|
+
case "file_end": {
|
|
1867
|
+
clearWatchdog();
|
|
1868
|
+
await writeQueue;
|
|
1869
|
+
const feIdx = msg.fileIndex;
|
|
1870
|
+
onFileEnd?.({ fileIndex: feIdx, receivedBytes: currentFileReceived });
|
|
1871
|
+
try {
|
|
1872
|
+
conn.send({ t: "file_end_ack", fileIndex: feIdx, received: currentFileReceived, size: currentFileReceived });
|
|
1873
|
+
} catch {
|
|
1874
|
+
}
|
|
1875
|
+
totalReceivedAllFiles += currentFileReceived;
|
|
1876
|
+
currentFileReceived = 0;
|
|
1877
|
+
resetWatchdog();
|
|
1878
|
+
break;
|
|
990
1879
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1880
|
+
case "end":
|
|
1881
|
+
clearWatchdog();
|
|
1882
|
+
await writeQueue;
|
|
1883
|
+
const finalReceived = fileList ? totalReceivedAllFiles + currentFileReceived : received;
|
|
1884
|
+
const finalTotal = fileList ? fileList.totalSize : total;
|
|
1885
|
+
if (finalTotal && finalReceived < finalTotal) {
|
|
1886
|
+
const err2 = new DropgateNetworkError(
|
|
1887
|
+
"Transfer ended before all data was received."
|
|
1888
|
+
);
|
|
1889
|
+
try {
|
|
1890
|
+
conn.send({ t: "error", message: err2.message });
|
|
1891
|
+
} catch {
|
|
1892
|
+
}
|
|
1893
|
+
throw err2;
|
|
1894
|
+
}
|
|
1895
|
+
try {
|
|
1896
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1899
|
+
safeComplete({ received: finalReceived, total: finalTotal });
|
|
1900
|
+
(async () => {
|
|
1901
|
+
for (let i = 0; i < 2; i++) {
|
|
1902
|
+
await sleep(P2P_END_ACK_RETRY_DELAY_MS);
|
|
1903
|
+
try {
|
|
1904
|
+
conn.send({ t: "end_ack", received: finalReceived, total: finalTotal });
|
|
1905
|
+
} catch {
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
})().catch(() => {
|
|
1910
|
+
});
|
|
1911
|
+
break;
|
|
1912
|
+
case "error":
|
|
1913
|
+
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
1914
|
+
case "cancelled":
|
|
1915
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1916
|
+
transitionTo("cancelled");
|
|
1917
|
+
onCancel?.({ cancelledBy: "sender", message: msg.reason });
|
|
1918
|
+
cleanup();
|
|
1919
|
+
break;
|
|
1000
1920
|
}
|
|
1921
|
+
} catch (err2) {
|
|
1922
|
+
safeError(err2);
|
|
1001
1923
|
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
throw new DropgateNetworkError("Download failed.", { cause: err });
|
|
1008
|
-
} finally {
|
|
1009
|
-
downloadCleanup();
|
|
1010
|
-
}
|
|
1011
|
-
progress({ phase: "complete", text: "Download complete!", percent: 100, processedBytes: receivedBytes, totalBytes });
|
|
1012
|
-
let data;
|
|
1013
|
-
if (collectData && dataChunks.length > 0) {
|
|
1014
|
-
const totalLength = dataChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1015
|
-
data = new Uint8Array(totalLength);
|
|
1016
|
-
let offset = 0;
|
|
1017
|
-
for (const chunk of dataChunks) {
|
|
1018
|
-
data.set(chunk, offset);
|
|
1019
|
-
offset += chunk.length;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
return {
|
|
1023
|
-
filename,
|
|
1024
|
-
receivedBytes,
|
|
1025
|
-
wasEncrypted: isEncrypted,
|
|
1026
|
-
...data ? { data } : {}
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
async attemptChunkUpload(url, fetchOptions, opts) {
|
|
1030
|
-
const {
|
|
1031
|
-
retries,
|
|
1032
|
-
backoffMs,
|
|
1033
|
-
maxBackoffMs,
|
|
1034
|
-
timeoutMs,
|
|
1035
|
-
signal,
|
|
1036
|
-
progress,
|
|
1037
|
-
chunkIndex,
|
|
1038
|
-
totalChunks,
|
|
1039
|
-
chunkSize,
|
|
1040
|
-
fileSizeBytes
|
|
1041
|
-
} = opts;
|
|
1042
|
-
let attemptsLeft = retries;
|
|
1043
|
-
let currentBackoff = backoffMs;
|
|
1044
|
-
const maxRetries = retries;
|
|
1045
|
-
while (true) {
|
|
1046
|
-
if (signal?.aborted) {
|
|
1047
|
-
throw signal.reason || new DropgateAbortError();
|
|
1924
|
+
});
|
|
1925
|
+
conn.on("close", () => {
|
|
1926
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1927
|
+
cleanup();
|
|
1928
|
+
return;
|
|
1048
1929
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if (res.ok) return;
|
|
1053
|
-
const text = await res.text().catch(() => "");
|
|
1054
|
-
const err = new DropgateProtocolError(
|
|
1055
|
-
`Chunk ${chunkIndex + 1} failed (HTTP ${res.status}).`,
|
|
1056
|
-
{
|
|
1057
|
-
details: { status: res.status, bodySnippet: text.slice(0, 120) }
|
|
1058
|
-
}
|
|
1059
|
-
);
|
|
1060
|
-
throw err;
|
|
1061
|
-
} catch (err) {
|
|
1930
|
+
if (state === "transferring") {
|
|
1931
|
+
transitionTo("cancelled");
|
|
1932
|
+
onCancel?.({ cancelledBy: "sender" });
|
|
1062
1933
|
cleanup();
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
if (signal?.aborted) {
|
|
1067
|
-
throw signal.reason || new DropgateAbortError();
|
|
1068
|
-
}
|
|
1069
|
-
if (attemptsLeft <= 0) {
|
|
1070
|
-
throw err instanceof DropgateError ? err : new DropgateNetworkError("Chunk upload failed.", { cause: err });
|
|
1071
|
-
}
|
|
1072
|
-
const attemptNumber = maxRetries - attemptsLeft + 1;
|
|
1073
|
-
const processedBytes = chunkIndex * chunkSize;
|
|
1074
|
-
const percent = chunkIndex / totalChunks * 100;
|
|
1075
|
-
let remaining = currentBackoff;
|
|
1076
|
-
const tick = 100;
|
|
1077
|
-
while (remaining > 0) {
|
|
1078
|
-
const secondsLeft = (remaining / 1e3).toFixed(1);
|
|
1079
|
-
progress({
|
|
1080
|
-
phase: "retry-wait",
|
|
1081
|
-
text: `Chunk upload failed. Retrying in ${secondsLeft}s... (${attemptNumber}/${maxRetries})`,
|
|
1082
|
-
percent,
|
|
1083
|
-
processedBytes,
|
|
1084
|
-
totalBytes: fileSizeBytes,
|
|
1085
|
-
chunkIndex,
|
|
1086
|
-
totalChunks
|
|
1087
|
-
});
|
|
1088
|
-
await sleep(Math.min(tick, remaining), signal);
|
|
1089
|
-
remaining -= tick;
|
|
1090
|
-
}
|
|
1091
|
-
progress({
|
|
1092
|
-
phase: "retry",
|
|
1093
|
-
text: `Chunk upload failed. Retrying now... (${attemptNumber}/${maxRetries})`,
|
|
1094
|
-
percent,
|
|
1095
|
-
processedBytes,
|
|
1096
|
-
totalBytes: fileSizeBytes,
|
|
1097
|
-
chunkIndex,
|
|
1098
|
-
totalChunks
|
|
1099
|
-
});
|
|
1100
|
-
attemptsLeft -= 1;
|
|
1101
|
-
currentBackoff = Math.min(currentBackoff * 2, maxBackoffMs);
|
|
1102
|
-
continue;
|
|
1103
|
-
} finally {
|
|
1934
|
+
} else if (state === "negotiating") {
|
|
1935
|
+
transitionTo("closed");
|
|
1104
1936
|
cleanup();
|
|
1937
|
+
onDisconnect?.();
|
|
1938
|
+
} else {
|
|
1939
|
+
safeError(new DropgateNetworkError("Sender disconnected before file details were received."));
|
|
1105
1940
|
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
return {
|
|
1944
|
+
peer,
|
|
1945
|
+
stop,
|
|
1946
|
+
getStatus: () => state,
|
|
1947
|
+
getBytesReceived: () => received,
|
|
1948
|
+
getTotalBytes: () => total,
|
|
1949
|
+
getSessionId: () => currentSessionId
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1109
1952
|
|
|
1110
|
-
// src/
|
|
1111
|
-
function
|
|
1112
|
-
|
|
1113
|
-
|
|
1953
|
+
// src/client/DropgateClient.ts
|
|
1954
|
+
function resolveServerToBaseUrl(server) {
|
|
1955
|
+
if (typeof server === "string") {
|
|
1956
|
+
return buildBaseUrl(parseServerUrl(server));
|
|
1957
|
+
}
|
|
1958
|
+
return buildBaseUrl(server);
|
|
1114
1959
|
}
|
|
1115
|
-
function
|
|
1116
|
-
|
|
1960
|
+
function estimateTotalUploadSizeBytes(fileSizeBytes, totalChunks, isEncrypted) {
|
|
1961
|
+
const base = Number(fileSizeBytes) || 0;
|
|
1962
|
+
if (!isEncrypted) return base;
|
|
1963
|
+
return base + (Number(totalChunks) || 0) * ENCRYPTION_OVERHEAD_PER_CHUNK;
|
|
1117
1964
|
}
|
|
1118
|
-
function
|
|
1119
|
-
const
|
|
1120
|
-
const
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1965
|
+
async function getServerInfo(opts) {
|
|
1966
|
+
const { server, timeoutMs = 5e3, signal, fetchFn: customFetch } = opts;
|
|
1967
|
+
const fetchFn = customFetch || getDefaultFetch();
|
|
1968
|
+
if (!fetchFn) {
|
|
1969
|
+
throw new DropgateValidationError("No fetch() implementation found.");
|
|
1970
|
+
}
|
|
1971
|
+
const baseUrl = resolveServerToBaseUrl(server);
|
|
1972
|
+
try {
|
|
1973
|
+
const { res, json } = await fetchJson(
|
|
1974
|
+
fetchFn,
|
|
1975
|
+
`${baseUrl}/api/info`,
|
|
1976
|
+
{
|
|
1977
|
+
method: "GET",
|
|
1978
|
+
timeoutMs,
|
|
1979
|
+
signal,
|
|
1980
|
+
headers: { Accept: "application/json" }
|
|
1981
|
+
}
|
|
1982
|
+
);
|
|
1983
|
+
if (res.ok && json && typeof json === "object" && "version" in json) {
|
|
1984
|
+
return { baseUrl, serverInfo: json };
|
|
1985
|
+
}
|
|
1986
|
+
throw new DropgateProtocolError(
|
|
1987
|
+
`Server info request failed (status ${res.status}).`
|
|
1988
|
+
);
|
|
1989
|
+
} catch (err2) {
|
|
1990
|
+
if (err2 instanceof DropgateError) throw err2;
|
|
1991
|
+
throw new DropgateNetworkError("Could not reach server /api/info.", {
|
|
1992
|
+
cause: err2
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
var DropgateClient = class {
|
|
1997
|
+
/**
|
|
1998
|
+
* Create a new DropgateClient instance.
|
|
1999
|
+
* @param opts - Client configuration options including server URL.
|
|
2000
|
+
* @throws {DropgateValidationError} If clientVersion or server is missing or invalid.
|
|
2001
|
+
*/
|
|
2002
|
+
constructor(opts) {
|
|
2003
|
+
/** Client version string for compatibility checking. */
|
|
2004
|
+
__publicField(this, "clientVersion");
|
|
2005
|
+
/** Chunk size in bytes for upload splitting. */
|
|
2006
|
+
__publicField(this, "chunkSize");
|
|
2007
|
+
/** Fetch implementation used for HTTP requests. */
|
|
2008
|
+
__publicField(this, "fetchFn");
|
|
2009
|
+
/** Crypto implementation for encryption operations. */
|
|
2010
|
+
__publicField(this, "cryptoObj");
|
|
2011
|
+
/** Base64 encoder/decoder for binary data. */
|
|
2012
|
+
__publicField(this, "base64");
|
|
2013
|
+
/** Resolved base URL (e.g. 'https://dropgate.link'). May change during HTTP fallback. */
|
|
2014
|
+
__publicField(this, "baseUrl");
|
|
2015
|
+
/** Whether to automatically retry with HTTP when HTTPS fails. */
|
|
2016
|
+
__publicField(this, "_fallbackToHttp");
|
|
2017
|
+
/** Cached compatibility result (null until first connect()). */
|
|
2018
|
+
__publicField(this, "_compat", null);
|
|
2019
|
+
/** In-flight connect promise to deduplicate concurrent calls. */
|
|
2020
|
+
__publicField(this, "_connectPromise", null);
|
|
2021
|
+
if (!opts || typeof opts.clientVersion !== "string") {
|
|
2022
|
+
throw new DropgateValidationError(
|
|
2023
|
+
"DropgateClient requires clientVersion (string)."
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
if (!opts.server) {
|
|
2027
|
+
throw new DropgateValidationError(
|
|
2028
|
+
"DropgateClient requires server (URL string or ServerTarget object)."
|
|
2029
|
+
);
|
|
1127
2030
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
2031
|
+
this.clientVersion = opts.clientVersion;
|
|
2032
|
+
this.chunkSize = Number.isFinite(opts.chunkSize) ? opts.chunkSize : DEFAULT_CHUNK_SIZE;
|
|
2033
|
+
const fetchFn = opts.fetchFn || getDefaultFetch();
|
|
2034
|
+
if (!fetchFn) {
|
|
2035
|
+
throw new DropgateValidationError("No fetch() implementation found.");
|
|
1131
2036
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
2037
|
+
this.fetchFn = fetchFn;
|
|
2038
|
+
const cryptoObj = opts.cryptoObj || getDefaultCrypto();
|
|
2039
|
+
if (!cryptoObj) {
|
|
2040
|
+
throw new DropgateValidationError("No crypto implementation found.");
|
|
2041
|
+
}
|
|
2042
|
+
this.cryptoObj = cryptoObj;
|
|
2043
|
+
this.base64 = opts.base64 || getDefaultBase64();
|
|
2044
|
+
this._fallbackToHttp = Boolean(opts.fallbackToHttp);
|
|
2045
|
+
this.baseUrl = resolveServerToBaseUrl(opts.server);
|
|
1137
2046
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
2047
|
+
/**
|
|
2048
|
+
* Get the server target (host, port, secure) derived from the current baseUrl.
|
|
2049
|
+
* Useful for passing to standalone functions that still need a ServerTarget.
|
|
2050
|
+
*/
|
|
2051
|
+
get serverTarget() {
|
|
2052
|
+
const url = new URL(this.baseUrl);
|
|
2053
|
+
return {
|
|
2054
|
+
host: url.hostname,
|
|
2055
|
+
port: url.port ? Number(url.port) : void 0,
|
|
2056
|
+
secure: url.protocol === "https:"
|
|
2057
|
+
};
|
|
1141
2058
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
secure,
|
|
1161
|
-
config: { iceServers },
|
|
1162
|
-
debug: 0
|
|
1163
|
-
};
|
|
1164
|
-
if (port) {
|
|
1165
|
-
peerOpts.port = port;
|
|
2059
|
+
/**
|
|
2060
|
+
* Connect to the server: fetch server info and check version compatibility.
|
|
2061
|
+
* Results are cached — subsequent calls return instantly without network requests.
|
|
2062
|
+
* Concurrent calls are deduplicated.
|
|
2063
|
+
*
|
|
2064
|
+
* @param opts - Optional timeout and abort signal.
|
|
2065
|
+
* @returns Compatibility result with server info.
|
|
2066
|
+
* @throws {DropgateNetworkError} If the server cannot be reached.
|
|
2067
|
+
* @throws {DropgateProtocolError} If the server returns an invalid response.
|
|
2068
|
+
*/
|
|
2069
|
+
async connect(opts) {
|
|
2070
|
+
if (this._compat) return this._compat;
|
|
2071
|
+
if (!this._connectPromise) {
|
|
2072
|
+
this._connectPromise = this._fetchAndCheckCompat(opts).finally(() => {
|
|
2073
|
+
this._connectPromise = null;
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return this._connectPromise;
|
|
1166
2077
|
}
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
let nextCode = code || codeGenerator();
|
|
1172
|
-
let peer = null;
|
|
1173
|
-
let lastError = null;
|
|
1174
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1175
|
-
onCode?.(nextCode, attempt);
|
|
2078
|
+
async _fetchAndCheckCompat(opts) {
|
|
2079
|
+
const { timeoutMs = 5e3, signal } = opts ?? {};
|
|
2080
|
+
let baseUrl = this.baseUrl;
|
|
2081
|
+
let serverInfo;
|
|
1176
2082
|
try {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
instance.destroy();
|
|
1183
|
-
} catch {
|
|
1184
|
-
}
|
|
1185
|
-
reject(err);
|
|
1186
|
-
});
|
|
2083
|
+
const result = await getServerInfo({
|
|
2084
|
+
server: baseUrl,
|
|
2085
|
+
timeoutMs,
|
|
2086
|
+
signal,
|
|
2087
|
+
fetchFn: this.fetchFn
|
|
1187
2088
|
});
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
2089
|
+
baseUrl = result.baseUrl;
|
|
2090
|
+
serverInfo = result.serverInfo;
|
|
2091
|
+
} catch (err2) {
|
|
2092
|
+
if (this._fallbackToHttp && this.baseUrl.startsWith("https://")) {
|
|
2093
|
+
const httpBaseUrl = this.baseUrl.replace("https://", "http://");
|
|
2094
|
+
try {
|
|
2095
|
+
const result = await getServerInfo({
|
|
2096
|
+
server: httpBaseUrl,
|
|
2097
|
+
timeoutMs,
|
|
2098
|
+
signal,
|
|
2099
|
+
fetchFn: this.fetchFn
|
|
2100
|
+
});
|
|
2101
|
+
this.baseUrl = httpBaseUrl;
|
|
2102
|
+
baseUrl = result.baseUrl;
|
|
2103
|
+
serverInfo = result.serverInfo;
|
|
2104
|
+
} catch {
|
|
2105
|
+
if (err2 instanceof DropgateError) throw err2;
|
|
2106
|
+
throw new DropgateNetworkError("Could not connect to the server.", { cause: err2 });
|
|
2107
|
+
}
|
|
2108
|
+
} else {
|
|
2109
|
+
if (err2 instanceof DropgateError) throw err2;
|
|
2110
|
+
throw new DropgateNetworkError("Could not connect to the server.", { cause: err2 });
|
|
2111
|
+
}
|
|
1192
2112
|
}
|
|
2113
|
+
const compat = this._checkVersionCompat(serverInfo);
|
|
2114
|
+
this._compat = { ...compat, serverInfo, baseUrl };
|
|
2115
|
+
return this._compat;
|
|
1193
2116
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
onProgress,
|
|
1225
|
-
onComplete,
|
|
1226
|
-
onError,
|
|
1227
|
-
onDisconnect,
|
|
1228
|
-
onCancel
|
|
1229
|
-
} = opts;
|
|
1230
|
-
if (!file) {
|
|
1231
|
-
throw new DropgateValidationError("File is missing.");
|
|
2117
|
+
/**
|
|
2118
|
+
* Pure version compatibility check (no network calls).
|
|
2119
|
+
*/
|
|
2120
|
+
_checkVersionCompat(serverInfo) {
|
|
2121
|
+
const serverVersion = String(serverInfo?.version || "0.0.0");
|
|
2122
|
+
const clientVersion = String(this.clientVersion || "0.0.0");
|
|
2123
|
+
const c = parseSemverMajorMinor(clientVersion);
|
|
2124
|
+
const s = parseSemverMajorMinor(serverVersion);
|
|
2125
|
+
if (c.major !== s.major) {
|
|
2126
|
+
return {
|
|
2127
|
+
compatible: false,
|
|
2128
|
+
clientVersion,
|
|
2129
|
+
serverVersion,
|
|
2130
|
+
message: `Incompatible versions. Client v${clientVersion}, Server v${serverVersion}${serverInfo?.name ? ` (${serverInfo.name})` : ""}.`
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
if (c.minor > s.minor) {
|
|
2134
|
+
return {
|
|
2135
|
+
compatible: true,
|
|
2136
|
+
clientVersion,
|
|
2137
|
+
serverVersion,
|
|
2138
|
+
message: `Client (v${clientVersion}) is newer than Server (v${serverVersion})${serverInfo?.name ? ` (${serverInfo.name})` : ""}. Some features may not work.`
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
return {
|
|
2142
|
+
compatible: true,
|
|
2143
|
+
clientVersion,
|
|
2144
|
+
serverVersion,
|
|
2145
|
+
message: `Server: v${serverVersion}, Client: v${clientVersion}${serverInfo?.name ? ` (${serverInfo.name})` : ""}.`
|
|
2146
|
+
};
|
|
1232
2147
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
2148
|
+
/**
|
|
2149
|
+
* Resolve a user-entered sharing code or URL via the server.
|
|
2150
|
+
* @param value - The sharing code or URL to resolve.
|
|
2151
|
+
* @param opts - Optional timeout and abort signal.
|
|
2152
|
+
* @returns The resolved share target information.
|
|
2153
|
+
* @throws {DropgateProtocolError} If the share lookup fails.
|
|
2154
|
+
*/
|
|
2155
|
+
async resolveShareTarget(value, opts) {
|
|
2156
|
+
const { timeoutMs = 5e3, signal } = opts ?? {};
|
|
2157
|
+
const compat = await this.connect(opts);
|
|
2158
|
+
if (!compat.compatible) {
|
|
2159
|
+
throw new DropgateValidationError(compat.message);
|
|
2160
|
+
}
|
|
2161
|
+
const { baseUrl } = compat;
|
|
2162
|
+
const { res, json } = await fetchJson(
|
|
2163
|
+
this.fetchFn,
|
|
2164
|
+
`${baseUrl}/api/resolve`,
|
|
2165
|
+
{
|
|
2166
|
+
method: "POST",
|
|
2167
|
+
timeoutMs,
|
|
2168
|
+
signal,
|
|
2169
|
+
headers: {
|
|
2170
|
+
"Content-Type": "application/json",
|
|
2171
|
+
Accept: "application/json"
|
|
2172
|
+
},
|
|
2173
|
+
body: JSON.stringify({ value })
|
|
2174
|
+
}
|
|
1236
2175
|
);
|
|
2176
|
+
if (!res.ok) {
|
|
2177
|
+
const msg = (json && typeof json === "object" && "error" in json ? json.error : null) || `Share lookup failed (status ${res.status}).`;
|
|
2178
|
+
throw new DropgateProtocolError(msg, { details: json });
|
|
2179
|
+
}
|
|
2180
|
+
return json || { valid: false, reason: "Unknown response." };
|
|
1237
2181
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
2182
|
+
/**
|
|
2183
|
+
* Fetch metadata for a single file from the server.
|
|
2184
|
+
* @param fileId - The file ID to fetch metadata for.
|
|
2185
|
+
* @param opts - Optional connection options (timeout, signal).
|
|
2186
|
+
* @returns File metadata including size, filename, and encryption status.
|
|
2187
|
+
* @throws {DropgateNetworkError} If the server cannot be reached.
|
|
2188
|
+
* @throws {DropgateProtocolError} If the file is not found or server returns an error.
|
|
2189
|
+
*/
|
|
2190
|
+
async getFileMetadata(fileId, opts) {
|
|
2191
|
+
if (!fileId || typeof fileId !== "string") {
|
|
2192
|
+
throw new DropgateValidationError("File ID is required.");
|
|
2193
|
+
}
|
|
2194
|
+
const { timeoutMs = 5e3, signal } = opts ?? {};
|
|
2195
|
+
const url = `${this.baseUrl}/api/file/${encodeURIComponent(fileId)}/meta`;
|
|
2196
|
+
const { res, json } = await fetchJson(this.fetchFn, url, {
|
|
2197
|
+
method: "GET",
|
|
2198
|
+
timeoutMs,
|
|
2199
|
+
signal
|
|
2200
|
+
});
|
|
2201
|
+
if (!res.ok) {
|
|
2202
|
+
const msg = (json && typeof json === "object" && "error" in json ? json.error : null) || `Failed to fetch file metadata (status ${res.status}).`;
|
|
2203
|
+
throw new DropgateProtocolError(msg, { details: json });
|
|
2204
|
+
}
|
|
2205
|
+
return json;
|
|
1241
2206
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
codeGenerator: finalCodeGenerator,
|
|
1258
|
-
maxAttempts,
|
|
1259
|
-
buildPeer,
|
|
1260
|
-
onCode
|
|
1261
|
-
});
|
|
1262
|
-
const sessionId = generateSessionId();
|
|
1263
|
-
let state = "listening";
|
|
1264
|
-
let activeConn = null;
|
|
1265
|
-
let sentBytes = 0;
|
|
1266
|
-
let heartbeatTimer = null;
|
|
1267
|
-
const reportProgress = (data) => {
|
|
1268
|
-
const safeTotal = Number.isFinite(data.total) && data.total > 0 ? data.total : file.size;
|
|
1269
|
-
const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);
|
|
1270
|
-
const percent = safeTotal ? safeReceived / safeTotal * 100 : 0;
|
|
1271
|
-
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
1272
|
-
};
|
|
1273
|
-
const safeError = (err) => {
|
|
1274
|
-
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1275
|
-
state = "closed";
|
|
1276
|
-
onError?.(err);
|
|
1277
|
-
cleanup();
|
|
1278
|
-
};
|
|
1279
|
-
const safeComplete = () => {
|
|
1280
|
-
if (state !== "finishing") return;
|
|
1281
|
-
state = "completed";
|
|
1282
|
-
onComplete?.();
|
|
1283
|
-
cleanup();
|
|
1284
|
-
};
|
|
1285
|
-
const cleanup = () => {
|
|
1286
|
-
if (heartbeatTimer) {
|
|
1287
|
-
clearInterval(heartbeatTimer);
|
|
1288
|
-
heartbeatTimer = null;
|
|
2207
|
+
/**
|
|
2208
|
+
* Fetch metadata for a bundle from the server and derive computed fields.
|
|
2209
|
+
* For sealed bundles, decrypts the manifest to extract file list.
|
|
2210
|
+
* Automatically derives totalSizeBytes and fileCount from the files array.
|
|
2211
|
+
* @param bundleId - The bundle ID to fetch metadata for.
|
|
2212
|
+
* @param keyB64 - Base64-encoded decryption key (required for encrypted bundles).
|
|
2213
|
+
* @param opts - Optional connection options (timeout, signal).
|
|
2214
|
+
* @returns Complete bundle metadata with all files and computed fields.
|
|
2215
|
+
* @throws {DropgateNetworkError} If the server cannot be reached.
|
|
2216
|
+
* @throws {DropgateProtocolError} If the bundle is not found or server returns an error.
|
|
2217
|
+
* @throws {DropgateValidationError} If decryption key is missing for encrypted bundle.
|
|
2218
|
+
*/
|
|
2219
|
+
async getBundleMetadata(bundleId, keyB64, opts) {
|
|
2220
|
+
if (!bundleId || typeof bundleId !== "string") {
|
|
2221
|
+
throw new DropgateValidationError("Bundle ID is required.");
|
|
1289
2222
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
2223
|
+
const { timeoutMs = 5e3, signal } = opts ?? {};
|
|
2224
|
+
const url = `${this.baseUrl}/api/bundle/${encodeURIComponent(bundleId)}/meta`;
|
|
2225
|
+
const { res, json } = await fetchJson(this.fetchFn, url, {
|
|
2226
|
+
method: "GET",
|
|
2227
|
+
timeoutMs,
|
|
2228
|
+
signal
|
|
2229
|
+
});
|
|
2230
|
+
if (!res.ok) {
|
|
2231
|
+
const msg = (json && typeof json === "object" && "error" in json ? json.error : null) || `Failed to fetch bundle metadata (status ${res.status}).`;
|
|
2232
|
+
throw new DropgateProtocolError(msg, { details: json });
|
|
2233
|
+
}
|
|
2234
|
+
const serverMeta = json;
|
|
2235
|
+
let files = [];
|
|
2236
|
+
if (serverMeta.sealed && serverMeta.encryptedManifest) {
|
|
2237
|
+
if (!keyB64) {
|
|
2238
|
+
throw new DropgateValidationError(
|
|
2239
|
+
"Decryption key (keyB64) is required for encrypted sealed bundles."
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
const key = await importKeyFromBase64(this.cryptoObj, keyB64);
|
|
2243
|
+
const encryptedBytes = this.base64.decode(serverMeta.encryptedManifest);
|
|
2244
|
+
const decryptedBuffer = await decryptChunk(this.cryptoObj, encryptedBytes, key);
|
|
2245
|
+
const manifestJson = new TextDecoder().decode(decryptedBuffer);
|
|
2246
|
+
const manifest = JSON.parse(manifestJson);
|
|
2247
|
+
files = manifest.files.map((f) => ({
|
|
2248
|
+
fileId: f.fileId,
|
|
2249
|
+
sizeBytes: f.sizeBytes,
|
|
2250
|
+
filename: f.name
|
|
2251
|
+
}));
|
|
2252
|
+
} else if (serverMeta.files) {
|
|
2253
|
+
files = serverMeta.files;
|
|
2254
|
+
} else {
|
|
2255
|
+
throw new DropgateProtocolError("Invalid bundle metadata: missing files or manifest.");
|
|
2256
|
+
}
|
|
2257
|
+
const totalSizeBytes = files.reduce((sum, f) => sum + (f.sizeBytes || 0), 0);
|
|
2258
|
+
const fileCount = files.length;
|
|
2259
|
+
return {
|
|
2260
|
+
isEncrypted: serverMeta.isEncrypted,
|
|
2261
|
+
sealed: serverMeta.sealed,
|
|
2262
|
+
encryptedManifest: serverMeta.encryptedManifest,
|
|
2263
|
+
files,
|
|
2264
|
+
totalSizeBytes,
|
|
2265
|
+
fileCount
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Validate file and upload settings against server capabilities.
|
|
2270
|
+
* @param opts - Validation options containing file, settings, and server info.
|
|
2271
|
+
* @returns True if validation passes.
|
|
2272
|
+
* @throws {DropgateValidationError} If any validation check fails.
|
|
2273
|
+
*/
|
|
2274
|
+
validateUploadInputs(opts) {
|
|
2275
|
+
const { files: rawFiles, lifetimeMs, encrypt, serverInfo } = opts;
|
|
2276
|
+
const caps = serverInfo?.capabilities?.upload;
|
|
2277
|
+
if (!caps || !caps.enabled) {
|
|
2278
|
+
throw new DropgateValidationError("Server does not support file uploads.");
|
|
1292
2279
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
2280
|
+
const files = Array.isArray(rawFiles) ? rawFiles : [rawFiles];
|
|
2281
|
+
if (files.length === 0) {
|
|
2282
|
+
throw new DropgateValidationError("At least one file is required.");
|
|
1296
2283
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
2284
|
+
for (let i = 0; i < files.length; i++) {
|
|
2285
|
+
const file = files[i];
|
|
2286
|
+
const fileSize = Number(file?.size || 0);
|
|
2287
|
+
if (!file || !Number.isFinite(fileSize) || fileSize <= 0) {
|
|
2288
|
+
throw new DropgateValidationError(`File at index ${i} is missing or invalid.`);
|
|
2289
|
+
}
|
|
2290
|
+
const maxMB = Number(caps.maxSizeMB);
|
|
2291
|
+
if (Number.isFinite(maxMB) && maxMB > 0) {
|
|
2292
|
+
const limitBytes = maxMB * 1e3 * 1e3;
|
|
2293
|
+
const validationChunkSize = Number.isFinite(caps.chunkSize) && caps.chunkSize > 0 ? caps.chunkSize : this.chunkSize;
|
|
2294
|
+
const totalChunks = Math.ceil(fileSize / validationChunkSize);
|
|
2295
|
+
const estimatedBytes = estimateTotalUploadSizeBytes(
|
|
2296
|
+
fileSize,
|
|
2297
|
+
totalChunks,
|
|
2298
|
+
Boolean(encrypt)
|
|
2299
|
+
);
|
|
2300
|
+
if (estimatedBytes > limitBytes) {
|
|
2301
|
+
const msg = encrypt ? `File at index ${i} too large once encryption overhead is included. Server limit: ${maxMB} MB.` : `File at index ${i} too large. Server limit: ${maxMB} MB.`;
|
|
2302
|
+
throw new DropgateValidationError(msg);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
1300
2305
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
2306
|
+
const maxHours = Number(caps.maxLifetimeHours);
|
|
2307
|
+
const lt = Number(lifetimeMs);
|
|
2308
|
+
if (!Number.isFinite(lt) || lt < 0 || !Number.isInteger(lt)) {
|
|
2309
|
+
throw new DropgateValidationError(
|
|
2310
|
+
"Invalid lifetime. Must be a non-negative integer (milliseconds)."
|
|
2311
|
+
);
|
|
1306
2312
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
2313
|
+
if (Number.isFinite(maxHours) && maxHours > 0) {
|
|
2314
|
+
const limitMs = Math.round(maxHours * 60 * 60 * 1e3);
|
|
2315
|
+
if (lt === 0) {
|
|
2316
|
+
throw new DropgateValidationError(
|
|
2317
|
+
`Server does not allow unlimited file lifetime. Max: ${maxHours} hours.`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
if (lt > limitMs) {
|
|
2321
|
+
throw new DropgateValidationError(
|
|
2322
|
+
`File lifetime too long. Server limit: ${maxHours} hours.`
|
|
2323
|
+
);
|
|
1319
2324
|
}
|
|
1320
|
-
} catch {
|
|
1321
2325
|
}
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
2326
|
+
if (encrypt && !caps.e2ee) {
|
|
2327
|
+
throw new DropgateValidationError(
|
|
2328
|
+
"End-to-end encryption is not supported on this server."
|
|
2329
|
+
);
|
|
1324
2330
|
}
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
2331
|
+
return true;
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Upload one or more files to the server with optional encryption.
|
|
2335
|
+
* Single files use the standard upload protocol.
|
|
2336
|
+
* Multiple files use the bundle protocol, grouping files under a single download link.
|
|
2337
|
+
*
|
|
2338
|
+
* @param opts - Upload options including file(s) and settings.
|
|
2339
|
+
* @returns Upload session with result promise and cancellation support.
|
|
2340
|
+
*/
|
|
2341
|
+
async uploadFiles(opts) {
|
|
2342
|
+
const {
|
|
2343
|
+
files: rawFiles,
|
|
2344
|
+
lifetimeMs,
|
|
2345
|
+
encrypt,
|
|
2346
|
+
maxDownloads,
|
|
2347
|
+
filenameOverrides,
|
|
2348
|
+
onProgress,
|
|
2349
|
+
onCancel,
|
|
2350
|
+
signal,
|
|
2351
|
+
timeouts = {},
|
|
2352
|
+
retry = {}
|
|
2353
|
+
} = opts;
|
|
2354
|
+
const files = Array.isArray(rawFiles) ? rawFiles : [rawFiles];
|
|
2355
|
+
if (files.length === 0) {
|
|
2356
|
+
throw new DropgateValidationError("At least one file is required.");
|
|
2357
|
+
}
|
|
2358
|
+
const internalController = signal ? null : new AbortController();
|
|
2359
|
+
const effectiveSignal = signal || internalController?.signal;
|
|
2360
|
+
let uploadState = "initializing";
|
|
2361
|
+
const currentUploadIds = [];
|
|
2362
|
+
const totalSizeBytes = files.reduce((sum, f) => sum + f.size, 0);
|
|
2363
|
+
const uploadPromise = (async () => {
|
|
2364
|
+
try {
|
|
2365
|
+
const progress = (evt) => {
|
|
2366
|
+
try {
|
|
2367
|
+
if (onProgress) onProgress(evt);
|
|
2368
|
+
} catch {
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
progress({ phase: "server-info", text: "Checking server...", percent: 0, processedBytes: 0, totalBytes: totalSizeBytes });
|
|
2372
|
+
const compat = await this.connect({
|
|
2373
|
+
timeoutMs: timeouts.serverInfoMs ?? 5e3,
|
|
2374
|
+
signal: effectiveSignal
|
|
2375
|
+
});
|
|
2376
|
+
const { baseUrl, serverInfo } = compat;
|
|
2377
|
+
progress({ phase: "server-compat", text: compat.message, percent: 0, processedBytes: 0, totalBytes: totalSizeBytes });
|
|
2378
|
+
if (!compat.compatible) {
|
|
2379
|
+
throw new DropgateValidationError(compat.message);
|
|
1336
2380
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
2381
|
+
const filenames = files.map((f, i) => filenameOverrides?.[i] ?? f.name ?? "file");
|
|
2382
|
+
const serverSupportsE2EE = Boolean(serverInfo?.capabilities?.upload?.e2ee);
|
|
2383
|
+
const effectiveEncrypt = encrypt ?? serverSupportsE2EE;
|
|
2384
|
+
if (!effectiveEncrypt) {
|
|
2385
|
+
for (const name of filenames) validatePlainFilename(name);
|
|
1340
2386
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
2387
|
+
this.validateUploadInputs({ files, lifetimeMs, encrypt: effectiveEncrypt, serverInfo });
|
|
2388
|
+
let cryptoKey = null;
|
|
2389
|
+
let keyB64 = null;
|
|
2390
|
+
const transmittedFilenames = [];
|
|
2391
|
+
if (effectiveEncrypt) {
|
|
2392
|
+
if (!this.cryptoObj?.subtle) {
|
|
2393
|
+
throw new DropgateValidationError(
|
|
2394
|
+
"Web Crypto API not available (crypto.subtle). Encryption requires a secure context (HTTPS or localhost)."
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
progress({ phase: "crypto", text: "Generating encryption key...", percent: 0, processedBytes: 0, totalBytes: totalSizeBytes });
|
|
2398
|
+
try {
|
|
2399
|
+
cryptoKey = await generateAesGcmKey(this.cryptoObj);
|
|
2400
|
+
keyB64 = await exportKeyBase64(this.cryptoObj, cryptoKey);
|
|
2401
|
+
for (const name of filenames) {
|
|
2402
|
+
transmittedFilenames.push(
|
|
2403
|
+
await encryptFilenameToBase64(this.cryptoObj, name, cryptoKey)
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
} catch (err2) {
|
|
2407
|
+
throw new DropgateError("Failed to prepare encryption.", { code: "CRYPTO_PREP_FAILED", cause: err2 });
|
|
2408
|
+
}
|
|
2409
|
+
} else {
|
|
2410
|
+
transmittedFilenames.push(...filenames);
|
|
1346
2411
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
2412
|
+
const serverChunkSize = serverInfo?.capabilities?.upload?.chunkSize;
|
|
2413
|
+
const effectiveChunkSize = Number.isFinite(serverChunkSize) && serverChunkSize > 0 ? serverChunkSize : this.chunkSize;
|
|
2414
|
+
const retries = Number.isFinite(retry.retries) ? retry.retries : 5;
|
|
2415
|
+
const baseBackoffMs = Number.isFinite(retry.backoffMs) ? retry.backoffMs : 1e3;
|
|
2416
|
+
const maxBackoffMs = Number.isFinite(retry.maxBackoffMs) ? retry.maxBackoffMs : 3e4;
|
|
2417
|
+
if (files.length === 1) {
|
|
2418
|
+
const file = files[0];
|
|
2419
|
+
const totalChunks = Math.ceil(file.size / effectiveChunkSize);
|
|
2420
|
+
const totalUploadSize = estimateTotalUploadSizeBytes(file.size, totalChunks, effectiveEncrypt);
|
|
2421
|
+
progress({ phase: "init", text: "Reserving server storage...", percent: 0, processedBytes: 0, totalBytes: file.size });
|
|
2422
|
+
const initRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init`, {
|
|
2423
|
+
method: "POST",
|
|
2424
|
+
timeoutMs: timeouts.initMs ?? 15e3,
|
|
2425
|
+
signal: effectiveSignal,
|
|
2426
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2427
|
+
body: JSON.stringify({
|
|
2428
|
+
filename: transmittedFilenames[0],
|
|
2429
|
+
lifetime: lifetimeMs,
|
|
2430
|
+
isEncrypted: effectiveEncrypt,
|
|
2431
|
+
totalSize: totalUploadSize,
|
|
2432
|
+
totalChunks,
|
|
2433
|
+
...maxDownloads !== void 0 ? { maxDownloads } : {}
|
|
2434
|
+
})
|
|
2435
|
+
});
|
|
2436
|
+
if (!initRes.res.ok) {
|
|
2437
|
+
const errorJson = initRes.json;
|
|
2438
|
+
throw new DropgateProtocolError(errorJson?.error || `Server initialisation failed: ${initRes.res.status}`, { details: initRes.json || initRes.text });
|
|
2439
|
+
}
|
|
2440
|
+
const uploadId = initRes.json?.uploadId;
|
|
2441
|
+
if (!uploadId) throw new DropgateProtocolError("Server did not return a valid uploadId.");
|
|
2442
|
+
currentUploadIds.push(uploadId);
|
|
2443
|
+
uploadState = "uploading";
|
|
2444
|
+
await this._uploadFileChunks({
|
|
2445
|
+
file,
|
|
2446
|
+
uploadId,
|
|
2447
|
+
cryptoKey,
|
|
2448
|
+
effectiveChunkSize,
|
|
2449
|
+
totalChunks,
|
|
2450
|
+
totalUploadSize,
|
|
2451
|
+
baseOffset: 0,
|
|
2452
|
+
totalBytesAllFiles: file.size,
|
|
2453
|
+
progress,
|
|
2454
|
+
signal: effectiveSignal,
|
|
2455
|
+
baseUrl,
|
|
2456
|
+
retries,
|
|
2457
|
+
backoffMs: baseBackoffMs,
|
|
2458
|
+
maxBackoffMs,
|
|
2459
|
+
chunkTimeoutMs: timeouts.chunkMs ?? 6e4
|
|
2460
|
+
});
|
|
2461
|
+
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: file.size, totalBytes: file.size });
|
|
2462
|
+
uploadState = "completing";
|
|
2463
|
+
const completeRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/complete`, {
|
|
2464
|
+
method: "POST",
|
|
2465
|
+
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
2466
|
+
signal: effectiveSignal,
|
|
2467
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2468
|
+
body: JSON.stringify({ uploadId })
|
|
2469
|
+
});
|
|
2470
|
+
if (!completeRes.res.ok) {
|
|
2471
|
+
const errorJson = completeRes.json;
|
|
2472
|
+
throw new DropgateProtocolError(errorJson?.error || "Finalisation failed.", { details: completeRes.json || completeRes.text });
|
|
2473
|
+
}
|
|
2474
|
+
const fileId = completeRes.json?.id;
|
|
2475
|
+
if (!fileId) throw new DropgateProtocolError("Server did not return a valid file id.");
|
|
2476
|
+
let downloadUrl2 = `${baseUrl}/${fileId}`;
|
|
2477
|
+
if (effectiveEncrypt && keyB64) downloadUrl2 += `#${keyB64}`;
|
|
2478
|
+
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: file.size, totalBytes: file.size });
|
|
2479
|
+
uploadState = "completed";
|
|
2480
|
+
return {
|
|
2481
|
+
downloadUrl: downloadUrl2,
|
|
2482
|
+
fileId,
|
|
2483
|
+
uploadId,
|
|
2484
|
+
baseUrl,
|
|
2485
|
+
...effectiveEncrypt && keyB64 ? { keyB64 } : {}
|
|
2486
|
+
};
|
|
1354
2487
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
2488
|
+
const fileManifest = files.map((f, i) => {
|
|
2489
|
+
const totalChunks = Math.ceil(f.size / effectiveChunkSize);
|
|
2490
|
+
const totalUploadSize = estimateTotalUploadSizeBytes(f.size, totalChunks, effectiveEncrypt);
|
|
2491
|
+
return { filename: transmittedFilenames[i], totalSize: totalUploadSize, totalChunks };
|
|
2492
|
+
});
|
|
2493
|
+
progress({ phase: "init", text: `Reserving server storage for ${files.length} files...`, percent: 0, processedBytes: 0, totalBytes: totalSizeBytes, totalFiles: files.length });
|
|
2494
|
+
const initBundleRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init-bundle`, {
|
|
2495
|
+
method: "POST",
|
|
2496
|
+
timeoutMs: timeouts.initMs ?? 15e3,
|
|
2497
|
+
signal: effectiveSignal,
|
|
2498
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2499
|
+
body: JSON.stringify({
|
|
2500
|
+
fileCount: files.length,
|
|
2501
|
+
files: fileManifest,
|
|
2502
|
+
lifetime: lifetimeMs,
|
|
2503
|
+
isEncrypted: effectiveEncrypt,
|
|
2504
|
+
...maxDownloads !== void 0 ? { maxDownloads } : {}
|
|
2505
|
+
})
|
|
2506
|
+
});
|
|
2507
|
+
if (!initBundleRes.res.ok) {
|
|
2508
|
+
const errorJson = initBundleRes.json;
|
|
2509
|
+
throw new DropgateProtocolError(errorJson?.error || `Bundle initialisation failed: ${initBundleRes.res.status}`, { details: initBundleRes.json || initBundleRes.text });
|
|
1358
2510
|
}
|
|
1359
|
-
|
|
2511
|
+
const bundleInitJson = initBundleRes.json;
|
|
2512
|
+
const bundleUploadId = bundleInitJson?.bundleUploadId;
|
|
2513
|
+
const fileUploadIds = bundleInitJson?.fileUploadIds;
|
|
2514
|
+
if (!bundleUploadId || !fileUploadIds || fileUploadIds.length !== files.length) {
|
|
2515
|
+
throw new DropgateProtocolError("Server did not return valid bundle upload IDs.");
|
|
2516
|
+
}
|
|
2517
|
+
currentUploadIds.push(...fileUploadIds);
|
|
2518
|
+
uploadState = "uploading";
|
|
2519
|
+
const fileResults = [];
|
|
2520
|
+
let cumulativeBytes = 0;
|
|
2521
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
2522
|
+
const file = files[fi];
|
|
2523
|
+
const uploadId = fileUploadIds[fi];
|
|
2524
|
+
const totalChunks = fileManifest[fi].totalChunks;
|
|
2525
|
+
const totalUploadSize = fileManifest[fi].totalSize;
|
|
2526
|
+
progress({
|
|
2527
|
+
phase: "file-start",
|
|
2528
|
+
text: `Uploading file ${fi + 1} of ${files.length}: ${filenames[fi]}`,
|
|
2529
|
+
percent: totalSizeBytes > 0 ? cumulativeBytes / totalSizeBytes * 100 : 0,
|
|
2530
|
+
processedBytes: cumulativeBytes,
|
|
2531
|
+
totalBytes: totalSizeBytes,
|
|
2532
|
+
fileIndex: fi,
|
|
2533
|
+
totalFiles: files.length,
|
|
2534
|
+
currentFileName: filenames[fi]
|
|
2535
|
+
});
|
|
2536
|
+
await this._uploadFileChunks({
|
|
2537
|
+
file,
|
|
2538
|
+
uploadId,
|
|
2539
|
+
cryptoKey,
|
|
2540
|
+
effectiveChunkSize,
|
|
2541
|
+
totalChunks,
|
|
2542
|
+
totalUploadSize,
|
|
2543
|
+
baseOffset: cumulativeBytes,
|
|
2544
|
+
totalBytesAllFiles: totalSizeBytes,
|
|
2545
|
+
progress,
|
|
2546
|
+
signal: effectiveSignal,
|
|
2547
|
+
baseUrl,
|
|
2548
|
+
retries,
|
|
2549
|
+
backoffMs: baseBackoffMs,
|
|
2550
|
+
maxBackoffMs,
|
|
2551
|
+
chunkTimeoutMs: timeouts.chunkMs ?? 6e4,
|
|
2552
|
+
fileIndex: fi,
|
|
2553
|
+
totalFiles: files.length,
|
|
2554
|
+
currentFileName: filenames[fi]
|
|
2555
|
+
});
|
|
2556
|
+
const completeRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/complete`, {
|
|
2557
|
+
method: "POST",
|
|
2558
|
+
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
2559
|
+
signal: effectiveSignal,
|
|
2560
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2561
|
+
body: JSON.stringify({ uploadId })
|
|
2562
|
+
});
|
|
2563
|
+
if (!completeRes.res.ok) {
|
|
2564
|
+
const errorJson = completeRes.json;
|
|
2565
|
+
throw new DropgateProtocolError(errorJson?.error || `File ${fi + 1} finalisation failed.`, { details: completeRes.json || completeRes.text });
|
|
2566
|
+
}
|
|
2567
|
+
const fileId = completeRes.json?.id;
|
|
2568
|
+
if (!fileId) throw new DropgateProtocolError(`Server did not return a valid file id for file ${fi + 1}.`);
|
|
2569
|
+
fileResults.push({ fileId, name: filenames[fi], size: file.size });
|
|
2570
|
+
cumulativeBytes += file.size;
|
|
2571
|
+
progress({
|
|
2572
|
+
phase: "file-complete",
|
|
2573
|
+
text: `File ${fi + 1} of ${files.length} uploaded.`,
|
|
2574
|
+
percent: totalSizeBytes > 0 ? cumulativeBytes / totalSizeBytes * 100 : 0,
|
|
2575
|
+
processedBytes: cumulativeBytes,
|
|
2576
|
+
totalBytes: totalSizeBytes,
|
|
2577
|
+
fileIndex: fi,
|
|
2578
|
+
totalFiles: files.length,
|
|
2579
|
+
currentFileName: filenames[fi]
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
progress({ phase: "complete", text: "Finalising bundle...", percent: 100, processedBytes: totalSizeBytes, totalBytes: totalSizeBytes });
|
|
2583
|
+
uploadState = "completing";
|
|
2584
|
+
let encryptedManifestB64;
|
|
2585
|
+
if (effectiveEncrypt && cryptoKey) {
|
|
2586
|
+
const manifest = JSON.stringify({
|
|
2587
|
+
files: fileResults.map((r) => ({
|
|
2588
|
+
fileId: r.fileId,
|
|
2589
|
+
name: r.name,
|
|
2590
|
+
sizeBytes: r.size
|
|
2591
|
+
}))
|
|
2592
|
+
});
|
|
2593
|
+
const manifestBytes = new TextEncoder().encode(manifest);
|
|
2594
|
+
const encryptedBlob = await encryptToBlob(this.cryptoObj, manifestBytes.buffer, cryptoKey);
|
|
2595
|
+
const encryptedBuffer = new Uint8Array(await encryptedBlob.arrayBuffer());
|
|
2596
|
+
encryptedManifestB64 = this.base64.encode(encryptedBuffer);
|
|
2597
|
+
}
|
|
2598
|
+
const completeBundleRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/complete-bundle`, {
|
|
2599
|
+
method: "POST",
|
|
2600
|
+
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
2601
|
+
signal: effectiveSignal,
|
|
2602
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2603
|
+
body: JSON.stringify({
|
|
2604
|
+
bundleUploadId,
|
|
2605
|
+
...encryptedManifestB64 ? { encryptedManifest: encryptedManifestB64 } : {}
|
|
2606
|
+
})
|
|
2607
|
+
});
|
|
2608
|
+
if (!completeBundleRes.res.ok) {
|
|
2609
|
+
const errorJson = completeBundleRes.json;
|
|
2610
|
+
throw new DropgateProtocolError(errorJson?.error || "Bundle finalisation failed.", { details: completeBundleRes.json || completeBundleRes.text });
|
|
2611
|
+
}
|
|
2612
|
+
const bundleId = completeBundleRes.json?.bundleId;
|
|
2613
|
+
if (!bundleId) throw new DropgateProtocolError("Server did not return a valid bundle id.");
|
|
2614
|
+
let downloadUrl = `${baseUrl}/b/${bundleId}`;
|
|
2615
|
+
if (effectiveEncrypt && keyB64) downloadUrl += `#${keyB64}`;
|
|
2616
|
+
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: totalSizeBytes, totalBytes: totalSizeBytes });
|
|
2617
|
+
uploadState = "completed";
|
|
2618
|
+
return {
|
|
2619
|
+
downloadUrl,
|
|
2620
|
+
bundleId,
|
|
2621
|
+
baseUrl,
|
|
2622
|
+
files: fileResults,
|
|
2623
|
+
...effectiveEncrypt && keyB64 ? { keyB64 } : {}
|
|
2624
|
+
};
|
|
2625
|
+
} catch (err2) {
|
|
2626
|
+
if (err2 instanceof Error && (err2.name === "AbortError" || err2.message?.includes("abort"))) {
|
|
2627
|
+
uploadState = "cancelled";
|
|
2628
|
+
onCancel?.();
|
|
2629
|
+
} else {
|
|
2630
|
+
uploadState = "error";
|
|
2631
|
+
}
|
|
2632
|
+
throw err2;
|
|
1360
2633
|
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
ackResolve = resolve;
|
|
1372
|
-
});
|
|
1373
|
-
conn.on("data", (data) => {
|
|
1374
|
-
if (!data || typeof data !== "object" || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
1375
|
-
return;
|
|
2634
|
+
})();
|
|
2635
|
+
const callCancelEndpoint = async (uploadId) => {
|
|
2636
|
+
try {
|
|
2637
|
+
await fetchJson(this.fetchFn, `${this.baseUrl}/upload/cancel`, {
|
|
2638
|
+
method: "POST",
|
|
2639
|
+
timeoutMs: 5e3,
|
|
2640
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2641
|
+
body: JSON.stringify({ uploadId })
|
|
2642
|
+
});
|
|
2643
|
+
} catch {
|
|
1376
2644
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
2645
|
+
};
|
|
2646
|
+
return {
|
|
2647
|
+
result: uploadPromise,
|
|
2648
|
+
cancel: (reason) => {
|
|
2649
|
+
if (uploadState === "completed" || uploadState === "cancelled") return;
|
|
2650
|
+
uploadState = "cancelled";
|
|
2651
|
+
for (const id of currentUploadIds) {
|
|
2652
|
+
callCancelEndpoint(id).catch(() => {
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
internalController?.abort(new DropgateAbortError(reason || "Upload cancelled by user."));
|
|
2656
|
+
},
|
|
2657
|
+
getStatus: () => uploadState
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Upload a single file's chunks to the server. Used internally by uploadFiles().
|
|
2662
|
+
*/
|
|
2663
|
+
async _uploadFileChunks(params) {
|
|
2664
|
+
const {
|
|
2665
|
+
file,
|
|
2666
|
+
uploadId,
|
|
2667
|
+
cryptoKey,
|
|
2668
|
+
effectiveChunkSize,
|
|
2669
|
+
totalChunks,
|
|
2670
|
+
baseOffset,
|
|
2671
|
+
totalBytesAllFiles,
|
|
2672
|
+
progress,
|
|
2673
|
+
signal,
|
|
2674
|
+
baseUrl,
|
|
2675
|
+
retries,
|
|
2676
|
+
backoffMs,
|
|
2677
|
+
maxBackoffMs,
|
|
2678
|
+
chunkTimeoutMs,
|
|
2679
|
+
fileIndex,
|
|
2680
|
+
totalFiles,
|
|
2681
|
+
currentFileName
|
|
2682
|
+
} = params;
|
|
2683
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
2684
|
+
if (signal?.aborted) {
|
|
2685
|
+
throw signal.reason || new DropgateAbortError();
|
|
1383
2686
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
2687
|
+
const start = i * effectiveChunkSize;
|
|
2688
|
+
const end = Math.min(start + effectiveChunkSize, file.size);
|
|
2689
|
+
const chunkSlice = file.slice(start, end);
|
|
2690
|
+
const processedBytes = baseOffset + start;
|
|
2691
|
+
const percent = totalBytesAllFiles > 0 ? processedBytes / totalBytesAllFiles * 100 : 0;
|
|
2692
|
+
progress({
|
|
2693
|
+
phase: "chunk",
|
|
2694
|
+
text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
|
|
2695
|
+
percent,
|
|
2696
|
+
processedBytes,
|
|
2697
|
+
totalBytes: totalBytesAllFiles,
|
|
2698
|
+
chunkIndex: i,
|
|
2699
|
+
totalChunks,
|
|
2700
|
+
...fileIndex !== void 0 ? { fileIndex, totalFiles, currentFileName } : {}
|
|
2701
|
+
});
|
|
2702
|
+
const chunkBuffer = await chunkSlice.arrayBuffer();
|
|
2703
|
+
let uploadBlob;
|
|
2704
|
+
if (cryptoKey) {
|
|
2705
|
+
uploadBlob = await encryptToBlob(this.cryptoObj, chunkBuffer, cryptoKey);
|
|
2706
|
+
} else {
|
|
2707
|
+
uploadBlob = new Blob([chunkBuffer]);
|
|
1387
2708
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
|
|
2709
|
+
if (uploadBlob.size > effectiveChunkSize + 1024) {
|
|
2710
|
+
throw new DropgateValidationError("Chunk too large (client-side). Check chunk size settings.");
|
|
2711
|
+
}
|
|
2712
|
+
const toHash = await uploadBlob.arrayBuffer();
|
|
2713
|
+
const hashHex = await sha256Hex(this.cryptoObj, toHash);
|
|
2714
|
+
await this._attemptChunkUpload(
|
|
2715
|
+
`${baseUrl}/upload/chunk`,
|
|
2716
|
+
{ method: "POST", headers: { "Content-Type": "application/octet-stream", "X-Upload-ID": uploadId, "X-Chunk-Index": String(i), "X-Chunk-Hash": hashHex }, body: uploadBlob },
|
|
2717
|
+
{ retries, backoffMs, maxBackoffMs, timeoutMs: chunkTimeoutMs, signal, progress, chunkIndex: i, totalChunks, chunkSize: effectiveChunkSize, fileSizeBytes: totalBytesAllFiles }
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Download one or more files from the server with optional decryption.
|
|
2723
|
+
*
|
|
2724
|
+
* For single files, use `fileId`. For bundles, use `bundleId`.
|
|
2725
|
+
* With `asZip: true` on bundles, streams a ZIP archive via `onData`.
|
|
2726
|
+
* Without `asZip`, delivers files individually via `onFileStart`/`onFileData`/`onFileEnd`.
|
|
2727
|
+
*
|
|
2728
|
+
* @param opts - Download options including file/bundle ID and optional key.
|
|
2729
|
+
* @returns Download result containing filename(s) and received bytes.
|
|
2730
|
+
*/
|
|
2731
|
+
async downloadFiles(opts) {
|
|
2732
|
+
const {
|
|
2733
|
+
fileId,
|
|
2734
|
+
bundleId,
|
|
2735
|
+
keyB64,
|
|
2736
|
+
asZip,
|
|
2737
|
+
zipFilename: _zipFilename,
|
|
2738
|
+
onProgress,
|
|
2739
|
+
onData,
|
|
2740
|
+
onFileStart,
|
|
2741
|
+
onFileData,
|
|
2742
|
+
onFileEnd,
|
|
2743
|
+
signal,
|
|
2744
|
+
timeoutMs = 6e4
|
|
2745
|
+
} = opts;
|
|
2746
|
+
const progress = (evt) => {
|
|
2747
|
+
try {
|
|
2748
|
+
if (onProgress) onProgress(evt);
|
|
2749
|
+
} catch {
|
|
1391
2750
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
2751
|
+
};
|
|
2752
|
+
if (!fileId && !bundleId) {
|
|
2753
|
+
throw new DropgateValidationError("Either fileId or bundleId is required.");
|
|
2754
|
+
}
|
|
2755
|
+
progress({ phase: "server-info", text: "Checking server...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
2756
|
+
const compat = await this.connect({ timeoutMs, signal });
|
|
2757
|
+
const { baseUrl } = compat;
|
|
2758
|
+
progress({ phase: "server-compat", text: compat.message, processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
2759
|
+
if (!compat.compatible) throw new DropgateValidationError(compat.message);
|
|
2760
|
+
if (fileId) {
|
|
2761
|
+
return this._downloadSingleFile({ fileId, keyB64, onProgress, onData, signal, timeoutMs, baseUrl, compat });
|
|
2762
|
+
}
|
|
2763
|
+
progress({ phase: "metadata", text: "Fetching bundle info...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
2764
|
+
let bundleMeta;
|
|
2765
|
+
try {
|
|
2766
|
+
bundleMeta = await this.getBundleMetadata(bundleId, keyB64, { timeoutMs, signal });
|
|
2767
|
+
} catch (err2) {
|
|
2768
|
+
if (err2 instanceof DropgateError) throw err2;
|
|
2769
|
+
if (err2 instanceof Error && err2.name === "AbortError") throw new DropgateAbortError("Download cancelled.");
|
|
2770
|
+
throw new DropgateNetworkError("Could not fetch bundle metadata.", { cause: err2 });
|
|
2771
|
+
}
|
|
2772
|
+
const isEncrypted = Boolean(bundleMeta.isEncrypted);
|
|
2773
|
+
const totalBytes = bundleMeta.totalSizeBytes || 0;
|
|
2774
|
+
let cryptoKey;
|
|
2775
|
+
const filenames = [];
|
|
2776
|
+
if (isEncrypted) {
|
|
2777
|
+
if (!keyB64) throw new DropgateValidationError("Decryption key is required for encrypted bundles.");
|
|
2778
|
+
if (!this.cryptoObj?.subtle) throw new DropgateValidationError("Web Crypto API not available for decryption.");
|
|
2779
|
+
try {
|
|
2780
|
+
cryptoKey = await importKeyFromBase64(this.cryptoObj, keyB64, this.base64);
|
|
2781
|
+
if (bundleMeta.sealed && bundleMeta.encryptedManifest) {
|
|
2782
|
+
const encryptedBytes = this.base64.decode(bundleMeta.encryptedManifest);
|
|
2783
|
+
const decryptedBuffer = await decryptChunk(this.cryptoObj, encryptedBytes, cryptoKey);
|
|
2784
|
+
const manifestJson = new TextDecoder().decode(decryptedBuffer);
|
|
2785
|
+
const manifest = JSON.parse(manifestJson);
|
|
2786
|
+
bundleMeta.files = manifest.files.map((f) => ({
|
|
2787
|
+
fileId: f.fileId,
|
|
2788
|
+
sizeBytes: f.sizeBytes,
|
|
2789
|
+
filename: f.name
|
|
2790
|
+
}));
|
|
2791
|
+
bundleMeta.fileCount = bundleMeta.files.length;
|
|
2792
|
+
for (const f of bundleMeta.files) {
|
|
2793
|
+
filenames.push(f.filename || "file");
|
|
2794
|
+
}
|
|
2795
|
+
} else {
|
|
2796
|
+
for (const f of bundleMeta.files) {
|
|
2797
|
+
filenames.push(await decryptFilenameFromBase64(this.cryptoObj, f.encryptedFilename, cryptoKey, this.base64));
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
} catch (err2) {
|
|
2801
|
+
throw new DropgateError("Failed to decrypt bundle manifest.", { code: "DECRYPT_MANIFEST_FAILED", cause: err2 });
|
|
1394
2802
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
2803
|
+
} else {
|
|
2804
|
+
for (const f of bundleMeta.files) {
|
|
2805
|
+
filenames.push(f.filename || "file");
|
|
1398
2806
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
2807
|
+
}
|
|
2808
|
+
let totalReceivedBytes = 0;
|
|
2809
|
+
if (asZip && onData) {
|
|
2810
|
+
const zipWriter = new StreamingZipWriter(onData);
|
|
2811
|
+
for (let fi = 0; fi < bundleMeta.files.length; fi++) {
|
|
2812
|
+
const fileMeta = bundleMeta.files[fi];
|
|
2813
|
+
const name = filenames[fi];
|
|
2814
|
+
progress({
|
|
2815
|
+
phase: "zipping",
|
|
2816
|
+
text: `Downloading ${name}...`,
|
|
2817
|
+
percent: totalBytes > 0 ? totalReceivedBytes / totalBytes * 100 : 0,
|
|
2818
|
+
processedBytes: totalReceivedBytes,
|
|
2819
|
+
totalBytes,
|
|
2820
|
+
fileIndex: fi,
|
|
2821
|
+
totalFiles: bundleMeta.files.length,
|
|
2822
|
+
currentFileName: name
|
|
2823
|
+
});
|
|
2824
|
+
zipWriter.startFile(name);
|
|
2825
|
+
const baseReceivedBytes = totalReceivedBytes;
|
|
2826
|
+
const bytesReceived = await this._streamFileIntoCallback(
|
|
2827
|
+
baseUrl,
|
|
2828
|
+
fileMeta.fileId,
|
|
2829
|
+
isEncrypted,
|
|
2830
|
+
cryptoKey,
|
|
2831
|
+
compat,
|
|
2832
|
+
signal,
|
|
2833
|
+
timeoutMs,
|
|
2834
|
+
(chunk) => {
|
|
2835
|
+
zipWriter.writeChunk(chunk);
|
|
2836
|
+
},
|
|
2837
|
+
(fileBytes) => {
|
|
2838
|
+
const current = baseReceivedBytes + fileBytes;
|
|
2839
|
+
progress({
|
|
2840
|
+
phase: "zipping",
|
|
2841
|
+
text: `Downloading ${name}...`,
|
|
2842
|
+
percent: totalBytes > 0 ? current / totalBytes * 100 : 0,
|
|
2843
|
+
processedBytes: current,
|
|
2844
|
+
totalBytes,
|
|
2845
|
+
fileIndex: fi,
|
|
2846
|
+
totalFiles: bundleMeta.files.length,
|
|
2847
|
+
currentFileName: name
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
);
|
|
2851
|
+
zipWriter.endFile();
|
|
2852
|
+
totalReceivedBytes += bytesReceived;
|
|
1404
2853
|
}
|
|
1405
|
-
|
|
1406
|
-
conn.on("open", async () => {
|
|
2854
|
+
await zipWriter.finalize();
|
|
1407
2855
|
try {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
size: file.size,
|
|
1414
|
-
mime: file.type || "application/octet-stream"
|
|
2856
|
+
await fetchJson(this.fetchFn, `${baseUrl}/api/bundle/${bundleId}/downloaded`, {
|
|
2857
|
+
method: "POST",
|
|
2858
|
+
timeoutMs: 5e3,
|
|
2859
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2860
|
+
body: "{}"
|
|
1415
2861
|
});
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
2862
|
+
} catch {
|
|
2863
|
+
}
|
|
2864
|
+
progress({ phase: "complete", text: "Download complete!", percent: 100, processedBytes: totalReceivedBytes, totalBytes });
|
|
2865
|
+
return { filenames, receivedBytes: totalReceivedBytes, wasEncrypted: isEncrypted };
|
|
2866
|
+
} else {
|
|
2867
|
+
const dataCallback = onFileData || onData;
|
|
2868
|
+
for (let fi = 0; fi < bundleMeta.files.length; fi++) {
|
|
2869
|
+
const fileMeta = bundleMeta.files[fi];
|
|
2870
|
+
const name = filenames[fi];
|
|
2871
|
+
progress({
|
|
2872
|
+
phase: "downloading",
|
|
2873
|
+
text: `Downloading ${name}...`,
|
|
2874
|
+
percent: totalBytes > 0 ? totalReceivedBytes / totalBytes * 100 : 0,
|
|
2875
|
+
processedBytes: totalReceivedBytes,
|
|
2876
|
+
totalBytes,
|
|
2877
|
+
fileIndex: fi,
|
|
2878
|
+
totalFiles: bundleMeta.files.length,
|
|
2879
|
+
currentFileName: name
|
|
2880
|
+
});
|
|
2881
|
+
onFileStart?.({ name, size: fileMeta.sizeBytes, index: fi });
|
|
2882
|
+
const baseReceivedBytes = totalReceivedBytes;
|
|
2883
|
+
const bytesReceived = await this._streamFileIntoCallback(
|
|
2884
|
+
baseUrl,
|
|
2885
|
+
fileMeta.fileId,
|
|
2886
|
+
isEncrypted,
|
|
2887
|
+
cryptoKey,
|
|
2888
|
+
compat,
|
|
2889
|
+
signal,
|
|
2890
|
+
timeoutMs,
|
|
2891
|
+
dataCallback ? (chunk) => dataCallback(chunk) : void 0,
|
|
2892
|
+
(fileBytes) => {
|
|
2893
|
+
const current = baseReceivedBytes + fileBytes;
|
|
2894
|
+
progress({
|
|
2895
|
+
phase: "downloading",
|
|
2896
|
+
text: `Downloading ${name}...`,
|
|
2897
|
+
percent: totalBytes > 0 ? current / totalBytes * 100 : 0,
|
|
2898
|
+
processedBytes: current,
|
|
2899
|
+
totalBytes,
|
|
2900
|
+
fileIndex: fi,
|
|
2901
|
+
totalFiles: bundleMeta.files.length,
|
|
2902
|
+
currentFileName: name
|
|
2903
|
+
});
|
|
1461
2904
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
conn.send({ t: "end" });
|
|
1466
|
-
const ackTimeoutMs = Number.isFinite(endAckTimeoutMs) ? Math.max(endAckTimeoutMs, Math.ceil(file.size / (1024 * 1024)) * 1e3) : null;
|
|
1467
|
-
const ackResult = await Promise.race([
|
|
1468
|
-
ackPromise,
|
|
1469
|
-
sleep(ackTimeoutMs || 15e3).catch(() => null)
|
|
1470
|
-
]);
|
|
1471
|
-
if (isStopped()) return;
|
|
1472
|
-
if (!ackResult || typeof ackResult !== "object") {
|
|
1473
|
-
throw new DropgateNetworkError("Receiver did not confirm completion.");
|
|
1474
|
-
}
|
|
1475
|
-
const ackData = ackResult;
|
|
1476
|
-
const ackTotal = Number(ackData.total) || file.size;
|
|
1477
|
-
const ackReceived = Number(ackData.received) || 0;
|
|
1478
|
-
if (ackTotal && ackReceived < ackTotal) {
|
|
1479
|
-
throw new DropgateNetworkError("Receiver reported an incomplete transfer.");
|
|
1480
|
-
}
|
|
1481
|
-
reportProgress({ received: ackReceived || ackTotal, total: ackTotal });
|
|
1482
|
-
safeComplete();
|
|
1483
|
-
} catch (err) {
|
|
1484
|
-
safeError(err);
|
|
2905
|
+
);
|
|
2906
|
+
onFileEnd?.({ name, index: fi });
|
|
2907
|
+
totalReceivedBytes += bytesReceived;
|
|
1485
2908
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
2909
|
+
progress({ phase: "complete", text: "Download complete!", percent: 100, processedBytes: totalReceivedBytes, totalBytes });
|
|
2910
|
+
return { filenames, receivedBytes: totalReceivedBytes, wasEncrypted: isEncrypted };
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Download a single file, handling encryption/decryption internally.
|
|
2915
|
+
* Preserves the original downloadFile() behavior.
|
|
2916
|
+
*/
|
|
2917
|
+
async _downloadSingleFile(params) {
|
|
2918
|
+
const { fileId, keyB64, onProgress, onData, signal, timeoutMs, baseUrl, compat } = params;
|
|
2919
|
+
const progress = (evt) => {
|
|
2920
|
+
try {
|
|
2921
|
+
if (onProgress) onProgress(evt);
|
|
2922
|
+
} catch {
|
|
1494
2923
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
2924
|
+
};
|
|
2925
|
+
progress({ phase: "metadata", text: "Fetching file info...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
2926
|
+
let metadata;
|
|
2927
|
+
try {
|
|
2928
|
+
metadata = await this.getFileMetadata(fileId, { timeoutMs, signal });
|
|
2929
|
+
} catch (err2) {
|
|
2930
|
+
if (err2 instanceof DropgateError) throw err2;
|
|
2931
|
+
if (err2 instanceof Error && err2.name === "AbortError") throw new DropgateAbortError("Download cancelled.");
|
|
2932
|
+
throw new DropgateNetworkError("Could not fetch file metadata.", { cause: err2 });
|
|
2933
|
+
}
|
|
2934
|
+
const isEncrypted = Boolean(metadata.isEncrypted);
|
|
2935
|
+
const totalBytes = metadata.sizeBytes || 0;
|
|
2936
|
+
if (!onData && totalBytes > MAX_IN_MEMORY_DOWNLOAD_BYTES) {
|
|
2937
|
+
const sizeMB = Math.round(totalBytes / (1024 * 1024));
|
|
2938
|
+
const limitMB = Math.round(MAX_IN_MEMORY_DOWNLOAD_BYTES / (1024 * 1024));
|
|
2939
|
+
throw new DropgateValidationError(
|
|
2940
|
+
`File is too large (${sizeMB}MB) to download without streaming. Provide an onData callback to stream files larger than ${limitMB}MB.`
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
2943
|
+
let filename;
|
|
2944
|
+
let cryptoKey;
|
|
2945
|
+
if (isEncrypted) {
|
|
2946
|
+
if (!keyB64) throw new DropgateValidationError("Decryption key is required for encrypted files.");
|
|
2947
|
+
if (!this.cryptoObj?.subtle) throw new DropgateValidationError("Web Crypto API not available for decryption.");
|
|
2948
|
+
progress({ phase: "decrypting", text: "Preparing decryption...", processedBytes: 0, totalBytes: 0, percent: 0 });
|
|
2949
|
+
try {
|
|
2950
|
+
cryptoKey = await importKeyFromBase64(this.cryptoObj, keyB64, this.base64);
|
|
2951
|
+
filename = await decryptFilenameFromBase64(this.cryptoObj, metadata.encryptedFilename, cryptoKey, this.base64);
|
|
2952
|
+
} catch (err2) {
|
|
2953
|
+
throw new DropgateError("Failed to decrypt filename.", { code: "DECRYPT_FILENAME_FAILED", cause: err2 });
|
|
1504
2954
|
}
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
return {
|
|
1508
|
-
peer,
|
|
1509
|
-
code,
|
|
1510
|
-
sessionId,
|
|
1511
|
-
stop,
|
|
1512
|
-
getStatus: () => state,
|
|
1513
|
-
getBytesSent: () => sentBytes,
|
|
1514
|
-
getConnectedPeerId: () => {
|
|
1515
|
-
if (!activeConn) return null;
|
|
1516
|
-
return activeConn.peer || null;
|
|
2955
|
+
} else {
|
|
2956
|
+
filename = metadata.filename || "file";
|
|
1517
2957
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
}
|
|
1546
|
-
if (!Peer) {
|
|
1547
|
-
throw new DropgateValidationError(
|
|
1548
|
-
"PeerJS Peer constructor is required. Install peerjs and pass it as the Peer option."
|
|
2958
|
+
progress({ phase: "downloading", text: "Starting download...", percent: 0, processedBytes: 0, totalBytes });
|
|
2959
|
+
const dataChunks = [];
|
|
2960
|
+
const collectData = !onData;
|
|
2961
|
+
const receivedBytes = await this._streamFileIntoCallback(
|
|
2962
|
+
baseUrl,
|
|
2963
|
+
fileId,
|
|
2964
|
+
isEncrypted,
|
|
2965
|
+
cryptoKey,
|
|
2966
|
+
compat,
|
|
2967
|
+
signal,
|
|
2968
|
+
timeoutMs,
|
|
2969
|
+
async (chunk) => {
|
|
2970
|
+
if (collectData) {
|
|
2971
|
+
dataChunks.push(chunk);
|
|
2972
|
+
} else {
|
|
2973
|
+
await onData(chunk);
|
|
2974
|
+
}
|
|
2975
|
+
},
|
|
2976
|
+
(bytes) => {
|
|
2977
|
+
progress({
|
|
2978
|
+
phase: "downloading",
|
|
2979
|
+
text: "Downloading...",
|
|
2980
|
+
percent: totalBytes > 0 ? bytes / totalBytes * 100 : 0,
|
|
2981
|
+
processedBytes: bytes,
|
|
2982
|
+
totalBytes
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
1549
2985
|
);
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
{ peerjsPath, iceServers },
|
|
1561
|
-
p2pCaps
|
|
1562
|
-
);
|
|
1563
|
-
const peerOpts = buildPeerOptions({
|
|
1564
|
-
host,
|
|
1565
|
-
port,
|
|
1566
|
-
peerjsPath: finalPath,
|
|
1567
|
-
secure,
|
|
1568
|
-
iceServers: finalIceServers
|
|
1569
|
-
});
|
|
1570
|
-
const peer = new Peer(void 0, peerOpts);
|
|
1571
|
-
let state = "initializing";
|
|
1572
|
-
let total = 0;
|
|
1573
|
-
let received = 0;
|
|
1574
|
-
let currentSessionId = null;
|
|
1575
|
-
let lastProgressSentAt = 0;
|
|
1576
|
-
const progressIntervalMs = 120;
|
|
1577
|
-
let writeQueue = Promise.resolve();
|
|
1578
|
-
let watchdogTimer = null;
|
|
1579
|
-
let activeConn = null;
|
|
1580
|
-
const resetWatchdog = () => {
|
|
1581
|
-
if (watchdogTimeoutMs <= 0) return;
|
|
1582
|
-
if (watchdogTimer) {
|
|
1583
|
-
clearTimeout(watchdogTimer);
|
|
2986
|
+
progress({ phase: "complete", text: "Download complete!", percent: 100, processedBytes: receivedBytes, totalBytes });
|
|
2987
|
+
let data;
|
|
2988
|
+
if (collectData && dataChunks.length > 0) {
|
|
2989
|
+
const totalLength = dataChunks.reduce((sum, c) => sum + c.length, 0);
|
|
2990
|
+
data = new Uint8Array(totalLength);
|
|
2991
|
+
let offset = 0;
|
|
2992
|
+
for (const c of dataChunks) {
|
|
2993
|
+
data.set(c, offset);
|
|
2994
|
+
offset += c.length;
|
|
2995
|
+
}
|
|
1584
2996
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
2997
|
+
return {
|
|
2998
|
+
filename,
|
|
2999
|
+
receivedBytes,
|
|
3000
|
+
wasEncrypted: isEncrypted,
|
|
3001
|
+
...data ? { data } : {}
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* Stream a single file's content into a callback, handling decryption if needed.
|
|
3006
|
+
* Returns total bytes received from the network (encrypted size).
|
|
3007
|
+
*/
|
|
3008
|
+
async _streamFileIntoCallback(baseUrl, fileId, isEncrypted, cryptoKey, compat, signal, timeoutMs, onChunk, onBytesReceived) {
|
|
3009
|
+
const { signal: downloadSignal, cleanup: downloadCleanup } = makeAbortSignal(signal, timeoutMs);
|
|
3010
|
+
let receivedBytes = 0;
|
|
3011
|
+
try {
|
|
3012
|
+
const downloadRes = await this.fetchFn(`${baseUrl}/api/file/${fileId}`, {
|
|
3013
|
+
method: "GET",
|
|
3014
|
+
signal: downloadSignal
|
|
3015
|
+
});
|
|
3016
|
+
if (!downloadRes.ok) throw new DropgateProtocolError(`Download failed (status ${downloadRes.status}).`);
|
|
3017
|
+
if (!downloadRes.body) throw new DropgateProtocolError("Streaming response not available.");
|
|
3018
|
+
const reader = downloadRes.body.getReader();
|
|
3019
|
+
if (isEncrypted && cryptoKey) {
|
|
3020
|
+
const downloadChunkSize = Number.isFinite(compat.serverInfo?.capabilities?.upload?.chunkSize) && compat.serverInfo.capabilities.upload.chunkSize > 0 ? compat.serverInfo.capabilities.upload.chunkSize : this.chunkSize;
|
|
3021
|
+
const ENCRYPTED_CHUNK_SIZE = downloadChunkSize + ENCRYPTION_OVERHEAD_PER_CHUNK;
|
|
3022
|
+
const pendingChunks = [];
|
|
3023
|
+
let pendingLength = 0;
|
|
3024
|
+
const flushPending = () => {
|
|
3025
|
+
if (pendingChunks.length === 0) return new Uint8Array(0);
|
|
3026
|
+
if (pendingChunks.length === 1) {
|
|
3027
|
+
const result2 = pendingChunks[0];
|
|
3028
|
+
pendingChunks.length = 0;
|
|
3029
|
+
pendingLength = 0;
|
|
3030
|
+
return result2;
|
|
3031
|
+
}
|
|
3032
|
+
const result = new Uint8Array(pendingLength);
|
|
3033
|
+
let offset = 0;
|
|
3034
|
+
for (const chunk of pendingChunks) {
|
|
3035
|
+
result.set(chunk, offset);
|
|
3036
|
+
offset += chunk.length;
|
|
3037
|
+
}
|
|
3038
|
+
pendingChunks.length = 0;
|
|
3039
|
+
pendingLength = 0;
|
|
3040
|
+
return result;
|
|
3041
|
+
};
|
|
3042
|
+
while (true) {
|
|
3043
|
+
if (signal?.aborted) throw new DropgateAbortError("Download cancelled.");
|
|
3044
|
+
const { done, value } = await reader.read();
|
|
3045
|
+
if (done) break;
|
|
3046
|
+
pendingChunks.push(value);
|
|
3047
|
+
pendingLength += value.length;
|
|
3048
|
+
receivedBytes += value.length;
|
|
3049
|
+
if (onBytesReceived) onBytesReceived(receivedBytes);
|
|
3050
|
+
while (pendingLength >= ENCRYPTED_CHUNK_SIZE) {
|
|
3051
|
+
const buffer = flushPending();
|
|
3052
|
+
const encryptedChunk = buffer.subarray(0, ENCRYPTED_CHUNK_SIZE);
|
|
3053
|
+
if (buffer.length > ENCRYPTED_CHUNK_SIZE) {
|
|
3054
|
+
pendingChunks.push(buffer.subarray(ENCRYPTED_CHUNK_SIZE));
|
|
3055
|
+
pendingLength = buffer.length - ENCRYPTED_CHUNK_SIZE;
|
|
3056
|
+
}
|
|
3057
|
+
const decryptedBuffer = await decryptChunk(this.cryptoObj, encryptedChunk, cryptoKey);
|
|
3058
|
+
if (onChunk) await onChunk(new Uint8Array(decryptedBuffer));
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (pendingLength > 0) {
|
|
3062
|
+
const buffer = flushPending();
|
|
3063
|
+
const decryptedBuffer = await decryptChunk(this.cryptoObj, buffer, cryptoKey);
|
|
3064
|
+
if (onChunk) await onChunk(new Uint8Array(decryptedBuffer));
|
|
3065
|
+
}
|
|
3066
|
+
} else {
|
|
3067
|
+
while (true) {
|
|
3068
|
+
if (signal?.aborted) throw new DropgateAbortError("Download cancelled.");
|
|
3069
|
+
const { done, value } = await reader.read();
|
|
3070
|
+
if (done) break;
|
|
3071
|
+
receivedBytes += value.length;
|
|
3072
|
+
if (onBytesReceived) onBytesReceived(receivedBytes);
|
|
3073
|
+
if (onChunk) await onChunk(value);
|
|
3074
|
+
}
|
|
1588
3075
|
}
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
}
|
|
1596
|
-
};
|
|
1597
|
-
const safeError = (err) => {
|
|
1598
|
-
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1599
|
-
state = "closed";
|
|
1600
|
-
onError?.(err);
|
|
1601
|
-
cleanup();
|
|
1602
|
-
};
|
|
1603
|
-
const safeComplete = (completeData) => {
|
|
1604
|
-
if (state !== "transferring") return;
|
|
1605
|
-
state = "completed";
|
|
1606
|
-
onComplete?.(completeData);
|
|
1607
|
-
cleanup();
|
|
1608
|
-
};
|
|
1609
|
-
const cleanup = () => {
|
|
1610
|
-
clearWatchdog();
|
|
1611
|
-
if (typeof window !== "undefined") {
|
|
1612
|
-
window.removeEventListener("beforeunload", handleUnload);
|
|
3076
|
+
} catch (err2) {
|
|
3077
|
+
if (err2 instanceof DropgateError) throw err2;
|
|
3078
|
+
if (err2 instanceof Error && err2.name === "AbortError") throw new DropgateAbortError("Download cancelled.");
|
|
3079
|
+
throw new DropgateNetworkError("Download failed.", { cause: err2 });
|
|
3080
|
+
} finally {
|
|
3081
|
+
downloadCleanup();
|
|
1613
3082
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
3083
|
+
return receivedBytes;
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Start a P2P send session. Connects to the signalling server and waits for a receiver.
|
|
3087
|
+
*
|
|
3088
|
+
* Server info, peerjsPath, iceServers, and cryptoObj are provided automatically
|
|
3089
|
+
* from the client's cached server info and configuration.
|
|
3090
|
+
*
|
|
3091
|
+
* @param opts - P2P send options (file, Peer constructor, callbacks, tuning).
|
|
3092
|
+
* @returns P2P send session with control methods.
|
|
3093
|
+
* @throws {DropgateValidationError} If P2P is not enabled on the server.
|
|
3094
|
+
* @throws {DropgateNetworkError} If the signalling server cannot be reached.
|
|
3095
|
+
*/
|
|
3096
|
+
async p2pSend(opts) {
|
|
3097
|
+
const compat = await this.connect();
|
|
3098
|
+
if (!compat.compatible) {
|
|
3099
|
+
throw new DropgateValidationError(compat.message);
|
|
1617
3100
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
} catch {
|
|
3101
|
+
const { serverInfo } = compat;
|
|
3102
|
+
const p2pCaps = serverInfo?.capabilities?.p2p;
|
|
3103
|
+
if (!p2pCaps?.enabled) {
|
|
3104
|
+
throw new DropgateValidationError("Direct transfer is disabled on this server.");
|
|
1623
3105
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
3106
|
+
const { host, port, secure } = this.serverTarget;
|
|
3107
|
+
const { path: peerjsPath, iceServers } = resolvePeerConfig({}, p2pCaps);
|
|
3108
|
+
return startP2PSend({
|
|
3109
|
+
...opts,
|
|
3110
|
+
host,
|
|
3111
|
+
port,
|
|
3112
|
+
secure,
|
|
3113
|
+
peerjsPath,
|
|
3114
|
+
iceServers,
|
|
3115
|
+
serverInfo,
|
|
3116
|
+
cryptoObj: this.cryptoObj
|
|
3117
|
+
});
|
|
1628
3118
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
3119
|
+
/**
|
|
3120
|
+
* Start a P2P receive session. Connects to a sender via their sharing code.
|
|
3121
|
+
*
|
|
3122
|
+
* Server info, peerjsPath, and iceServers are provided automatically
|
|
3123
|
+
* from the client's cached server info.
|
|
3124
|
+
*
|
|
3125
|
+
* @param opts - P2P receive options (code, Peer constructor, callbacks, tuning).
|
|
3126
|
+
* @returns P2P receive session with control methods.
|
|
3127
|
+
* @throws {DropgateValidationError} If P2P is not enabled on the server.
|
|
3128
|
+
* @throws {DropgateNetworkError} If the signalling server cannot be reached.
|
|
3129
|
+
*/
|
|
3130
|
+
async p2pReceive(opts) {
|
|
3131
|
+
const compat = await this.connect();
|
|
3132
|
+
if (!compat.compatible) {
|
|
3133
|
+
throw new DropgateValidationError(compat.message);
|
|
1638
3134
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
3135
|
+
const { serverInfo } = compat;
|
|
3136
|
+
const p2pCaps = serverInfo?.capabilities?.p2p;
|
|
3137
|
+
if (!p2pCaps?.enabled) {
|
|
3138
|
+
throw new DropgateValidationError("Direct transfer is disabled on this server.");
|
|
1641
3139
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
state = "negotiating";
|
|
1653
|
-
onStatus?.({ phase: "connected", message: "Waiting for file details..." });
|
|
3140
|
+
const { host, port, secure } = this.serverTarget;
|
|
3141
|
+
const { path: peerjsPath, iceServers } = resolvePeerConfig({}, p2pCaps);
|
|
3142
|
+
return startP2PReceive({
|
|
3143
|
+
...opts,
|
|
3144
|
+
host,
|
|
3145
|
+
port,
|
|
3146
|
+
secure,
|
|
3147
|
+
peerjsPath,
|
|
3148
|
+
iceServers,
|
|
3149
|
+
serverInfo
|
|
1654
3150
|
});
|
|
1655
|
-
|
|
3151
|
+
}
|
|
3152
|
+
async _attemptChunkUpload(url, fetchOptions, opts) {
|
|
3153
|
+
const {
|
|
3154
|
+
retries,
|
|
3155
|
+
backoffMs,
|
|
3156
|
+
maxBackoffMs,
|
|
3157
|
+
timeoutMs,
|
|
3158
|
+
signal,
|
|
3159
|
+
progress,
|
|
3160
|
+
chunkIndex,
|
|
3161
|
+
totalChunks,
|
|
3162
|
+
chunkSize,
|
|
3163
|
+
fileSizeBytes
|
|
3164
|
+
} = opts;
|
|
3165
|
+
let attemptsLeft = retries;
|
|
3166
|
+
let currentBackoff = backoffMs;
|
|
3167
|
+
const maxRetries = retries;
|
|
3168
|
+
while (true) {
|
|
3169
|
+
if (signal?.aborted) {
|
|
3170
|
+
throw signal.reason || new DropgateAbortError();
|
|
3171
|
+
}
|
|
3172
|
+
const { signal: s, cleanup } = makeAbortSignal(signal, timeoutMs);
|
|
1656
3173
|
try {
|
|
1657
|
-
|
|
1658
|
-
if (
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
} catch {
|
|
1665
|
-
}
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
if (msg.sessionId) {
|
|
1669
|
-
currentSessionId = msg.sessionId;
|
|
1670
|
-
}
|
|
1671
|
-
const name = String(msg.name || "file");
|
|
1672
|
-
total = Number(msg.size) || 0;
|
|
1673
|
-
received = 0;
|
|
1674
|
-
writeQueue = Promise.resolve();
|
|
1675
|
-
const sendReady = () => {
|
|
1676
|
-
state = "transferring";
|
|
1677
|
-
resetWatchdog();
|
|
1678
|
-
try {
|
|
1679
|
-
conn.send({ t: "ready" });
|
|
1680
|
-
} catch {
|
|
1681
|
-
}
|
|
1682
|
-
};
|
|
1683
|
-
if (autoReady) {
|
|
1684
|
-
onMeta?.({ name, total });
|
|
1685
|
-
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1686
|
-
sendReady();
|
|
1687
|
-
} else {
|
|
1688
|
-
onMeta?.({ name, total, sendReady });
|
|
1689
|
-
onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });
|
|
1690
|
-
}
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
if (msg.t === "ping") {
|
|
1694
|
-
try {
|
|
1695
|
-
conn.send({ t: "pong" });
|
|
1696
|
-
} catch {
|
|
1697
|
-
}
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
if (msg.t === "end") {
|
|
1701
|
-
clearWatchdog();
|
|
1702
|
-
await writeQueue;
|
|
1703
|
-
if (total && received < total) {
|
|
1704
|
-
const err = new DropgateNetworkError(
|
|
1705
|
-
"Transfer ended before the full file was received."
|
|
1706
|
-
);
|
|
1707
|
-
try {
|
|
1708
|
-
conn.send({ t: "error", message: err.message });
|
|
1709
|
-
} catch {
|
|
1710
|
-
}
|
|
1711
|
-
throw err;
|
|
1712
|
-
}
|
|
1713
|
-
try {
|
|
1714
|
-
conn.send({ t: "ack", phase: "end", received, total });
|
|
1715
|
-
} catch {
|
|
1716
|
-
}
|
|
1717
|
-
safeComplete({ received, total });
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
if (msg.t === "error") {
|
|
1721
|
-
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
1722
|
-
}
|
|
1723
|
-
if (msg.t === "cancelled") {
|
|
1724
|
-
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1725
|
-
state = "cancelled";
|
|
1726
|
-
onCancel?.({ cancelledBy: "sender", message: msg.message });
|
|
1727
|
-
cleanup();
|
|
1728
|
-
return;
|
|
3174
|
+
const res = await this.fetchFn(url, { ...fetchOptions, signal: s });
|
|
3175
|
+
if (res.ok) return;
|
|
3176
|
+
const text = await res.text().catch(() => "");
|
|
3177
|
+
const err2 = new DropgateProtocolError(
|
|
3178
|
+
`Chunk ${chunkIndex + 1} failed (HTTP ${res.status}).`,
|
|
3179
|
+
{
|
|
3180
|
+
details: { status: res.status, bodySnippet: text.slice(0, 120) }
|
|
1729
3181
|
}
|
|
1730
|
-
|
|
3182
|
+
);
|
|
3183
|
+
throw err2;
|
|
3184
|
+
} catch (err2) {
|
|
3185
|
+
cleanup();
|
|
3186
|
+
if (err2 instanceof Error && (err2.name === "AbortError" || err2.code === "ABORT_ERR")) {
|
|
3187
|
+
throw err2;
|
|
1731
3188
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
bufPromise = Promise.resolve(new Uint8Array(data));
|
|
1735
|
-
} else if (ArrayBuffer.isView(data)) {
|
|
1736
|
-
bufPromise = Promise.resolve(
|
|
1737
|
-
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
1738
|
-
);
|
|
1739
|
-
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
1740
|
-
bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
1741
|
-
} else {
|
|
1742
|
-
return;
|
|
3189
|
+
if (signal?.aborted) {
|
|
3190
|
+
throw signal.reason || new DropgateAbortError();
|
|
1743
3191
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
3192
|
+
if (attemptsLeft <= 0) {
|
|
3193
|
+
throw err2 instanceof DropgateError ? err2 : new DropgateNetworkError("Chunk upload failed.", { cause: err2 });
|
|
3194
|
+
}
|
|
3195
|
+
const attemptNumber = maxRetries - attemptsLeft + 1;
|
|
3196
|
+
const processedBytes = chunkIndex * chunkSize;
|
|
3197
|
+
const percent = chunkIndex / totalChunks * 100;
|
|
3198
|
+
let remaining = currentBackoff;
|
|
3199
|
+
const tick = 100;
|
|
3200
|
+
while (remaining > 0) {
|
|
3201
|
+
const secondsLeft = (remaining / 1e3).toFixed(1);
|
|
3202
|
+
progress({
|
|
3203
|
+
phase: "retry-wait",
|
|
3204
|
+
text: `Chunk upload failed. Retrying in ${secondsLeft}s... (${attemptNumber}/${maxRetries})`,
|
|
3205
|
+
percent,
|
|
3206
|
+
processedBytes,
|
|
3207
|
+
totalBytes: fileSizeBytes,
|
|
3208
|
+
chunkIndex,
|
|
3209
|
+
totalChunks
|
|
3210
|
+
});
|
|
3211
|
+
await sleep(Math.min(tick, remaining), signal);
|
|
3212
|
+
remaining -= tick;
|
|
3213
|
+
}
|
|
3214
|
+
progress({
|
|
3215
|
+
phase: "retry",
|
|
3216
|
+
text: `Chunk upload failed. Retrying now... (${attemptNumber}/${maxRetries})`,
|
|
3217
|
+
percent,
|
|
3218
|
+
processedBytes,
|
|
3219
|
+
totalBytes: fileSizeBytes,
|
|
3220
|
+
chunkIndex,
|
|
3221
|
+
totalChunks
|
|
1769
3222
|
});
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
conn.on("close", () => {
|
|
1775
|
-
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1776
|
-
cleanup();
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
if (state === "transferring") {
|
|
1780
|
-
state = "cancelled";
|
|
1781
|
-
onCancel?.({ cancelledBy: "sender" });
|
|
1782
|
-
cleanup();
|
|
1783
|
-
} else if (state === "negotiating") {
|
|
1784
|
-
state = "closed";
|
|
3223
|
+
attemptsLeft -= 1;
|
|
3224
|
+
currentBackoff = Math.min(currentBackoff * 2, maxBackoffMs);
|
|
3225
|
+
continue;
|
|
3226
|
+
} finally {
|
|
1785
3227
|
cleanup();
|
|
1786
|
-
onDisconnect?.();
|
|
1787
|
-
} else {
|
|
1788
|
-
safeError(new DropgateNetworkError("Sender disconnected before file details were received."));
|
|
1789
3228
|
}
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
peer,
|
|
1794
|
-
stop,
|
|
1795
|
-
getStatus: () => state,
|
|
1796
|
-
getBytesReceived: () => received,
|
|
1797
|
-
getTotalBytes: () => total,
|
|
1798
|
-
getSessionId: () => currentSessionId
|
|
1799
|
-
};
|
|
1800
|
-
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
1801
3232
|
export {
|
|
1802
3233
|
AES_GCM_IV_BYTES,
|
|
1803
3234
|
AES_GCM_TAG_BYTES,
|
|
@@ -1810,12 +3241,11 @@ export {
|
|
|
1810
3241
|
DropgateTimeoutError,
|
|
1811
3242
|
DropgateValidationError,
|
|
1812
3243
|
ENCRYPTION_OVERHEAD_PER_CHUNK,
|
|
3244
|
+
StreamingZipWriter,
|
|
1813
3245
|
arrayBufferToBase64,
|
|
1814
3246
|
base64ToBytes,
|
|
1815
3247
|
buildBaseUrl,
|
|
1816
|
-
buildPeerOptions,
|
|
1817
3248
|
bytesToBase64,
|
|
1818
|
-
createPeerWithRetries,
|
|
1819
3249
|
decryptChunk,
|
|
1820
3250
|
decryptFilenameFromBase64,
|
|
1821
3251
|
encryptFilenameToBase64,
|
|
@@ -1837,11 +3267,8 @@ export {
|
|
|
1837
3267
|
makeAbortSignal,
|
|
1838
3268
|
parseSemverMajorMinor,
|
|
1839
3269
|
parseServerUrl,
|
|
1840
|
-
resolvePeerConfig,
|
|
1841
3270
|
sha256Hex,
|
|
1842
3271
|
sleep,
|
|
1843
|
-
startP2PReceive,
|
|
1844
|
-
startP2PSend,
|
|
1845
3272
|
validatePlainFilename
|
|
1846
3273
|
};
|
|
1847
3274
|
//# sourceMappingURL=index.js.map
|