diff --git a/src/components/pages/NodeFilesPage.vue b/src/components/pages/NodeFilesPage.vue index 46d3069..89382f2 100644 --- a/src/components/pages/NodeFilesPage.vue +++ b/src/components/pages/NodeFilesPage.vue @@ -75,12 +75,12 @@
- + - + @@ -117,12 +117,12 @@ import NodeIcon from "../nodes/NodeIcon.vue"; import Page from "./Page.vue"; import NodeUtils from "../../js/NodeUtils.js"; import NodeDropDownMenu from "../nodes/NodeDropDownMenu.vue"; -import NodeAPI from "../../js/NodeAPI.js"; import TextButton from "../TextButton.vue"; import DialogUtils from "../../js/DialogUtils.js"; import SaveButton from "../SaveButton.vue"; import FileTransferAPI from "../../js/FileTransferAPI.js"; import IconButton from "../IconButton.vue"; +import FileTransferrer from "../../js/FileTransferrer.js"; export default { name: 'NodeFilesPage', @@ -197,61 +197,24 @@ export default { return; } - // generate random file transfer id - const fileTransferId = NodeAPI.generatePacketId(); - - // get file details - const to = parseInt(this.nodeId); - const fileName = file.name; - const fileData = new Uint8Array(await file.arrayBuffer()); - const fileSize = fileData.length; - + // offer file try { - - // add to file transfers list - GlobalState.fileTransfers.push({ - id: fileTransferId, - to: to, - from: GlobalState.myNodeId, - direction: "outgoing", - status: "offering", - filename: fileName, - filesize: fileSize, - progress: 0, - data: fileData, - }); - - // send data - await FileTransferAPI.sendFileTransferRequest(to, fileTransferId, fileName, fileSize); - + await FileTransferrer.offerFileTransfer(this.nodeId, file); } catch(e) { DialogUtils.showErrorAlert(e); } + }, async acceptFileTransfer(fileTransfer) { try { - - // mark as accepted - fileTransfer.status = "accepted"; - - // tell remote node we rejected file transfer - await FileTransferAPI.acceptFileTransfer(fileTransfer.from, fileTransfer.id); - + await FileTransferrer.acceptFileTransfer(fileTransfer); } catch(e) { - console.log(e); + DialogUtils.showErrorAlert(e); } }, async rejectFileTransfer(fileTransfer) { try { - - // remove from ui - GlobalState.fileTransfers = GlobalState.fileTransfers.filter((existingFileTransfer) => { - return existingFileTransfer.id !== fileTransfer.id; - }); - - // tell remote node we rejected file transfer - await FileTransferAPI.rejectFileTransfer(fileTransfer.from, fileTransfer.id); - + await FileTransferrer.rejectFileTransfer(fileTransfer); } catch(e) { console.log(e); } @@ -263,37 +226,21 @@ export default { return; } - // remove from ui - GlobalState.fileTransfers = GlobalState.fileTransfers.filter((existingFileTransfer) => { - return existingFileTransfer.id !== fileTransfer.id; - }); - - // do nothing if already completed - if(fileTransfer.status === "completed"){ - return; - } - try { - - // tell remote node we cancelled the file transfer await FileTransferAPI.cancelFileTransfer(fileTransfer.to, fileTransfer.id); - } catch(e) { console.log(e); } }, - async removeFileTransfer(fileTransfer) { + removeFileTransfer(fileTransfer) { // ask user to confirm if(!confirm("Are you sure you want to remove this file transfer?")){ return; } - // remove from ui - GlobalState.fileTransfers = GlobalState.fileTransfers.filter((existingFileTransfer) => { - return existingFileTransfer.id !== fileTransfer.id; - }); + FileTransferrer.removeFileTransfer(fileTransfer); }, }, diff --git a/src/js/Connection.js b/src/js/Connection.js index b80e496..1819fc0 100644 --- a/src/js/Connection.js +++ b/src/js/Connection.js @@ -4,6 +4,7 @@ import Database from "./Database.js"; import NodeAPI from "./NodeAPI.js"; import PacketUtils from "./PacketUtils.js"; import FileTransferAPI from "./FileTransferAPI.js"; +import FileTransferrer from "./FileTransferrer.js"; class Connection { @@ -511,8 +512,8 @@ class Connection { id: fileTransferOffer.id, to: meshPacket.to, from: meshPacket.from, - direction: "incoming", - status: "offering", + direction: FileTransferrer.DIRECTION_INCOMING, + status: FileTransferrer.STATUS_OFFERING, filename: fileTransferOffer.fileName, filesize: fileTransferOffer.fileSize, progress: 0, @@ -546,12 +547,12 @@ class Connection { const totalParts = Math.ceil(fileTransfer.data.length / maxAcceptablePartSize); // update file transfer status - fileTransfer.status = "accepted"; + fileTransfer.status = FileTransferrer.STATUS_ACCEPTED; fileTransfer.total_parts = totalParts; fileTransfer.max_acceptable_part_size = maxAcceptablePartSize; // send first file part - await this.sendFilePart(fileTransfer, 0); + await FileTransferrer.sendFilePart(fileTransfer, 0); } @@ -570,7 +571,7 @@ class Connection { console.log(`[FileTransfer] ${fileTransfer.id} rejected`); // update file transfer status - fileTransfer.status = "rejected"; + fileTransfer.status = FileTransferrer.STATUS_REJECTED; } @@ -589,7 +590,7 @@ class Connection { console.log(`[FileTransfer] ${fileTransfer.id} cancelled`); // remove cancelled file transfer if it was in offering state - if(fileTransfer.status === "offering"){ + if(fileTransfer.status === FileTransferrer.STATUS_OFFERING){ GlobalState.fileTransfers = GlobalState.fileTransfers.filter((existingFileTransfer) => { return existingFileTransfer.id !== fileTransfer.id; }); @@ -597,7 +598,7 @@ class Connection { } // update file transfer status - fileTransfer.status = "cancelled"; + fileTransfer.status = FileTransferrer.STATUS_CANCELLED; } @@ -616,7 +617,7 @@ class Connection { console.log(`[FileTransfer] ${fileTransfer.id} completed`); // update file transfer status - fileTransfer.status = "complete"; + fileTransfer.status = FileTransferrer.STATUS_COMPLETED; } @@ -638,23 +639,23 @@ class Connection { fileTransfer.chunks[filePart.partIndex] = filePart.data; // update file transfer status - fileTransfer.status = "receiving"; + fileTransfer.status = FileTransferrer.STATUS_RECEIVING; fileTransfer.progress = Math.ceil((filePart.partIndex + 1) / filePart.totalParts * 100); // check if complete // todo, check if all chunks received, and request others if not? if(filePart.partIndex === filePart.totalParts - 1){ - fileTransfer.status = "complete"; + fileTransfer.status = FileTransferrer.STATUS_COMPLETED; fileTransfer.blob = new Blob(Object.values(fileTransfer.chunks), { type: "application/octet-stream", }); - await this.completeFileTransfer(fileTransfer); + await FileTransferrer.completeFileTransfer(fileTransfer); return; } // request next part const nextFilePartIndex = filePart.partIndex + 1; - await this.requestFileParts(fileTransfer, [ + await FileTransferrer.requestFileParts(fileTransfer, [ nextFilePartIndex, ]); @@ -680,80 +681,16 @@ class Connection { console.log(`[FileTransfer] ${fileTransfer.id} sending part ${partIndex}`); // send file part - await this.sendFilePart(fileTransfer, partIndex); + await FileTransferrer.sendFilePart(fileTransfer, partIndex); // update file transfer progress - fileTransfer.status = "sending"; + fileTransfer.status = FileTransferrer.STATUS_SENDING; fileTransfer.progress = Math.ceil((partIndex + 1) / fileTransfer.total_parts * 100); } } - static async completeFileTransfer(fileTransfer) { - try { - - // tell remote node we completed the file transfer - for(var attempt = 0; attempt < 3; attempt++){ - try { - await FileTransferAPI.completeFileTransfer(fileTransfer.from, fileTransfer.id); - console.log(`completeFileTransfer attempt ${attempt + 1} success`); - break; - } catch(e) { - console.log(`completeFileTransfer attempt ${attempt + 1} failed`); - } - } - - } catch(e) { - console.log(e); - } - } - - static async requestFileParts(fileTransfer, partIndexes) { - try { - - // ask remote node for parts - for(var attempt = 0; attempt < 3; attempt++){ - try { - await FileTransferAPI.requestFileParts(fileTransfer.from, fileTransfer.id, partIndexes); - console.log(`requestFileParts attempt ${attempt + 1} success`); - break; - } catch(e) { - console.log(`requestFileParts attempt ${attempt + 1} failed`); - } - } - - } catch(e) { - console.log(e); - } - } - - static async sendFilePart(fileTransfer, partIndex) { - try { - - // get data for this part - const partSize = fileTransfer.max_acceptable_part_size; - const start = partIndex * partSize; - const end = start + partSize; - const partData = fileTransfer.data.slice(start, end); - - // send part to remote node - for(var attempt = 0; attempt < 3; attempt++){ - try { - await FileTransferAPI.sendFilePart(fileTransfer.to, fileTransfer.id, partIndex, fileTransfer.total_parts, partData); - console.log(`sendFilePart attempt ${attempt + 1} success`); - break; - } catch(e) { - console.log(`sendFilePart attempt ${attempt + 1} failed`); - } - } - - - } catch(e) { - console.log(e); - } - } - } export default Connection; diff --git a/src/js/FileTransferrer.js b/src/js/FileTransferrer.js new file mode 100644 index 0000000..c2d14d8 --- /dev/null +++ b/src/js/FileTransferrer.js @@ -0,0 +1,211 @@ +import NodeAPI from "./NodeAPI.js"; +import GlobalState from "./GlobalState.js"; +import FileTransferAPI from "./FileTransferAPI.js"; + +class FileTransferrer { + + static DIRECTION_INCOMING = "incoming"; + static DIRECTION_OUTGOING = "outgoing"; + + static STATUS_OFFERING = "offering"; + static STATUS_OFFERED = "offered"; + static STATUS_ACCEPTED = "accepted"; + static STATUS_REJECTED = "rejected"; + static STATUS_CANCELLED = "cancelled"; + static STATUS_COMPLETED = "completed"; + static STATUS_SENDING = "sending"; + static STATUS_RECEIVING = "receiving"; + + static MAX_PACKET_ATTEMPTS = 3; + + static log(message) { + console.log(`[FileTransferrer] ${message}`); + } + + static async offerFileTransfer(to, file) { + + // generate random file transfer id + const fileTransferId = NodeAPI.generatePacketId(); + + // get file details + to = parseInt(to); + const fileName = file.name; + const fileData = new Uint8Array(await file.arrayBuffer()); + const fileSize = fileData.length; + + const fileTransfer = { + id: fileTransferId, + to: to, + from: GlobalState.myNodeId, + direction: this.DIRECTION_OUTGOING, + status: this.STATUS_OFFERING, + filename: fileName, + filesize: fileSize, + progress: 0, + data: fileData, + }; + + // add to file transfers list + GlobalState.fileTransfers.push(fileTransfer); + + // send file transfer request + for(var attempt = 1; attempt <= this.MAX_PACKET_ATTEMPTS; attempt++){ + try { + this.log(`offerFileTransfer attempt ${attempt}`); + await FileTransferAPI.sendFileTransferRequest(to, fileTransferId, fileName, fileSize); + this.log(`offerFileTransfer attempt ${attempt} success`); + fileTransfer.status = this.STATUS_OFFERED; + return; + } catch(e) { + console.log(e); + if(attempt === this.MAX_PACKET_ATTEMPTS){ + + this.log("offerFileTransfer failed", e); + + // remove file transfer + GlobalState.fileTransfers = GlobalState.fileTransfers.filter((fileTransfer) => { + return fileTransfer.id !== fileTransferId; + }); + + // rethrow exception + throw e; + + } + } + } + + } + + static async acceptFileTransfer(fileTransfer) { + + for(var attempt = 1; attempt <= this.MAX_PACKET_ATTEMPTS; attempt++){ + try { + this.log(`acceptFileTransfer attempt ${attempt}`); + await FileTransferAPI.acceptFileTransfer(fileTransfer.from, fileTransfer.id); + fileTransfer.status = this.STATUS_ACCEPTED; + return; + } catch(e) { + console.log(e); + if(attempt === this.MAX_PACKET_ATTEMPTS){ + this.log("acceptFileTransfer failed", e); + throw e; + } + } + } + + } + + static async rejectFileTransfer(fileTransfer) { + + // remove from ui + GlobalState.fileTransfers = GlobalState.fileTransfers.filter((existingFileTransfer) => { + return existingFileTransfer.id !== fileTransfer.id; + }); + + for(var attempt = 1; attempt <= this.MAX_PACKET_ATTEMPTS; attempt++){ + try { + this.log(`rejectFileTransfer attempt ${attempt}`); + await FileTransferAPI.acceptFileTransfer(fileTransfer.from, fileTransfer.id); + fileTransfer.status = this.STATUS_ACCEPTED; + return; + } catch(e) { + console.log(e); + if(attempt === this.MAX_PACKET_ATTEMPTS){ + this.log("rejectFileTransfer failed", e); + throw e; + } + } + } + + } + + static async cancelFileTransfer(fileTransfer) { + + fileTransfer.status = this.STATUS_CANCELLED; + + for(var attempt = 1; attempt <= this.MAX_PACKET_ATTEMPTS; attempt++){ + try { + this.log(`cancelFileTransfer attempt ${attempt}`); + await FileTransferAPI.cancelFileTransfer(fileTransfer.to, fileTransfer.id); + return; + } catch(e) { + console.log(e); + if(attempt === this.MAX_PACKET_ATTEMPTS){ + this.log("cancelFileTransfer failed", e); + throw e; + } + } + } + + } + + static removeFileTransfer(fileTransfer) { + GlobalState.fileTransfers = GlobalState.fileTransfers.filter((existingFileTransfer) => { + return existingFileTransfer.id !== fileTransfer.id; + }); + } + + static async sendFilePart(fileTransfer, partIndex) { + try { + + // get data for this part + const partSize = fileTransfer.max_acceptable_part_size; + const start = partIndex * partSize; + const end = start + partSize; + const partData = fileTransfer.data.slice(start, end); + + // send part to remote node + for(var attempt = 1; attempt <= this.MAX_PACKET_ATTEMPTS; attempt++){ + try { + this.log(`sendFilePart attempt ${attempt}`); + await FileTransferAPI.sendFilePart(fileTransfer.to, fileTransfer.id, partIndex, fileTransfer.total_parts, partData); + return; + } catch(e) { + console.log(e); + if(attempt === this.MAX_PACKET_ATTEMPTS){ + this.log("sendFilePart failed", e); + throw e; + } + } + } + + } catch(e) { + console.log(e); + } + } + + static async requestFileParts(fileTransfer, partIndexes) { + for(var attempt = 1; attempt <= this.MAX_PACKET_ATTEMPTS; attempt++){ + try { + this.log(`requestFileParts attempt ${attempt}`); + await FileTransferAPI.requestFileParts(fileTransfer.from, fileTransfer.id, partIndexes); + return; + } catch(e) { + console.log(e); + if(attempt === this.MAX_PACKET_ATTEMPTS){ + this.log("requestFileParts failed", e); + throw e; + } + } + } + } + + static async completeFileTransfer(fileTransfer) { + for(var attempt = 1; attempt <= this.MAX_PACKET_ATTEMPTS; attempt++){ + try { + this.log(`completeFileTransfer attempt ${attempt}`); + await FileTransferAPI.completeFileTransfer(fileTransfer.from, fileTransfer.id); + return; + } catch(e) { + console.log(e); + if(attempt === this.MAX_PACKET_ATTEMPTS){ + this.log("completeFileTransfer failed", e); + throw e; + } + } + } + } + +} + +export default FileTransferrer;