UNPKG

@trap_stevo/filetide

Version:

Revolutionizing real-time file transfer with seamless, instant communication across any device. Deliver files instantly, regardless of platform, and experience unparalleled speed and control in managing transfers. Elevate your file-sharing capabilities wi

319 lines (317 loc) 15 kB
"use strict"; const { HUDIoTide } = require("@trap_stevo/iotide-client"); const chalk = require("chalk"); const path = require("path"); const fs = require("fs"); const { FileTideDebugUtilityManager, significantMessageColors, errorMessageColors } = require("./HUDManagers/FileTideDebugUtilityManager"); const { FileTransferTrackingManager } = require("./HUDManagers/FileTransferTrackingManager.js"); const FileTransferRecoveryManager = require("./HUDManagers/FileTransferRecoveryManager"); const { FileMessagerConfigManager } = require("./HUDManagers/FileMessagerConfigManager"); const { FileTransferManager } = require("./HUDManagers/FileTransferManager"); const { FileUtilityManager } = require("./HUDManagers/FileUtilityManager"); class FileMessagerClient { constructor(options = {}, transportOptions = { parallelChunks: 3, maxRetries: 3 }, messagerSettings = { maxReconnectionAttempts: 15, reconnectionAttempts: 15, maxReconnectionDelay: 10000 }) { const { maxReconnectionAttempts = 15, reconnectionAttempts = 15, maxReconnectionDelay = 10000, ...messagerConfigurations } = messagerSettings; this.clientOptions = FileMessagerConfigManager.getClientOptions(options); this.transporter = new FileTransferManager(transportOptions); this.activeTransferTypes = new Map(); this.clientTide = new HUDIoTide(maxReconnectionAttempts, reconnectionAttempts, maxReconnectionDelay, { persistentTideIDs: true, persistentTideID: true, ...messagerConfigurations }); this.clients = new Map(); } createClientInstance(clientID, clientOptions, options = {}, headers = {}, onConnect = null, onDisconnect = null, authURL = "", authHeaders = {}, useAuthentication = false, queryAuthToken = false) { this.clientTide.createIO(clientID, clientOptions.url, { transports: ["websocket"], ...options }, headers, authURL, authHeaders, useAuthentication, queryAuthToken, onConnect, async () => { FileTransferTrackingManager.outputCurrentTransfers(); FileTransferRecoveryManager.flushToDisk(); if (onDisconnect) { await onDisconnect(); } }); FileTideDebugUtilityManager.outputGradient(`\n[FileTide ~ File Messager] ~ Created client with ID ${clientID} and socket ${clientID}!`, significantMessageColors); return clientID; } joinRoom(userID, roomName, options = {}, headers = {}, onConnect = null, onDisconnect = null, authURL = "", authHeaders = {}, useAuthentication = false, queryAuthToken = false) { const socketName = this.createClientInstance(userID, { url: this.clientOptions.url }, options, headers, onConnect, onDisconnect, authURL, authHeaders, useAuthentication, queryAuthToken); return () => { this.clientTide.joinChannel(socketName, roomName, userID, () => { FileTideDebugUtilityManager.outputGradient(`\n[FileTide ~ File Messager] ~ Client ~ ${userID} joined room ${roomName} successfully!`, significantMessageColors); this.clients.set(userID, { roomName }); }); }; } async requestTransfer(clientID, senderID, filePath, destinationPath, filterContent = [], transferRequestExpirationTime = 30000) { const transferContentType = FileUtilityManager.inferPathType(filePath); const transferAllowed = await this.allowedTransfer(clientID, senderID, destinationPath, filePath, "N/A", transferContentType === "file" ? 1 : "N/A", transferContentType, "requestSend", transferRequestExpirationTime); if (!transferAllowed) { return; } this.clientTide.emitEvent(clientID, "request-transfer-from-client", { requesterID: clientID, senderID, destinationPath, filterContent, filePath }); return; } listFiles(clientID, recipientID, depth = 1) { this.clientTide.emitEvent(clientID, "list-client-files", { requesterID: clientID, recipientID, depth }); return; } async allowedTransfer(clientID, recipientID, filePath, originPath, fileSize, fileCount = 1, transferContentType = "file", transferType = "send", transferExpirationTime = 30000) { try { const currentDate = Date.now(); const transferID = `${clientID}_${filePath}_${currentDate}`; const transferAccepted = await this.clientTide.emitEventWithResponse(clientID, "get-client-transfer-barrier-response", "transfer-barrier-response", { clientID, recipientID, transferID, originPath, path: filePath, date: currentDate, transferExpirationTime, timeUntilTransferExpiration: transferExpirationTime, itemCount: fileCount, itemSize: fileSize, transferContentType, transferType }, transferExpirationTime); if (!transferAccepted) { FileTideDebugUtilityManager.outputGradient(`[FileTide ~ File Messager] ${recipientID} denied your transfer.`, errorMessageColors); return false; } else if (transferAccepted) { FileTideDebugUtilityManager.outputGradient(`[FileTide ~ File Messager] ${recipientID} accepted your transfer!`, significantMessageColors); return true; } } catch (error) { if (error.message.startsWith("Timeout: No response")) { FileTideDebugUtilityManager.outputGradient("[FileTide ~ File Messager] Transfer approval timed out.", errorMessageColors); return false; } FileTideDebugUtilityManager.outputGradient(`[FileTide ~ File Messager] Transfer approval refused.\n${error}`, errorMessageColors); return false; } } /** * Sends all files in a directory in parallel using the sendFile method. * @param {string} clientID - The ID of the client sending the files. * @param {string} recipientId - The ID of the recipient. * @param {Array} filesData - Array of file data objects from the directory. * @param {string} baseDirectory - The base directory path that contains the files. * @param {string} destinationPath - The path to which the files should get sent. * @param {Number} tideSize - The tide size in sending files (500). * @param {Number} minTideSize - The minimum tide size in sending files (10). * @param {Number} maxTideSize - The maximum tide size in sending files (2000). * @param {Number} transferRequestExpirationTime - The maximum time until the transfer barrier times out the transfer request. */ async sendDirectoryFiles(clientID, recipientId, filesData, baseDirectory, destinationPath, tideSize = 500, minTideSize = 10, maxTideSize = 2000, transferRequestExpirationTime = 30000) { try { const baseDirectoryName = path.basename(baseDirectory); const adjustedDestinationPath = FileUtilityManager.normalizePath(path.join(destinationPath, baseDirectoryName)); let totalSize = 0; filesData.forEach(fileInfo => { totalSize += fileInfo.size; }); const incomingType = this.activeTransferTypes.get(destinationPath); const transferAllowed = incomingType && incomingType === "requestedTransfer" ? true : await this.allowedTransfer(clientID, recipientId, adjustedDestinationPath, baseDirectory, totalSize, filesData.length, "directory", "send", transferRequestExpirationTime); if (incomingType) { this.activeTransferTypes.delete(destinationPath); } if (!transferAllowed) { return; } const sendFilePromises = filesData.map(async (fileInfo, index) => { const relativeFilePath = path.dirname(path.relative(baseDirectory, fileInfo.filePath)); const fileDestinationPath = FileUtilityManager.normalizePath(path.join(adjustedDestinationPath, relativeFilePath)); if (fileInfo.largeFile) { return await this.sendDirectoryLargeFile(clientID, recipientId, fileInfo.fileName, fileInfo.filePath, fileDestinationPath, fileInfo.fileSize, index, tideSize, minTideSize, maxTideSize, transferRequestExpirationTime); } return await this.sendDirectoryFile(clientID, recipientId, fileInfo.fileName, fileInfo.filePath, fileInfo.size, fileDestinationPath, index, tideSize, minTideSize, maxTideSize, transferRequestExpirationTime); }); Promise.all(sendFilePromises).then(() => console.log("\nAll files in directory transferred successfully!")).catch(error => console.error("Did not transfer directory: ", error)); } catch (error) { FileTideDebugUtilityManager.outputGradient(`[FileTide ~ File Messager] ~ Refused directory transfer: ${error}`, errorMessageColors); } } async sendDirectoryLargeFile(clientID, recipientId, fileName, filePath = process.cwd(), destinationPath = null, fileSize, index = 0, tideSize = 500, minTideSize = 10, maxTideSize = 2000, transferRequestExpirationTime = 30000) { let initiatingTransfer = false; if (index === 0) { initiatingTransfer = true; } return await this.sendFile(clientID, recipientId, fileName, filePath, destinationPath, fileSize, tideSize, minTideSize, maxTideSize, initiatingTransfer, true, true, true, transferRequestExpirationTime); } async sendDirectoryFile(clientID, recipientId, fileName, filePath = process.cwd(), fileSize, directoryPath = null, index = 0, tideSize = 200, minTideSize = 10, maxTideSize = 2000, transferRequestExpirationTime = 30000) { if (!filePath) { return; } let initiatingTransfer = false; if (index === 0) { this.clientTide.emitEvent(clientID, "transfer-start", { fileName, senderID: clientID, recipientID: recipientId, path: directoryPath || filePath, fileSize }); initiatingTransfer = true; } return await this.sendFile(clientID, recipientId, fileName, filePath, directoryPath, fileSize, tideSize, minTideSize, maxTideSize, initiatingTransfer, true, true, false, transferRequestExpirationTime); } async sendFile(clientID, recipientId, fileName, filePath = process.cwd(), destination = process.cwd(), fileSize, tideSize = 500, minTideSize = 10, maxTideSize = 2000, initiatingTransfer = true, inDirectory = false, largeFile = false, transferRequestExpirationTime = 30000) { const destinationPath = FileUtilityManager.normalizePath(path.join(destination, fileName)); FileTideDebugUtilityManager.outputGradient(largeFile ? `[FileTide ~ File Messager] ~ Requesting to transfer large file ~ ${destinationPath} to ${recipientId}!` : `[FileTide ~ File Messager] ~ Requesting to transfer ~ ${destinationPath} to ${recipientId}!`, significantMessageColors); const incomingType = this.activeTransferTypes.get(filePath); if (!inDirectory && initiatingTransfer) { const transferAllowed = incomingType && incomingType === "requestedTransfer" ? true : await this.allowedTransfer(clientID, recipientId, destinationPath, filePath, fileSize, 1, "file", "send", transferRequestExpirationTime); if (incomingType) { this.activeTransferTypes.delete(filePath); } if (!transferAllowed) { return; } this.clientTide.emitEvent(clientID, "transfer-start", { fileName, senderID: clientID, recipientID: recipientId, path: destination, fileSize }); } else if (inDirectory && !initiatingTransfer) { if (incomingType) { this.activeTransferTypes.delete(filePath); } } const fileHandle = await fs.promises.open(filePath, "r"); const fileDetails = { destinationPath, name: fileName, path: filePath, size: fileSize }; try { let lastSendTime = Date.now(); const maxBatchSize = maxTideSize; const minBatchSize = minTideSize; let batchSize = tideSize > maxBatchSize || tideSize > minBatchSize ? tideSize : tideSize <= minBatchSize ? minBatchSize : 10; let transferInitiated = false; const chunkBatch = []; await this.transporter.sendLargeFile(fileHandle, { recipientID: recipientId, originDetails: { recipientID: recipientId, senderID: clientID }, fileDetails, onSendChunk: (transferId, chunkIndex, chunkSize, chunkData, totalChunks, completedChunks) => { return new Promise(resolve => { chunkBatch.push({ senderID: clientID, recipientId, fileName, fileChunk: chunkData, completedChunks, totalChunks, chunkIndex, chunkSize, filePath: destination, fileSize }); if (!transferInitiated) { this.clientTide.emitEvent(clientID, "client-to-client-transfer", { chunkBatch }); transferInitiated = true; chunkBatch.length = 0; } if (chunkBatch.length >= batchSize) { const now = Date.now(); const rtt = now - lastSendTime; lastSendTime = now; this.clientTide.emitEvent(clientID, "client-to-client-transfer", { chunkBatch }); chunkBatch.length = 0; if (rtt >= 200 && batchSize < maxBatchSize) { batchSize = Math.min(batchSize + 50, maxBatchSize); } else if (rtt < 200 && batchSize > minBatchSize) { batchSize = Math.max(batchSize - 50, minBatchSize); } } resolve(); }); }, onComplete: transferId => { return new Promise(resolve => { if (chunkBatch.length > 0) { this.clientTide.emitEvent(clientID, "client-to-client-transfer", { chunkBatch }); chunkBatch.length = 0; } FileTransferRecoveryManager.clearTransferState(clientID, recipientId, `${filePath}-${fileName}`); this.clientTide.emitEvent(clientID, "transfer-complete", { senderID: clientID, recipientId, fileName, filePath: destination }); resolve(); }); }, onTiding: () => { this.clientTide.emitEvent(clientID, "client-tiding", { senderID: clientID, message: "Transfer ongoing..." }); } }, this.clientTide); FileTideDebugUtilityManager.outputGradient(largeFile ? `[FileTide ~ File Messager] ~ Large file ~ ${fileName} transfer complete!` : `[FileTide ~ File Messager] ~ File ~ ${fileName} transfer complete!`, significantMessageColors); } catch (error) { FileTideDebugUtilityManager.outputGradient(largeFile ? `[FileTide ~ File Messager] ~ Error transferring large file: ${error.message}` : `[FileTide ~ File Messager] ~ Error transferring file ~ ${error.message}`, errorMessageColors); } finally { await fileHandle.close(); } } } ; module.exports = FileMessagerClient;