@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
JavaScript
;
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;