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