@dropgate/core 2.1.0 → 2.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +287 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -3
- package/dist/index.d.ts +28 -3
- package/dist/index.js +287 -198
- 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.cjs
CHANGED
|
@@ -600,201 +600,251 @@ var DropgateClient = class {
|
|
|
600
600
|
encrypt,
|
|
601
601
|
filenameOverride,
|
|
602
602
|
onProgress,
|
|
603
|
+
onCancel,
|
|
603
604
|
signal,
|
|
604
605
|
timeouts = {},
|
|
605
606
|
retry = {}
|
|
606
607
|
} = opts;
|
|
607
|
-
const
|
|
608
|
+
const internalController = signal ? null : new AbortController();
|
|
609
|
+
const effectiveSignal = signal || internalController?.signal;
|
|
610
|
+
let uploadState = "initializing";
|
|
611
|
+
let currentUploadId = null;
|
|
612
|
+
let currentBaseUrl = null;
|
|
613
|
+
const uploadPromise = (async () => {
|
|
608
614
|
try {
|
|
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
|
-
|
|
615
|
+
const progress = (evt) => {
|
|
616
|
+
try {
|
|
617
|
+
if (onProgress) onProgress(evt);
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
if (!this.cryptoObj?.subtle) {
|
|
622
|
+
throw new DropgateValidationError(
|
|
623
|
+
"Web Crypto API not available (crypto.subtle)."
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const fileSizeBytes = file.size;
|
|
627
|
+
progress({ phase: "server-info", text: "Checking server...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
628
|
+
const compat = await this.checkCompatibility({
|
|
629
|
+
host,
|
|
630
|
+
port,
|
|
631
|
+
secure,
|
|
632
|
+
timeoutMs: timeouts.serverInfoMs ?? 5e3,
|
|
633
|
+
signal: effectiveSignal
|
|
634
|
+
});
|
|
635
|
+
const { baseUrl, serverInfo } = compat;
|
|
636
|
+
progress({ phase: "server-compat", text: compat.message, percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
637
|
+
if (!compat.compatible) {
|
|
638
|
+
throw new DropgateValidationError(compat.message);
|
|
639
|
+
}
|
|
640
|
+
const filename = filenameOverride ?? file.name ?? "file";
|
|
641
|
+
if (!encrypt) {
|
|
642
|
+
validatePlainFilename(filename);
|
|
643
|
+
}
|
|
644
|
+
this.validateUploadInputs({ file, lifetimeMs, encrypt, serverInfo });
|
|
645
|
+
let cryptoKey = null;
|
|
646
|
+
let keyB64 = null;
|
|
647
|
+
let transmittedFilename = filename;
|
|
648
|
+
if (encrypt) {
|
|
649
|
+
progress({ phase: "crypto", text: "Generating encryption key...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
650
|
+
try {
|
|
651
|
+
cryptoKey = await generateAesGcmKey(this.cryptoObj);
|
|
652
|
+
keyB64 = await exportKeyBase64(this.cryptoObj, cryptoKey);
|
|
653
|
+
transmittedFilename = await encryptFilenameToBase64(
|
|
654
|
+
this.cryptoObj,
|
|
655
|
+
filename,
|
|
656
|
+
cryptoKey
|
|
657
|
+
);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
throw new DropgateError("Failed to prepare encryption.", {
|
|
660
|
+
code: "CRYPTO_PREP_FAILED",
|
|
661
|
+
cause: err
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const totalChunks = Math.ceil(file.size / this.chunkSize);
|
|
666
|
+
const totalUploadSize = estimateTotalUploadSizeBytes(
|
|
667
|
+
file.size,
|
|
668
|
+
totalChunks,
|
|
669
|
+
encrypt
|
|
649
670
|
);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
671
|
+
progress({ phase: "init", text: "Reserving server storage...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
|
|
672
|
+
const initPayload = {
|
|
673
|
+
filename: transmittedFilename,
|
|
674
|
+
lifetime: lifetimeMs,
|
|
675
|
+
isEncrypted: Boolean(encrypt),
|
|
676
|
+
totalSize: totalUploadSize,
|
|
677
|
+
totalChunks
|
|
678
|
+
};
|
|
679
|
+
const initRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init`, {
|
|
680
|
+
method: "POST",
|
|
681
|
+
timeoutMs: timeouts.initMs ?? 15e3,
|
|
682
|
+
signal: effectiveSignal,
|
|
683
|
+
headers: {
|
|
684
|
+
"Content-Type": "application/json",
|
|
685
|
+
Accept: "application/json"
|
|
686
|
+
},
|
|
687
|
+
body: JSON.stringify(initPayload)
|
|
654
688
|
});
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
689
|
+
if (!initRes.res.ok) {
|
|
690
|
+
const errorJson = initRes.json;
|
|
691
|
+
const msg = errorJson?.error || `Server initialisation failed: ${initRes.res.status}`;
|
|
692
|
+
throw new DropgateProtocolError(msg, {
|
|
693
|
+
details: initRes.json || initRes.text
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
const initJson = initRes.json;
|
|
697
|
+
const uploadId = initJson?.uploadId;
|
|
698
|
+
if (!uploadId || typeof uploadId !== "string") {
|
|
699
|
+
throw new DropgateProtocolError(
|
|
700
|
+
"Server did not return a valid uploadId."
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
currentUploadId = uploadId;
|
|
704
|
+
currentBaseUrl = baseUrl;
|
|
705
|
+
uploadState = "uploading";
|
|
706
|
+
const retries = Number.isFinite(retry.retries) ? retry.retries : 5;
|
|
707
|
+
const baseBackoffMs = Number.isFinite(retry.backoffMs) ? retry.backoffMs : 1e3;
|
|
708
|
+
const maxBackoffMs = Number.isFinite(retry.maxBackoffMs) ? retry.maxBackoffMs : 3e4;
|
|
709
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
710
|
+
if (effectiveSignal?.aborted) {
|
|
711
|
+
throw effectiveSignal.reason || new DropgateAbortError();
|
|
712
|
+
}
|
|
713
|
+
const start = i * this.chunkSize;
|
|
714
|
+
const end = Math.min(start + this.chunkSize, file.size);
|
|
715
|
+
let chunkBlob = file.slice(start, end);
|
|
716
|
+
const percentComplete = i / totalChunks * 100;
|
|
717
|
+
const processedBytes = i * this.chunkSize;
|
|
718
|
+
progress({
|
|
719
|
+
phase: "chunk",
|
|
720
|
+
text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
|
|
721
|
+
percent: percentComplete,
|
|
722
|
+
processedBytes,
|
|
723
|
+
totalBytes: fileSizeBytes,
|
|
724
|
+
chunkIndex: i,
|
|
725
|
+
totalChunks
|
|
726
|
+
});
|
|
727
|
+
const chunkBuffer = await chunkBlob.arrayBuffer();
|
|
728
|
+
let uploadBlob;
|
|
729
|
+
if (encrypt && cryptoKey) {
|
|
730
|
+
uploadBlob = await encryptToBlob(this.cryptoObj, chunkBuffer, cryptoKey);
|
|
731
|
+
} else {
|
|
732
|
+
uploadBlob = new Blob([chunkBuffer]);
|
|
733
|
+
}
|
|
734
|
+
if (uploadBlob.size > DEFAULT_CHUNK_SIZE + 1024) {
|
|
735
|
+
throw new DropgateValidationError(
|
|
736
|
+
"Chunk too large (client-side). Check chunk size settings."
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
const toHash = await uploadBlob.arrayBuffer();
|
|
740
|
+
const hashHex = await sha256Hex(this.cryptoObj, toHash);
|
|
741
|
+
const headers = {
|
|
742
|
+
"Content-Type": "application/octet-stream",
|
|
743
|
+
"X-Upload-ID": uploadId,
|
|
744
|
+
"X-Chunk-Index": String(i),
|
|
745
|
+
"X-Chunk-Hash": hashHex
|
|
746
|
+
};
|
|
747
|
+
const chunkUrl = `${baseUrl}/upload/chunk`;
|
|
748
|
+
await this.attemptChunkUpload(
|
|
749
|
+
chunkUrl,
|
|
750
|
+
{
|
|
751
|
+
method: "POST",
|
|
752
|
+
headers,
|
|
753
|
+
body: uploadBlob
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
retries,
|
|
757
|
+
backoffMs: baseBackoffMs,
|
|
758
|
+
maxBackoffMs,
|
|
759
|
+
timeoutMs: timeouts.chunkMs ?? 6e4,
|
|
760
|
+
signal: effectiveSignal,
|
|
761
|
+
progress,
|
|
762
|
+
chunkIndex: i,
|
|
763
|
+
totalChunks,
|
|
764
|
+
chunkSize: this.chunkSize,
|
|
765
|
+
fileSizeBytes
|
|
766
|
+
}
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
770
|
+
uploadState = "completing";
|
|
771
|
+
const completeRes = await fetchJson(
|
|
772
|
+
this.fetchFn,
|
|
773
|
+
`${baseUrl}/upload/complete`,
|
|
774
|
+
{
|
|
775
|
+
method: "POST",
|
|
776
|
+
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
777
|
+
signal: effectiveSignal,
|
|
778
|
+
headers: {
|
|
779
|
+
"Content-Type": "application/json",
|
|
780
|
+
Accept: "application/json"
|
|
781
|
+
},
|
|
782
|
+
body: JSON.stringify({ uploadId })
|
|
783
|
+
}
|
|
726
784
|
);
|
|
785
|
+
if (!completeRes.res.ok) {
|
|
786
|
+
const errorJson = completeRes.json;
|
|
787
|
+
const msg = errorJson?.error || "Finalisation failed.";
|
|
788
|
+
throw new DropgateProtocolError(msg, {
|
|
789
|
+
details: completeRes.json || completeRes.text
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
const completeJson = completeRes.json;
|
|
793
|
+
const fileId = completeJson?.id;
|
|
794
|
+
if (!fileId || typeof fileId !== "string") {
|
|
795
|
+
throw new DropgateProtocolError(
|
|
796
|
+
"Server did not return a valid file id."
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
let downloadUrl = `${baseUrl}/${fileId}`;
|
|
800
|
+
if (encrypt && keyB64) {
|
|
801
|
+
downloadUrl += `#${keyB64}`;
|
|
802
|
+
}
|
|
803
|
+
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
804
|
+
uploadState = "completed";
|
|
805
|
+
return {
|
|
806
|
+
downloadUrl,
|
|
807
|
+
fileId,
|
|
808
|
+
uploadId,
|
|
809
|
+
baseUrl,
|
|
810
|
+
...encrypt && keyB64 ? { keyB64 } : {}
|
|
811
|
+
};
|
|
812
|
+
} catch (err) {
|
|
813
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message?.includes("abort"))) {
|
|
814
|
+
uploadState = "cancelled";
|
|
815
|
+
onCancel?.();
|
|
816
|
+
} else {
|
|
817
|
+
uploadState = "error";
|
|
818
|
+
}
|
|
819
|
+
throw err;
|
|
727
820
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
"X-Upload-ID": uploadId,
|
|
733
|
-
"X-Chunk-Index": String(i),
|
|
734
|
-
"X-Chunk-Hash": hashHex
|
|
735
|
-
};
|
|
736
|
-
const chunkUrl = `${baseUrl}/upload/chunk`;
|
|
737
|
-
await this.attemptChunkUpload(
|
|
738
|
-
chunkUrl,
|
|
739
|
-
{
|
|
821
|
+
})();
|
|
822
|
+
const callCancelEndpoint = async (uploadId, baseUrl) => {
|
|
823
|
+
try {
|
|
824
|
+
await fetchJson(this.fetchFn, `${baseUrl}/upload/cancel`, {
|
|
740
825
|
method: "POST",
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
signal,
|
|
750
|
-
progress,
|
|
751
|
-
chunkIndex: i,
|
|
752
|
-
totalChunks,
|
|
753
|
-
chunkSize: this.chunkSize,
|
|
754
|
-
fileSizeBytes
|
|
755
|
-
}
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
759
|
-
const completeRes = await fetchJson(
|
|
760
|
-
this.fetchFn,
|
|
761
|
-
`${baseUrl}/upload/complete`,
|
|
762
|
-
{
|
|
763
|
-
method: "POST",
|
|
764
|
-
timeoutMs: timeouts.completeMs ?? 3e4,
|
|
765
|
-
signal,
|
|
766
|
-
headers: {
|
|
767
|
-
"Content-Type": "application/json",
|
|
768
|
-
Accept: "application/json"
|
|
769
|
-
},
|
|
770
|
-
body: JSON.stringify({ uploadId })
|
|
826
|
+
timeoutMs: 5e3,
|
|
827
|
+
headers: {
|
|
828
|
+
"Content-Type": "application/json",
|
|
829
|
+
Accept: "application/json"
|
|
830
|
+
},
|
|
831
|
+
body: JSON.stringify({ uploadId })
|
|
832
|
+
});
|
|
833
|
+
} catch {
|
|
771
834
|
}
|
|
772
|
-
|
|
773
|
-
if (!completeRes.res.ok) {
|
|
774
|
-
const errorJson = completeRes.json;
|
|
775
|
-
const msg = errorJson?.error || "Finalisation failed.";
|
|
776
|
-
throw new DropgateProtocolError(msg, {
|
|
777
|
-
details: completeRes.json || completeRes.text
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
const completeJson = completeRes.json;
|
|
781
|
-
const fileId = completeJson?.id;
|
|
782
|
-
if (!fileId || typeof fileId !== "string") {
|
|
783
|
-
throw new DropgateProtocolError(
|
|
784
|
-
"Server did not return a valid file id."
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
let downloadUrl = `${baseUrl}/${fileId}`;
|
|
788
|
-
if (encrypt && keyB64) {
|
|
789
|
-
downloadUrl += `#${keyB64}`;
|
|
790
|
-
}
|
|
791
|
-
progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
|
|
835
|
+
};
|
|
792
836
|
return {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
837
|
+
result: uploadPromise,
|
|
838
|
+
cancel: (reason) => {
|
|
839
|
+
if (uploadState === "completed" || uploadState === "cancelled") return;
|
|
840
|
+
uploadState = "cancelled";
|
|
841
|
+
if (currentUploadId && currentBaseUrl) {
|
|
842
|
+
callCancelEndpoint(currentUploadId, currentBaseUrl).catch(() => {
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
internalController?.abort(new DropgateAbortError(reason || "Upload cancelled by user."));
|
|
846
|
+
},
|
|
847
|
+
getStatus: () => uploadState
|
|
798
848
|
};
|
|
799
849
|
}
|
|
800
850
|
/**
|
|
@@ -1239,7 +1289,8 @@ async function startP2PSend(opts) {
|
|
|
1239
1289
|
onProgress,
|
|
1240
1290
|
onComplete,
|
|
1241
1291
|
onError,
|
|
1242
|
-
onDisconnect
|
|
1292
|
+
onDisconnect,
|
|
1293
|
+
onCancel
|
|
1243
1294
|
} = opts;
|
|
1244
1295
|
if (!file) {
|
|
1245
1296
|
throw new DropgateValidationError("File is missing.");
|
|
@@ -1285,7 +1336,7 @@ async function startP2PSend(opts) {
|
|
|
1285
1336
|
onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
|
|
1286
1337
|
};
|
|
1287
1338
|
const safeError = (err) => {
|
|
1288
|
-
if (state === "closed" || state === "completed") return;
|
|
1339
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1289
1340
|
state = "closed";
|
|
1290
1341
|
onError?.(err);
|
|
1291
1342
|
cleanup();
|
|
@@ -1324,11 +1375,21 @@ async function startP2PSend(opts) {
|
|
|
1324
1375
|
window.addEventListener("beforeunload", handleUnload);
|
|
1325
1376
|
}
|
|
1326
1377
|
const stop = () => {
|
|
1327
|
-
if (state === "closed") return;
|
|
1328
|
-
|
|
1378
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1379
|
+
const wasActive = state === "transferring" || state === "finishing";
|
|
1380
|
+
state = "cancelled";
|
|
1381
|
+
try {
|
|
1382
|
+
if (activeConn && activeConn.open) {
|
|
1383
|
+
activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
|
|
1384
|
+
}
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
if (wasActive && onCancel) {
|
|
1388
|
+
onCancel({ cancelledBy: "sender" });
|
|
1389
|
+
}
|
|
1329
1390
|
cleanup();
|
|
1330
1391
|
};
|
|
1331
|
-
const isStopped = () => state === "closed";
|
|
1392
|
+
const isStopped = () => state === "closed" || state === "cancelled";
|
|
1332
1393
|
peer.on("connection", (conn) => {
|
|
1333
1394
|
if (state === "closed") return;
|
|
1334
1395
|
if (activeConn) {
|
|
@@ -1398,6 +1459,13 @@ async function startP2PSend(opts) {
|
|
|
1398
1459
|
}
|
|
1399
1460
|
if (msg.t === "error") {
|
|
1400
1461
|
safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (msg.t === "cancelled") {
|
|
1465
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1466
|
+
state = "cancelled";
|
|
1467
|
+
onCancel?.({ cancelledBy: "receiver", message: msg.message });
|
|
1468
|
+
cleanup();
|
|
1401
1469
|
}
|
|
1402
1470
|
});
|
|
1403
1471
|
conn.on("open", async () => {
|
|
@@ -1435,6 +1503,7 @@ async function startP2PSend(opts) {
|
|
|
1435
1503
|
if (isStopped()) return;
|
|
1436
1504
|
const slice = file.slice(offset, offset + chunkSize);
|
|
1437
1505
|
const buf = await slice.arrayBuffer();
|
|
1506
|
+
if (isStopped()) return;
|
|
1438
1507
|
conn.send(buf);
|
|
1439
1508
|
sentBytes += buf.byteLength;
|
|
1440
1509
|
if (dc) {
|
|
@@ -1484,14 +1553,14 @@ async function startP2PSend(opts) {
|
|
|
1484
1553
|
safeError(err);
|
|
1485
1554
|
});
|
|
1486
1555
|
conn.on("close", () => {
|
|
1487
|
-
if (state === "closed" || state === "completed") {
|
|
1556
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1488
1557
|
cleanup();
|
|
1489
1558
|
return;
|
|
1490
1559
|
}
|
|
1491
1560
|
if (state === "transferring" || state === "finishing") {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
);
|
|
1561
|
+
state = "cancelled";
|
|
1562
|
+
onCancel?.({ cancelledBy: "receiver" });
|
|
1563
|
+
cleanup();
|
|
1495
1564
|
} else {
|
|
1496
1565
|
activeConn = null;
|
|
1497
1566
|
state = "listening";
|
|
@@ -1533,7 +1602,8 @@ async function startP2PReceive(opts) {
|
|
|
1533
1602
|
onProgress,
|
|
1534
1603
|
onComplete,
|
|
1535
1604
|
onError,
|
|
1536
|
-
onDisconnect
|
|
1605
|
+
onDisconnect,
|
|
1606
|
+
onCancel
|
|
1537
1607
|
} = opts;
|
|
1538
1608
|
if (!code) {
|
|
1539
1609
|
throw new DropgateValidationError("No sharing code was provided.");
|
|
@@ -1590,7 +1660,7 @@ async function startP2PReceive(opts) {
|
|
|
1590
1660
|
}
|
|
1591
1661
|
};
|
|
1592
1662
|
const safeError = (err) => {
|
|
1593
|
-
if (state === "closed" || state === "completed") return;
|
|
1663
|
+
if (state === "closed" || state === "completed" || state === "cancelled") return;
|
|
1594
1664
|
state = "closed";
|
|
1595
1665
|
onError?.(err);
|
|
1596
1666
|
cleanup();
|
|
@@ -1622,8 +1692,18 @@ async function startP2PReceive(opts) {
|
|
|
1622
1692
|
window.addEventListener("beforeunload", handleUnload);
|
|
1623
1693
|
}
|
|
1624
1694
|
const stop = () => {
|
|
1625
|
-
if (state === "closed") return;
|
|
1626
|
-
|
|
1695
|
+
if (state === "closed" || state === "cancelled") return;
|
|
1696
|
+
const wasActive = state === "transferring";
|
|
1697
|
+
state = "cancelled";
|
|
1698
|
+
try {
|
|
1699
|
+
if (activeConn && activeConn.open) {
|
|
1700
|
+
activeConn.send({ t: "cancelled", message: "Receiver cancelled the transfer." });
|
|
1701
|
+
}
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
if (wasActive && onCancel) {
|
|
1705
|
+
onCancel({ cancelledBy: "receiver" });
|
|
1706
|
+
}
|
|
1627
1707
|
cleanup();
|
|
1628
1708
|
};
|
|
1629
1709
|
peer.on("error", (err) => {
|
|
@@ -1705,6 +1785,13 @@ async function startP2PReceive(opts) {
|
|
|
1705
1785
|
if (msg.t === "error") {
|
|
1706
1786
|
throw new DropgateNetworkError(msg.message || "Sender reported an error.");
|
|
1707
1787
|
}
|
|
1788
|
+
if (msg.t === "cancelled") {
|
|
1789
|
+
if (state === "cancelled" || state === "closed" || state === "completed") return;
|
|
1790
|
+
state = "cancelled";
|
|
1791
|
+
onCancel?.({ cancelledBy: "sender", message: msg.message });
|
|
1792
|
+
cleanup();
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1708
1795
|
return;
|
|
1709
1796
|
}
|
|
1710
1797
|
let bufPromise;
|
|
@@ -1750,12 +1837,14 @@ async function startP2PReceive(opts) {
|
|
|
1750
1837
|
}
|
|
1751
1838
|
});
|
|
1752
1839
|
conn.on("close", () => {
|
|
1753
|
-
if (state === "closed" || state === "completed") {
|
|
1840
|
+
if (state === "closed" || state === "completed" || state === "cancelled") {
|
|
1754
1841
|
cleanup();
|
|
1755
1842
|
return;
|
|
1756
1843
|
}
|
|
1757
1844
|
if (state === "transferring") {
|
|
1758
|
-
|
|
1845
|
+
state = "cancelled";
|
|
1846
|
+
onCancel?.({ cancelledBy: "sender" });
|
|
1847
|
+
cleanup();
|
|
1759
1848
|
} else if (state === "negotiating") {
|
|
1760
1849
|
state = "closed";
|
|
1761
1850
|
cleanup();
|