@dropgate/core 2.1.0 → 2.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +290 -199
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -7
- package/dist/index.d.ts +32 -7
- package/dist/index.js +290 -199
- package/dist/index.js.map +1 -1
- package/dist/p2p/index.cjs +54 -15
- package/dist/p2p/index.cjs.map +1 -1
- package/dist/p2p/index.d.cts +13 -2
- package/dist/p2p/index.d.ts +13 -2
- package/dist/p2p/index.js +54 -15
- package/dist/p2p/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -507,7 +507,7 @@ var DropgateClient = class {
|
|
|
507
507
|
}
|
|
508
508
|
if (encrypt && !caps.e2ee) {
|
|
509
509
|
throw new DropgateValidationError(
|
|
510
|
-
"
|
|
510
|
+
"End-to-end encryption is not supported on this server."
|
|
511
511
|
);
|
|
512
512
|
}
|
|
513
513
|
return true;
|
|
@@ -531,201 +531,253 @@ var DropgateClient = class {
|
|
|
531
531
|
encrypt,
|
|
532
532
|
filenameOverride,
|
|
533
533
|
onProgress,
|
|
534
|
+
onCancel,
|
|
534
535
|
signal,
|
|
535
536
|
timeouts = {},
|
|
536
537
|
retry = {}
|
|
537
538
|
} = opts;
|
|
538
|
-
const
|
|
539
|
+
const internalController = signal ? null : new AbortController();
|
|
540
|
+
const effectiveSignal = signal || internalController?.signal;
|
|
541
|
+
let uploadState = "initializing";
|
|
542
|
+
let currentUploadId = null;
|
|
543
|
+
let currentBaseUrl = null;
|
|
544
|
+
const uploadPromise = (async () => {
|
|
539
545
|
try {
|
|
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
|
-
|
|
546
|
+
const progress = (evt) => {
|
|
547
|
+
try {
|
|
548
|
+
if (onProgress) onProgress(evt);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
if (!this.cryptoObj?.subtle) {
|
|
553
|
+
throw new DropgateValidationError(
|
|
554
|
+
"Web Crypto API not available (crypto.subtle)."
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const fileSizeBytes = file.size;
|
|
558
|
+
progress({ phase: "server-info", text: "Checking server...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
559
|
+
const compat = await this.checkCompatibility({
|
|
560
|
+
host,
|
|
561
|
+
port,
|
|
562
|
+
secure,
|
|
563
|
+
timeoutMs: timeouts.serverInfoMs ?? 5e3,
|
|
564
|
+
signal: effectiveSignal
|
|
565
|
+
});
|
|
566
|
+
const { baseUrl, serverInfo } = compat;
|
|
567
|
+
progress({ phase: "server-compat", text: compat.message, percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
568
|
+
if (!compat.compatible) {
|
|
569
|
+
throw new DropgateValidationError(compat.message);
|
|
570
|
+
}
|
|
571
|
+
const filename = filenameOverride ?? file.name ?? "file";
|
|
572
|
+
const serverSupportsE2EE = Boolean(serverInfo?.capabilities?.upload?.e2ee);
|
|
573
|
+
const effectiveEncrypt = encrypt ?? serverSupportsE2EE;
|
|
574
|
+
if (!effectiveEncrypt) {
|
|
575
|
+
validatePlainFilename(filename);
|
|
576
|
+
}
|
|
577
|
+
this.validateUploadInputs({ file, lifetimeMs, encrypt: effectiveEncrypt, serverInfo });
|
|
578
|
+
let cryptoKey = null;
|
|
579
|
+
let keyB64 = null;
|
|
580
|
+
let transmittedFilename = filename;
|
|
581
|
+
if (effectiveEncrypt) {
|
|
582
|
+
progress({ phase: "crypto", text: "Generating encryption key...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
583
|
+
try {
|
|
584
|
+
cryptoKey = await generateAesGcmKey(this.cryptoObj);
|
|
585
|
+
keyB64 = await exportKeyBase64(this.cryptoObj, cryptoKey);
|
|
586
|
+
transmittedFilename = await encryptFilenameToBase64(
|
|
587
|
+
this.cryptoObj,
|
|
588
|
+
filename,
|
|
589
|
+
cryptoKey
|
|
590
|
+
);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
throw new DropgateError("Failed to prepare encryption.", {
|
|
593
|
+
code: "CRYPTO_PREP_FAILED",
|
|
594
|
+
cause: err
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const totalChunks = Math.ceil(file.size / this.chunkSize);
|
|
599
|
+
const totalUploadSize = estimateTotalUploadSizeBytes(
|
|
600
|
+
file.size,
|
|
601
|
+
totalChunks,
|
|
602
|
+
effectiveEncrypt
|
|
580
603
|
);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
604
|
+
progress({ phase: "init", text: "Reserving server storage...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
605
|
+
const initPayload = {
|
|
606
|
+
filename: transmittedFilename,
|
|
607
|
+
lifetime: lifetimeMs,
|
|
608
|
+
isEncrypted: effectiveEncrypt,
|
|
609
|
+
totalSize: totalUploadSize,
|
|
610
|
+
totalChunks
|
|
611
|
+
};
|
|
612
|
+
const initRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init`, {
|
|
613
|
+
method: "POST",
|
|
614
|
+
timeoutMs: timeouts.initMs ?? 15e3,
|
|
615
|
+
signal: effectiveSignal,
|
|
616
|
+
headers: {
|
|
617
|
+
"Content-Type": "application/json",
|
|
618
|
+
Accept: "application/json"
|
|
619
|
+
},
|
|
620
|
+
body: JSON.stringify(initPayload)
|
|
585
621
|
});
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
622
|
+
if (!initRes.res.ok) {
|
|
623
|
+
const errorJson = initRes.json;
|
|
624
|
+
const msg = errorJson?.error || `Server initialisation failed: ${initRes.res.status}`;
|
|
625
|
+
throw new DropgateProtocolError(msg, {
|
|
626
|
+
details: initRes.json || initRes.text
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const initJson = initRes.json;
|
|
630
|
+
const uploadId = initJson?.uploadId;
|
|
631
|
+
if (!uploadId || typeof uploadId !== "string") {
|
|
632
|
+
throw new DropgateProtocolError(
|
|
633
|
+
"Server did not return a valid uploadId."
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
currentUploadId = uploadId;
|
|
637
|
+
currentBaseUrl = baseUrl;
|
|
638
|
+
uploadState = "uploading";
|
|
639
|
+
const retries = Number.isFinite(retry.retries) ? retry.retries : 5;
|
|
640
|
+
const baseBackoffMs = Number.isFinite(retry.backoffMs) ? retry.backoffMs : 1e3;
|
|
641
|
+
const maxBackoffMs = Number.isFinite(retry.maxBackoffMs) ? retry.maxBackoffMs : 3e4;
|
|
642
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
643
|
+
if (effectiveSignal?.aborted) {
|
|
644
|
+
throw effectiveSignal.reason || new DropgateAbortError();
|
|
645
|
+
}
|
|
646
|
+
const start = i * this.chunkSize;
|
|
647
|
+
const end = Math.min(start + this.chunkSize, file.size);
|
|
648
|
+
let chunkBlob = file.slice(start, end);
|
|
649
|
+
const percentComplete = i / totalChunks * 100;
|
|
650
|
+
const processedBytes = i * this.chunkSize;
|
|
651
|
+
progress({
|
|
652
|
+
phase: "chunk",
|
|
653
|
+
text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
|
|
654
|
+
percent: percentComplete,
|
|
655
|
+
processedBytes,
|
|
656
|
+
totalBytes: fileSizeBytes,
|
|
657
|
+
chunkIndex: i,
|
|
658
|
+
totalChunks
|
|
659
|
+
});
|
|
660
|
+
const chunkBuffer = await chunkBlob.arrayBuffer();
|
|
661
|
+
let uploadBlob;
|
|
662
|
+
if (effectiveEncrypt && cryptoKey) {
|
|
663
|
+
uploadBlob = await encryptToBlob(this.cryptoObj, chunkBuffer, cryptoKey);
|
|
664
|
+
} else {
|
|
665
|
+
uploadBlob = new Blob([chunkBuffer]);
|
|
666
|
+
}
|
|
667
|
+
if (uploadBlob.size > DEFAULT_CHUNK_SIZE + 1024) {
|
|
668
|
+
throw new DropgateValidationError(
|
|
669
|
+
"Chunk too large (client-side). Check chunk size settings."
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
const toHash = await uploadBlob.arrayBuffer();
|
|
673
|
+
const hashHex = await sha256Hex(this.cryptoObj, toHash);
|
|
674
|
+
const headers = {
|
|
675
|
+
"Content-Type": "application/octet-stream",
|
|
676
|
+
"X-Upload-ID": uploadId,
|
|
677
|
+
"X-Chunk-Index": String(i),
|
|
678
|
+
"X-Chunk-Hash": hashHex
|
|
679
|
+
};
|
|
680
|
+
const chunkUrl = `${baseUrl}/upload/chunk`;
|
|
681
|
+
await this.attemptChunkUpload(
|
|
682
|
+
chunkUrl,
|
|
683
|
+
{
|
|
684
|
+
method: "POST",
|
|
685
|
+
headers,
|
|
686
|
+
body: uploadBlob
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
retries,
|
|
690
|
+
backoffMs: baseBackoffMs,
|
|
691
|
+
maxBackoffMs,
|
|
692
|
+
timeoutMs: timeouts.chunkMs ?? 6e4,
|
|
693
|
+
signal: effectiveSignal,
|
|
694
|
+
progress,
|
|
695
|
+
chunkIndex: i,
|
|
696
|
+
totalChunks,
|
|
697
|
+
chunkSize: this.chunkSize,
|
|
698
|
+
fileSizeBytes
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
703
|
+
uploadState = "completing";
|
|
704
|
+
const completeRes = await fetchJson(
|
|
705
|
+
this.fetchFn,
|
|
706
|
+
`${baseUrl}/upload/complete`,
|
|
707
|
+
{
|
|
708
|
+
method: "POST",
|
|
709
|
+
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
710
|
+
signal: effectiveSignal,
|
|
711
|
+
headers: {
|
|
712
|
+
"Content-Type": "application/json",
|
|
713
|
+
Accept: "application/json"
|
|
714
|
+
},
|
|
715
|
+
body: JSON.stringify({ uploadId })
|
|
716
|
+
}
|
|
657
717
|
);
|
|
718
|
+
if (!completeRes.res.ok) {
|
|
719
|
+
const errorJson = completeRes.json;
|
|
720
|
+
const msg = errorJson?.error || "Finalisation failed.";
|
|
721
|
+
throw new DropgateProtocolError(msg, {
|
|
722
|
+
details: completeRes.json || completeRes.text
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
const completeJson = completeRes.json;
|
|
726
|
+
const fileId = completeJson?.id;
|
|
727
|
+
if (!fileId || typeof fileId !== "string") {
|
|
728
|
+
throw new DropgateProtocolError(
|
|
729
|
+
"Server did not return a valid file id."
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
let downloadUrl = `${baseUrl}/${fileId}`;
|
|
733
|
+
if (effectiveEncrypt && keyB64) {
|
|
734
|
+
downloadUrl += `#${keyB64}`;
|
|
735
|
+
}
|
|
736
|
+
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
737
|
+
uploadState = "completed";
|
|
738
|
+
return {
|
|
739
|
+
downloadUrl,
|
|
740
|
+
fileId,
|
|
741
|
+
uploadId,
|
|
742
|
+
baseUrl,
|
|
743
|
+
...effectiveEncrypt && keyB64 ? { keyB64 } : {}
|
|
744
|
+
};
|
|
745
|
+
} catch (err) {
|
|
746
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message?.includes("abort"))) {
|
|
747
|
+
uploadState = "cancelled";
|
|
748
|
+
onCancel?.();
|
|
749
|
+
} else {
|
|
750
|
+
uploadState = "error";
|
|
751
|
+
}
|
|
752
|
+
throw err;
|
|
658
753
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
"X-Upload-ID": uploadId,
|
|
664
|
-
"X-Chunk-Index": String(i),
|
|
665
|
-
"X-Chunk-Hash": hashHex
|
|
666
|
-
};
|
|
667
|
-
const chunkUrl = `${baseUrl}/upload/chunk`;
|
|
668
|
-
await this.attemptChunkUpload(
|
|
669
|
-
chunkUrl,
|
|
670
|
-
{
|
|
754
|
+
})();
|
|
755
|
+
const callCancelEndpoint = async (uploadId, baseUrl) => {
|
|
756
|
+
try {
|
|
757
|
+
await fetchJson(this.fetchFn, `${baseUrl}/upload/cancel`, {
|
|
671
758
|
method: "POST",
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
signal,
|
|
681
|
-
progress,
|
|
682
|
-
chunkIndex: i,
|
|
683
|
-
totalChunks,
|
|
684
|
-
chunkSize: this.chunkSize,
|
|
685
|
-
fileSizeBytes
|
|
686
|
-
}
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
690
|
-
const completeRes = await fetchJson(
|
|
691
|
-
this.fetchFn,
|
|
692
|
-
`${baseUrl}/upload/complete`,
|
|
693
|
-
{
|
|
694
|
-
method: "POST",
|
|
695
|
-
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
696
|
-
signal,
|
|
697
|
-
headers: {
|
|
698
|
-
"Content-Type": "application/json",
|
|
699
|
-
Accept: "application/json"
|
|
700
|
-
},
|
|
701
|
-
body: JSON.stringify({ uploadId })
|
|
759
|
+
timeoutMs: 5e3,
|
|
760
|
+
headers: {
|
|
761
|
+
"Content-Type": "application/json",
|
|
762
|
+
Accept: "application/json"
|
|
763
|
+
},
|
|
764
|
+
body: JSON.stringify({ uploadId })
|
|
765
|
+
});
|
|
766
|
+
} catch {
|
|
702
767
|
}
|
|
703
|
-
|
|
704
|
-
if (!completeRes.res.ok) {
|
|
705
|
-
const errorJson = completeRes.json;
|
|
706
|
-
const msg = errorJson?.error || "Finalisation failed.";
|
|
707
|
-
throw new DropgateProtocolError(msg, {
|
|
708
|
-
details: completeRes.json || completeRes.text
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
const completeJson = completeRes.json;
|
|
712
|
-
const fileId = completeJson?.id;
|
|
713
|
-
if (!fileId || typeof fileId !== "string") {
|
|
714
|
-
throw new DropgateProtocolError(
|
|
715
|
-
"Server did not return a valid file id."
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
let downloadUrl = `${baseUrl}/${fileId}`;
|
|
719
|
-
if (encrypt && keyB64) {
|
|
720
|
-
downloadUrl += `#${keyB64}`;
|
|
721
|
-
}
|
|
722
|
-
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
768
|
+
};
|
|
723
769
|
return {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
770
|
+
result: uploadPromise,
|
|
771
|
+
cancel: (reason) => {
|
|
772
|
+
if (uploadState === "completed" || uploadState === "cancelled") return;
|
|
773
|
+
uploadState = "cancelled";
|
|
774
|
+
if (currentUploadId && currentBaseUrl) {
|
|
775
|
+
callCancelEndpoint(currentUploadId, currentBaseUrl).catch(() => {
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
internalController?.abort(new DropgateAbortError(reason || "Upload cancelled by user."));
|
|
779
|
+
},
|
|
780
|
+
getStatus: () => uploadState
|
|
729
781
|
};
|
|
730
782
|
}
|
|
731
783
|
/**
|
|
@@ -1170,7 +1222,8 @@ async function startP2PSend(opts) {
|
|
|
1170
1222
|
onProgress,
|
|
1171
1223
|
onComplete,
|
|
1172
1224
|
onError,
|
|
1173
|
-
onDisconnect
|
|
1225
|
+
onDisconnect,
|
|
1226
|
+
onCancel
|
|
1174
1227
|
} = opts;
|
|
1175
1228
|
if (!file) {
|
|
1176
1229
|
throw new DropgateValidationError("File is missing.");
|
|
@@ -1216,7 +1269,7 @@ async function startP2PSend(opts) {
|
|
|
1216
1269
|
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
1217
1270
|
};
|
|
1218
1271
|
const safeError = (err) => {
|
|
1219
|
-
if (state === "closed" || state === "completed") return;
|
|
1272
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1220
1273
|
state = "closed";
|
|
1221
1274
|
onError?.(err);
|
|
1222
1275
|
cleanup();
|
|
@@ -1255,11 +1308,21 @@ async function startP2PSend(opts) {
|
|
|
1255
1308
|
window.addEventListener("beforeunload", handleUnload);
|
|
1256
1309
|
}
|
|
1257
1310
|
const stop = () => {
|
|
1258
|
-
if (state === "closed") return;
|
|
1259
|
-
|
|
1311
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1312
|
+
const wasActive = state === "transferring" || state === "finishing";
|
|
1313
|
+
state = "cancelled";
|
|
1314
|
+
try {
|
|
1315
|
+
if (activeConn && activeConn.open) {
|
|
1316
|
+
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
1317
|
+
}
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
if (wasActive && onCancel) {
|
|
1321
|
+
onCancel({ cancelledBy: "sender" });
|
|
1322
|
+
}
|
|
1260
1323
|
cleanup();
|
|
1261
1324
|
};
|
|
1262
|
-
const isStopped = () => state === "closed";
|
|
1325
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
1263
1326
|
peer.on("connection", (conn) => {
|
|
1264
1327
|
if (state === "closed") return;
|
|
1265
1328
|
if (activeConn) {
|
|
@@ -1329,6 +1392,13 @@ async function startP2PSend(opts) {
|
|
|
1329
1392
|
}
|
|
1330
1393
|
if (msg.t === "error") {
|
|
1331
1394
|
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (msg.t === "cancelled") {
|
|
1398
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1399
|
+
state = "cancelled";
|
|
1400
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.message });
|
|
1401
|
+
cleanup();
|
|
1332
1402
|
}
|
|
1333
1403
|
});
|
|
1334
1404
|
conn.on("open", async () => {
|
|
@@ -1366,6 +1436,7 @@ async function startP2PSend(opts) {
|
|
|
1366
1436
|
if (isStopped()) return;
|
|
1367
1437
|
const slice = file.slice(offset, offset + chunkSize);
|
|
1368
1438
|
const buf = await slice.arrayBuffer();
|
|
1439
|
+
if (isStopped()) return;
|
|
1369
1440
|
conn.send(buf);
|
|
1370
1441
|
sentBytes += buf.byteLength;
|
|
1371
1442
|
if (dc) {
|
|
@@ -1415,14 +1486,14 @@ async function startP2PSend(opts) {
|
|
|
1415
1486
|
safeError(err);
|
|
1416
1487
|
});
|
|
1417
1488
|
conn.on("close", () => {
|
|
1418
|
-
if (state === "closed" || state === "completed") {
|
|
1489
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1419
1490
|
cleanup();
|
|
1420
1491
|
return;
|
|
1421
1492
|
}
|
|
1422
1493
|
if (state === "transferring" || state === "finishing") {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
);
|
|
1494
|
+
state = "cancelled";
|
|
1495
|
+
onCancel?.({ cancelledBy: "receiver" });
|
|
1496
|
+
cleanup();
|
|
1426
1497
|
} else {
|
|
1427
1498
|
activeConn = null;
|
|
1428
1499
|
state = "listening";
|
|
@@ -1464,7 +1535,8 @@ async function startP2PReceive(opts) {
|
|
|
1464
1535
|
onProgress,
|
|
1465
1536
|
onComplete,
|
|
1466
1537
|
onError,
|
|
1467
|
-
onDisconnect
|
|
1538
|
+
onDisconnect,
|
|
1539
|
+
onCancel
|
|
1468
1540
|
} = opts;
|
|
1469
1541
|
if (!code) {
|
|
1470
1542
|
throw new DropgateValidationError("No sharing code was provided.");
|
|
@@ -1521,7 +1593,7 @@ async function startP2PReceive(opts) {
|
|
|
1521
1593
|
}
|
|
1522
1594
|
};
|
|
1523
1595
|
const safeError = (err) => {
|
|
1524
|
-
if (state === "closed" || state === "completed") return;
|
|
1596
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1525
1597
|
state = "closed";
|
|
1526
1598
|
onError?.(err);
|
|
1527
1599
|
cleanup();
|
|
@@ -1553,8 +1625,18 @@ async function startP2PReceive(opts) {
|
|
|
1553
1625
|
window.addEventListener("beforeunload", handleUnload);
|
|
1554
1626
|
}
|
|
1555
1627
|
const stop = () => {
|
|
1556
|
-
if (state === "closed") return;
|
|
1557
|
-
|
|
1628
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1629
|
+
const wasActive = state === "transferring";
|
|
1630
|
+
state = "cancelled";
|
|
1631
|
+
try {
|
|
1632
|
+
if (activeConn && activeConn.open) {
|
|
1633
|
+
activeConn.send({ t: "cancelled", message: "Receiver cancelled the transfer." });
|
|
1634
|
+
}
|
|
1635
|
+
} catch {
|
|
1636
|
+
}
|
|
1637
|
+
if (wasActive && onCancel) {
|
|
1638
|
+
onCancel({ cancelledBy: "receiver" });
|
|
1639
|
+
}
|
|
1558
1640
|
cleanup();
|
|
1559
1641
|
};
|
|
1560
1642
|
peer.on("error", (err) => {
|
|
@@ -1636,6 +1718,13 @@ async function startP2PReceive(opts) {
|
|
|
1636
1718
|
if (msg.t === "error") {
|
|
1637
1719
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
1638
1720
|
}
|
|
1721
|
+
if (msg.t === "cancelled") {
|
|
1722
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1723
|
+
state = "cancelled";
|
|
1724
|
+
onCancel?.({ cancelledBy: "sender", message: msg.message });
|
|
1725
|
+
cleanup();
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1639
1728
|
return;
|
|
1640
1729
|
}
|
|
1641
1730
|
let bufPromise;
|
|
@@ -1681,12 +1770,14 @@ async function startP2PReceive(opts) {
|
|
|
1681
1770
|
}
|
|
1682
1771
|
});
|
|
1683
1772
|
conn.on("close", () => {
|
|
1684
|
-
if (state === "closed" || state === "completed") {
|
|
1773
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1685
1774
|
cleanup();
|
|
1686
1775
|
return;
|
|
1687
1776
|
}
|
|
1688
1777
|
if (state === "transferring") {
|
|
1689
|
-
|
|
1778
|
+
state = "cancelled";
|
|
1779
|
+
onCancel?.({ cancelledBy: "sender" });
|
|
1780
|
+
cleanup();
|
|
1690
1781
|
} else if (state === "negotiating") {
|
|
1691
1782
|
state = "closed";
|
|
1692
1783
|
cleanup();
|