local-file-transfer-protocol
Version:
A fast and reliable protocol for transferring files between computers on a local network with directory structure preservation
456 lines (387 loc) • 16.4 kB
JavaScript
var isServer = false;
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const net = require("net");
const interfaces = require("os").networkInterfaces();
const fs = require("fs");
const path = require("path");
const crypto = require('crypto');
function askQuestion(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer);
});
});
}
rl.on('SIGINT', () => process.exit());
async function main() {
if(!process.argv[2]) {
const Server = await askQuestion("ITS SERVER ? (y/n): ");
if (Server === "y" || Server === "Y" || Server === "yes" || Server === "YES")
isServer = true;
}else{
if(process.argv[2] === "server") isServer = true;
if(process.argv[2] === "client") isServer = false;
}
if (isServer) {
const folderpath = process.argv[3] ? process.argv[3] : await askQuestion("Enter the folder path: ");
// Check if folder exists
if (!fs.existsSync(folderpath)) {
console.error(`Error: Folder '${folderpath}' does not exist`);
process.exit(1);
}
function getAllFiles(dirPath, baseDir = '') {
const files = fs.readdirSync(dirPath);
const Directorys = new Set();
const Files = new Set();
files.forEach((file) => {
const fullPath = path.join(dirPath, file);
const relativePath = path.join(baseDir, file);
if (fs.statSync(fullPath).isDirectory()) {
Directorys.add(relativePath);
const subFiles = getAllFiles(fullPath, relativePath);
subFiles.directories.forEach(d => Directorys.add(d));
subFiles.files.forEach(f => Files.add(f));
} else {
Files.add(relativePath);
}
});
return {
directories: Array.from(Directorys),
files: Array.from(Files)
};
}
const temp = getAllFiles(folderpath);
const Files = temp.files;
const Directorys = temp.directories;
console.log(`Found ${Directorys.length} directories and ${Files.length} files`);
const server = net.createServer((socket) => {
console.log("Client connected");
// Better directory format with JSON
socket.write(JSON.stringify({ directories: Directorys }));
var speed = 0;
var currentFile = null;
const speedInterval = setInterval(() => {
console.log("Speed:",(speed/1024/1024).toFixed(2)+"MB/s",currentFile);
speed = 0;
}, 1000);
socket.on("close", () => {
clearInterval(speedInterval);
});
// Improved chunk-based transfer system
let pendingAck = false;
let chunkQueue = [];
let currentChunkId = 0;
let fileReadStream = null;
let bytesTransferred = 0;
let fileSize = 0;
let fileHash = null;
// Function to send the next chunk (if any)
function sendNextChunk() {
if (pendingAck || chunkQueue.length === 0) return;
const nextChunk = chunkQueue.shift();
currentChunkId = nextChunk.id;
pendingAck = true;
// Format: [0x01][chunk-id(4 bytes)][data]
const header = Buffer.alloc(5);
header[0] = 0x01;
header.writeUInt32LE(nextChunk.id, 1);
const result = socket.write(Buffer.concat([header, nextChunk.data]));
if (!result && fileReadStream) {
fileReadStream.pause();
}
}
socket.on("data", (data) => {
const message = data.toString().trim();
// Handle chunk acknowledgment
if (message.startsWith("ACK:")) {
const ackId = parseInt(message.split(":")[1]);
if (ackId === currentChunkId) {
pendingAck = false;
if (fileReadStream && fileReadStream.isPaused()) {
fileReadStream.resume();
}
sendNextChunk();
}
return;
}
console.log("Received from client:", message);
if(message === "iamokey") {
if(Files.length === 0) {
console.log("No more files to send");
socket.write(Buffer.from([0x03]));
return;
}
const file = Files.shift();
if(file && fs.existsSync(path.join(folderpath, file))) {
console.log("Sending file:", file);
const filePath = path.join(folderpath, file);
// Send file with metadata (name and size)
const stats = fs.statSync(filePath);
socket.write(`FILE:${file}:${stats.size}`);
} else {
console.log("File not found:", file);
socket.write("iamokey"); // Ask for next file
}
} else if(message.startsWith("READY:")) {
// Client is ready to receive the requested file
const requestedFile = message.replace("READY:", "");
console.log("Client ready for file:", requestedFile);
// Reset transfer state
currentFile = requestedFile;
chunkQueue = [];
currentChunkId = 0;
pendingAck = false;
bytesTransferred = 0;
const filePath = path.join(folderpath, requestedFile);
if(!fs.existsSync(filePath)) {
console.log("File not found:", filePath);
socket.write(Buffer.from([0x03]));
return;
}
fileSize = fs.statSync(filePath).size;
fileHash = crypto.createHash('md5');
// Create a smaller chunk size for better reliability
fileReadStream = fs.createReadStream(filePath, {
highWaterMark: 16 * 1024 // 16KB chunks for better reliability
});
let chunkId = 0;
fileReadStream.on("data", (chunk) => {
fileHash.update(chunk);
bytesTransferred += chunk.length;
speed += chunk.length;
// Add chunk to queue
chunkQueue.push({
id: chunkId++,
data: chunk
});
// If no pending ACK, send the next chunk
if (!pendingAck) {
sendNextChunk();
}
});
socket.on("drain", () => {
// Socket buffer drained, resume if needed
if (fileReadStream && fileReadStream.isPaused()) {
fileReadStream.resume();
}
});
fileReadStream.on("end", () => {
console.log("Reading file complete:", requestedFile,
`(${bytesTransferred}/${fileSize} bytes)`);
// Wait for all chunks to be sent before sending EOF
const checkComplete = setInterval(() => {
if (chunkQueue.length === 0 && !pendingAck) {
clearInterval(checkComplete);
// Send EOF message with file hash and size for verification
const md5Hash = fileHash.digest('hex');
socket.write(Buffer.concat([
Buffer.from([0x02]),
Buffer.from(`${md5Hash}:${bytesTransferred}:${fileSize}`)
]));
// Clean up
fileReadStream = null;
fileHash = null;
}
}, 100);
});
fileReadStream.on("error", (err) => {
console.error("Error reading file:", err);
socket.write(Buffer.from([0x03]));
fileReadStream = null;
});
}
});
socket.on("end", () => {
clearInterval(speedInterval);
console.log("Client disconnected");
});
socket.on("error", (err) => {
clearInterval(speedInterval);
if (err.code === "ECONNRESET") {
console.log("Client disconnected");
} else console.error("Socket error: ", err);
});
});
server.on("error", (err) => {
console.error("Server error: ", err);
process.exit();
});
const PORT = 18080;
server.listen(PORT, () => {
let found = false;
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (
iface.family === "IPv4" &&
!iface.internal &&
iface.address.startsWith("192.168.")
) {
console.log(`Server listening on ${iface.address}:${PORT}`);
found = true;
}
}
}
if (!found) {
console.log(`Server listening on localhost:${PORT}`);
}
});
} else {
const Serverip = await askQuestion("Enter the Server ip: 192.168.");
const Folderpath = process.argv[3] ? process.argv[3] : await askQuestion("Enter the folder path: ");
// Create destination folder if it doesn't exist
if (!fs.existsSync(Folderpath)) {
fs.mkdirSync(Folderpath, { recursive: true });
console.log(`Created destination folder: ${Folderpath}`);
}
const client = new net.Socket();
const PORT = 18080;
client.connect(PORT, "192.168."+Serverip, () => {
console.log(`Connected to server at 192.168.${Serverip}:${PORT}`);
});
client.once('data', (data) => {
try {
// Parse directory structure from JSON
const dirInfo = JSON.parse(data.toString());
console.log(`Received ${dirInfo.directories.length} directories`);
dirInfo.directories.forEach((element) => {
if (element && element.trim() !== '') {
const dirPath = path.join(Folderpath, element);
console.log(`Creating directory: ${dirPath}`);
fs.mkdirSync(dirPath, { recursive: true });
}
});
var fileStream = null;
var speed = 0;
var currentFile = null;
var expectedFileSize = 0;
var receivedBytes = 0;
var fileHash = null;
var lastChunkId = -1;
const speedInterval = setInterval(() => {
const percentage = expectedFileSize > 0 ? ((receivedBytes / expectedFileSize) * 100).toFixed(2) : 0;
console.log(`Speed: ${(speed/1024/1024).toFixed(2)}MB/s ${currentFile} ` +
`${receivedBytes}/${expectedFileSize} bytes (${percentage}%)`);
speed = 0;
}, 1000);
client.on("close", () => {
clearInterval(speedInterval);
if (fileStream) fileStream.end();
});
console.log("Requesting first file...");
client.write("iamokey");
client.on("data", (data) => {
// Handle control bytes
if (data[0] === 0x01) { // File data chunk
if (!fileStream) {
console.error("Received file data without an open file");
return;
}
// Extract chunk ID from header (4 bytes after control byte)
const chunkId = data.readUInt32LE(1);
const chunk = data.slice(5); // Skip control byte and chunk ID
// Only process if this is the next expected chunk or we haven't received any chunks yet
if (chunkId === lastChunkId + 1) {
if (fileHash) {
fileHash.update(chunk);
}
receivedBytes += chunk.length;
speed += chunk.length;
lastChunkId = chunkId;
fileStream.write(chunk, (err) => {
if (err) {
console.error("Error writing to file:", err);
}
});
} else {
console.warn(`Received out-of-order chunk. Expected: ${lastChunkId + 1}, Got: ${chunkId}`);
}
// Send acknowledgment for the chunk we received
client.write(`ACK:${chunkId}`);
return;
}
if (data[0] === 0x02) { // File complete
const endInfo = data.slice(1).toString();
const [hashValue, sentSize, originalSize] = endInfo.split(':');
console.log(`File transfer complete: ${currentFile}`);
console.log(`Received: ${receivedBytes} bytes, Expected: ${expectedFileSize} bytes`);
if (fileStream) {
// Ensure all data is written before closing
fileStream.end(() => {
const actualSize = fs.statSync(path.join(Folderpath, currentFile)).size;
const calculatedHash = fileHash ? fileHash.digest('hex') : '';
if (receivedBytes !== parseInt(sentSize) ||
receivedBytes !== actualSize ||
receivedBytes !== parseInt(originalSize)) {
console.error(`⚠️ FILE SIZE MISMATCH! Original: ${originalSize}, Sent: ${sentSize}, Received: ${receivedBytes}, Actual: ${actualSize}`);
} else if (calculatedHash !== hashValue) {
console.error(`⚠️ FILE HASH MISMATCH! Expected: ${hashValue}, Actual: ${calculatedHash}`);
} else {
console.log(`✓ File integrity verified (${actualSize} bytes)`);
}
fileStream = null;
currentFile = null;
receivedBytes = 0;
expectedFileSize = 0;
fileHash = null;
lastChunkId = -1;
client.write("iamokey");
});
}
return;
}
if (data[0] === 0x03) { // No more files
console.log("No more files to transfer");
if (fileStream) {
fileStream.end();
fileStream = null;
}
client.end();
return;
}
// Handle file name message
const message = data.toString().trim();
if (message.startsWith("FILE:")) {
const [, filename, filesize] = message.split(":");
currentFile = filename;
expectedFileSize = parseInt(filesize);
receivedBytes = 0;
fileHash = crypto.createHash('md5');
lastChunkId = -1;
console.log(`Preparing to receive file: ${currentFile} (${expectedFileSize} bytes)`);
// Create directory for the file if needed
const fileDir = path.dirname(path.join(Folderpath, currentFile));
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir, { recursive: true });
}
// Create write stream with proper options
fileStream = fs.createWriteStream(path.join(Folderpath, currentFile), {
flags: 'w',
encoding: null, // binary
highWaterMark: 16 * 1024 // 16KB buffer to match server chunks
});
fileStream.on("error", (err) => {
console.error("Error writing file:", err);
client.end();
});
// Acknowledge that we're ready to receive the file
client.write("READY:" + currentFile);
}
});
} catch (error) {
console.error("Error processing server data:", error);
client.end();
}
});
client.on('close', () => {
console.log('Connection closed');
});
client.on('error', (err) => {
console.error('Connection error:', err);
});
}
}
main();