/** * SERVER.JS * * - Supports both single-chunk and multi-chunk uploads * - No separate "init" request for chunk #0. The first chunk * of data (index=0) immediately writes to the file. * - Uses an in-memory tracker to handle partial uploads. * - Cleans up stale uploads. */ const http = require('http'); const fs = require('fs'); const path = require('path'); const { parse } = require('url'); const { exec } = require('child_process'); const os = require('os'); const crypto = require('crypto'); const zlib = require('zlib'); // Keep an in-memory map of ongoing uploads const uploadTracker = new Map(); // Keep track of errors const uploadErrors = new Map(); // Periodic cleanup of stale uploads setupUploadCleanup(); // Basic dynamic config const systemConfig = { totalMem: os.totalmem(), freeMem: os.freemem(), cpuCount: os.cpus().length, update() { this.freeMem = os.freemem(); return this; }, getUploadTimeout(estimatedSize) { // 24h default if <1GB, else scale const base = 24 * 60 * 60 * 1000; if (estimatedSize < 1_073_741_824) return base; // <1GB const scale = Math.log2(estimatedSize / 1_073_741_824) + 1; return Math.ceil(base * scale); } }; function getCurrentPath() { // Default base "uploads" folder in current working directory const base = process.cwd(); const uploadPath = path.join(base, 'uploads'); if (!fs.existsSync(uploadPath)) { fs.mkdirSync(uploadPath, { recursive: true }); } return uploadPath; } function cleanupFile(filePath) { if (fs.existsSync(filePath)) { // Optionally rename to ".incomplete" or remove const incomplete = filePath + '.incomplete'; try { fs.renameSync(filePath, incomplete); console.log(`Renamed incomplete file to: ${incomplete}`); } catch (err) { console.error('Failed renaming incomplete file:', err); } } } // Periodically remove stale uploads function setupUploadCleanup() { setInterval(() => { const now = Date.now(); for (const [fileId, upload] of uploadTracker) { const inactive = now - upload.lastActivity; // Timeout is dynamic: totalChunks * chunkSize estimate const estimateSize = upload.totalChunks * upload.chunkSize; const uploadTimeout = systemConfig.getUploadTimeout(estimateSize); if (inactive > uploadTimeout) { console.log(`Cleaning up stale upload ${fileId}`); if (upload.writeStream) { try { upload.writeStream.end(); } catch {} } if (upload.receivedChunks.size < upload.totalChunks) { // mark incomplete cleanupFile(upload.finalPath); } clearTimeout(upload.timeout); uploadTracker.delete(fileId); } } // Clean older error records const errorStale = 24 * 60 * 60 * 1000; // 24h for (const [fileId, errInfo] of uploadErrors) { const errTime = new Date(errInfo.timestamp).getTime(); if ((now - errTime) > errorStale) { uploadErrors.delete(fileId); } } }, 60_000); } // Helper: respond with JSON error function sendError(res, code, msg, details=null) { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, error: msg, details })); } // Start HTTP server const server = http.createServer(async (req, res) => { systemConfig.update(); // CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', [ 'Origin','X-Requested-With','Content-Type','Accept', 'X-File-Name','X-Chunk-Index','X-Total-Chunks','X-File-Path', 'Content-Disposition','Content-Range','X-File-Id','X-Client-Speed','X-Chunk-Size' ].join(', ')); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const { pathname, query } = parse(req.url, true); // ------------------------------------------- // 1) Cancel Upload Endpoint // ------------------------------------------- if (req.method === 'POST' && pathname === '/cancel-upload') { try { const fileId = query.fileId; const fileName = query.fileName ? decodeURIComponent(query.fileName) : ''; console.log(`Cancelling upload for fileId: ${fileId}, fileName: ${fileName}`); if (fileId && uploadTracker.has(fileId)) { const upload = uploadTracker.get(fileId); // Close the write stream if open if (upload.writeStream) { try { upload.writeStream.end(); } catch (err) { console.error('Error closing write stream:', err); } } // Clean up the file if it exists cleanupFile(upload.finalPath); // Clean up temporary chunks if any const tempDir = path.dirname(upload.finalPath); const tempPrefix = path.basename(upload.finalPath) + '.part'; try { const files = fs.readdirSync(tempDir); for (const file of files) { if (file.startsWith(tempPrefix)) { fs.unlinkSync(path.join(tempDir, file)); console.log(`Deleted temp chunk: ${file}`); } } } catch (err) { console.error('Error cleaning up temp chunks:', err); } // Remove from tracker clearTimeout(upload.timeout); uploadTracker.delete(fileId); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Upload cancelled and temporary files cleaned up' })); } else { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'No active upload found with this ID' })); } } catch (error) { console.error('Error cancelling upload:', error); sendError(res, 500, 'Server error cancelling upload', error.message); } return; } // ------------------------------------------- // 2) Speed Test Endpoint // ------------------------------------------- if (req.method === 'GET' && pathname === '/speed-test') { try { const requestedSize = query.size || '500KB'; let sizeInBytes = 500 * 1024; if (requestedSize.toUpperCase().endsWith('KB')) { sizeInBytes = parseInt(requestedSize) * 1024; } else if (requestedSize.toUpperCase().endsWith('MB')) { sizeInBytes = parseInt(requestedSize) * 1024 * 1024; } else { sizeInBytes = parseInt(requestedSize); } // cap at 1MB sizeInBytes = Math.min(sizeInBytes, 1_048_576); const buffer = Buffer.alloc(sizeInBytes); crypto.randomFillSync(buffer); res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': buffer.length, 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', 'Pragma': 'no-cache', 'Expires': '0' }); res.end(buffer); } catch (err) { console.error('Speed-test error:', err); sendError(res, 500, 'Failed speed test'); } } // ------------------------------------------- // 3) Serve index.html at root // ------------------------------------------- else if (req.method === 'GET' && pathname === '/') { fs.readFile('./public/index.html', (err, data) => { if (err) { res.writeHead(500); return res.end('Error loading index.html'); } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(data); }); } // ------------------------------------------- // 4) List drives (Windows only) // ------------------------------------------- else if (req.method === 'GET' && pathname === '/list-drives') { if (process.platform === 'win32') { exec('wmic logicaldisk get name', (error, stdout) => { if (error) { return sendError(res, 500, error.message); } const lines = stdout.split('\r\r\n').map(x => x.trim()).filter(x => /^[A-Za-z]:$/.test(x)); const drives = lines.map(d => d + '\\'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ drives })); }); } else { // Non-windows res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ drives: ['/'] })); } } // ------------------------------------------- // 5) List folders // ------------------------------------------- else if (req.method === 'GET' && pathname === '/list-folders') { let currentPath = query.path || '/'; if ((currentPath === '/' || currentPath === '\\') && process.platform === 'win32') { // Return that we are at Windows root res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ path:'/', folders:[], isRoot:true })); } try { currentPath = path.resolve(currentPath); const items = fs.readdirSync(currentPath, { withFileTypes: true }); const folders = items.filter(d => d.isDirectory()).map(d => d.name); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ path: currentPath, folders })); } catch (err) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ path:'/', folders:[], error:err.message })); } } // ------------------------------------------- // 6) Chunked/Single-chunk upload // ------------------------------------------- else if (req.method === 'POST' && pathname === '/upload-chunk') { try { const fileName = decodeURIComponent(req.headers['x-file-name'] || ''); const chunkIndex = parseInt(req.headers['x-chunk-index'] || '0', 10); const totalChunks = parseInt(req.headers['x-total-chunks'] || '1', 10); const fileId = req.headers['x-file-id'] || Date.now().toString(); const targetPath = query.path || getCurrentPath(); const chunkSz = parseInt(req.headers['x-chunk-size'] || '0', 10); if (!fileName) return sendError(res, 400, 'Missing file name'); // Create target dir if needed (including nested directories for folder uploads) const outDir = path.resolve(targetPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } const finalFilePath = path.join(outDir, fileName); // Ensure the parent directory of the file exists (for nested folder uploads) const fileDir = path.dirname(finalFilePath); if (!fs.existsSync(fileDir)) { fs.mkdirSync(fileDir, { recursive: true }); } const isSingleChunk = (totalChunks === 1 && chunkIndex === 0); // If single-chunk => just pipe it to disk if (isSingleChunk) { const ws = fs.createWriteStream(finalFilePath, { flags:'w' }); ws.on('error', err => { console.error('Single-chunk write error:', err); if (!res.headersSent) { sendError(res, 500, `Write error: ${err.message}`); } }); req.pipe(ws); req.on('end', () => { ws.end(); ws.on('finish', () => { res.writeHead(200, { 'Content-Type':'application/json' }); res.end(JSON.stringify({ success: true, message: 'File upload complete (single-chunk)', filePath: finalFilePath })); }); }); return; } // --- MULTI-CHUNK LOGIC --- let upload = uploadTracker.get(fileId); // If no existing upload session, create it if (!upload) { console.log(`Creating new upload session for fileId=${fileId}, file=${fileName}`); const ws = fs.createWriteStream(finalFilePath, { flags:'w' }); ws.on('error', err => { uploadErrors.set(fileId, { code:'WRITE_ERROR', message: err.message, timestamp: new Date().toISOString() }); }); const estimateBytes = totalChunks * chunkSz; const timeoutValue = systemConfig.getUploadTimeout(estimateBytes); const timeoutHandle = setTimeout(() => { console.log(`Upload session timed out for ${fileId}`); if (ws) { ws.end(); } cleanupFile(finalFilePath); uploadTracker.delete(fileId); }, timeoutValue); upload = { finalPath: finalFilePath, writeStream: ws, totalChunks, chunkSize: chunkSz, receivedChunks: new Set(), createdAt: Date.now(), lastActivity: Date.now(), timeout: timeoutHandle }; uploadTracker.set(fileId, upload); } // If this chunk is already received, respond success immediately (e.g. retry) if (upload.receivedChunks.has(chunkIndex)) { res.writeHead(200, { 'Content-Type':'application/json' }); return res.end(JSON.stringify({ success: true, message:`Chunk ${chunkIndex} already received` })); } // Update activity upload.lastActivity = Date.now(); // Accumulate chunk data as it arrives const ws = upload.writeStream; let bytesWritten = 0; req.on('data', chunk => { ws.write(chunk); bytesWritten += chunk.length; }); req.on('end', () => { ws.once('drain', () => {}); // just to ensure we handle backpressure if needed upload.receivedChunks.add(chunkIndex); // If that was the last chunk, close up the file if (upload.receivedChunks.size === upload.totalChunks) { // All chunks complete ws.end(); ws.on('finish', () => { console.log(`All chunks uploaded for fileId=${fileId}, saved at ${upload.finalPath}`); clearTimeout(upload.timeout); uploadTracker.delete(fileId); // Send final success res.writeHead(200, { 'Content-Type':'application/json' }); res.end(JSON.stringify({ success: true, message: 'File upload complete (multi-chunk)', filePath: upload.finalPath })); }); } else { // More chunks to go, respond success res.writeHead(200, { 'Content-Type':'application/json' }); res.end(JSON.stringify({ success: true, message: `Chunk ${chunkIndex} received`, fileId })); } }); req.on('error', err => { console.error(`Error receiving chunk ${chunkIndex} for fileId=${fileId}:`, err); uploadErrors.set(fileId, { code:'CHUNK_RECEIVE_ERROR', message: err.message, timestamp: new Date().toISOString() }); if (!res.headersSent) { sendError(res, 500, `Chunk receive error: ${err.message}`); } }); } catch (err) { console.error('Upload chunk error:', err); sendError(res, 500, `Server error: ${err.message}`); } } // ------------------------------------------- // 7) Check upload status // ------------------------------------------- else if (req.method === 'GET' && pathname === '/check-upload') { const fileId = query.fileId; if (!fileId) { return sendError(res, 400, 'Missing fileId'); } const upload = uploadTracker.get(fileId); if (!upload) { // Maybe it finished or got cleaned up? const error = uploadErrors.get(fileId) || null; res.writeHead(200, { 'Content-Type':'application/json' }); return res.end(JSON.stringify({ success: true, exists: false, error })); } // If found res.writeHead(200, { 'Content-Type':'application/json' }); res.end(JSON.stringify({ success: true, exists: true, receivedChunks: Array.from(upload.receivedChunks), totalChunks: upload.totalChunks, lastActivity: upload.lastActivity, error: null })); } // ------------------------------------------- // 8) Report upload error // ------------------------------------------- else if (req.method === 'GET' && pathname === '/report-upload-error') { const fileId = query.fileId; const code = query.code || 'CLIENT_ERROR'; const message = query.message || 'Unknown client error'; const chunkIndex = (query.chunkIndex) ? parseInt(query.chunkIndex, 10) : null; if (!fileId) { return sendError(res, 400, 'Missing fileId'); } uploadErrors.set(fileId, { code, message, chunkIndex, timestamp: new Date().toISOString() }); console.log(`Client reported error for ${fileId}:`, code, message); res.writeHead(200, { 'Content-Type':'application/json' }); res.end(JSON.stringify({ success:true, message:'Error reported' })); } // ------------------------------------------- // 9) List files in a directory // ------------------------------------------- else if (req.method === 'GET' && pathname === '/list-files') { const dirPath = query.path || getCurrentPath(); try { const fullPath = path.resolve(dirPath); if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { fs.readdir(fullPath, (err, items) => { if (err) { return sendError(res, 500, 'Failed to read directory'); } const fileList = []; // Get detailed file info including size, type, and dates for (const item of items) { const filePath = path.join(fullPath, item); try { const stats = fs.statSync(filePath); if (stats.isFile()) { // Get file extension for type identification const ext = path.extname(item).toLowerCase().substring(1); fileList.push({ name: item, size: stats.size, modified: stats.mtime.toISOString(), created: stats.birthtime.toISOString(), extension: ext, type: getFileType(ext) }); } } catch (statErr) { // console.error(`Error getting stats for ${item}:`, statErr); // Still include the file with limited info fileList.push({ name: item, size: 0, error: 'Could not read file info' }); } } res.writeHead(200, { 'Content-Type':'application/json' }); res.end(JSON.stringify({ success: true, path: dirPath, files: fileList })); }); } else { sendError(res, 404, 'Directory not found'); } } catch (err) { sendError(res, 500, `List files error: ${err.message}`); } } // ------------------------------------------- // 10) Download a single file // ------------------------------------------- else if (req.method === 'GET' && pathname === '/download') { try { // Get the file path from the query const filePath = query.file; if (!filePath) { return sendError(res, 400, 'Missing file path'); } // Resolve the file path to prevent directory traversal const fullPath = path.resolve(filePath); // Check if the file exists if (!fs.existsSync(fullPath)) { return sendError(res, 404, 'File not found'); } // Check if it's a file const stats = fs.statSync(fullPath); if (!stats.isFile()) { return sendError(res, 400, 'Not a file'); } // Get the file name const fileName = path.basename(fullPath); // Set the content type based on the file extension const ext = path.extname(fileName).toLowerCase(); let contentType = 'application/octet-stream'; // Default to binary if (ext === '.pdf') contentType = 'application/pdf'; else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg'; else if (ext === '.png') contentType = 'image/png'; else if (ext === '.txt') contentType = 'text/plain'; else if (ext === '.mp4') contentType = 'video/mp4'; else if (ext === '.mp3') contentType = 'audio/mpeg'; // Set headers for the file download res.setHeader('Content-Type', contentType); res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`); res.setHeader('Content-Length', stats.size); // Create a read stream and pipe it to the response const fileStream = fs.createReadStream(fullPath); fileStream.on('error', (err) => { console.error('Error streaming file:', err); if (!res.headersSent) { sendError(res, 500, `File streaming error: ${err.message}`); } }); fileStream.pipe(res); } catch (err) { console.error('Download error:', err); sendError(res, 500, `Server error: ${err.message}`); } } // ------------------------------------------- // 11) Download multiple files as a zip archive // ------------------------------------------- else if (req.method === 'GET' && pathname === '/download-multiple') { try { // Parse the files from the query parameter (comma-separated list) const fileList = query.files ? query.files.split(',') : []; if (fileList.length === 0) { return sendError(res, 400, 'No files specified'); } // Prepare archive name const timestamp = Date.now(); const archiveName = `download_${timestamp}.zip`; // For single file that's not too large, redirect to single file download if (fileList.length === 1) { const fullPath = path.resolve(fileList[0]); if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { res.writeHead(302, { 'Location': `/download?file=${encodeURIComponent(fileList[0])}` }); return res.end(); } } // Validate all files exist and gather file details const fileDetails = []; for (const filePath of fileList) { const fullPath = path.resolve(filePath); if (!fs.existsSync(fullPath)) { return sendError(res, 404, `File not found: ${path.basename(fullPath)}`); } const stats = fs.statSync(fullPath); if (!stats.isFile()) { return sendError(res, 400, `Not a file: ${path.basename(fullPath)}`); } fileDetails.push({ path: fullPath, name: path.basename(fullPath), size: stats.size }); } // Set headers for the zip download res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${archiveName}"`); // Create temporary directory for creating the archive const tmpDir = path.join(os.tmpdir(), `download_${timestamp}`); if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }); } // Path for the temporary zip file const zipFilePath = path.join(tmpDir, archiveName); // Create a ZIP file using the archiver module or similar approach // Since we're not using third-party packages, we'll implement a simple ZIP creation // Using Node.js child_process to leverage system zip command if available const platform = process.platform; let zipCommand; if (platform === 'win32') { // On Windows, check if PowerShell is available for ZIP creation zipCommand = `powershell -command "Compress-Archive -Path '${fileDetails.map(f => f.path.replace(/'/g, "''")).join("','")}'`; zipCommand += ` -DestinationPath '${zipFilePath.replace(/'/g, "''")}'`; zipCommand += ` -Force"`; } else { // On Unix-like systems (Linux, macOS), use the zip command zipCommand = `zip -j "${zipFilePath}"`; fileDetails.forEach(file => { zipCommand += ` "${file.path.replace(/"/g, '\\"')}"`; }); } // Execute the zip command exec(zipCommand, (error, stdout, stderr) => { if (error) { console.error('Error creating ZIP archive:', error); // Fallback to manual concatenation if zip command fails return sendManualDownload(res, fileDetails); } // Stream the zip file to the client const zipStream = fs.createReadStream(zipFilePath); zipStream.on('error', (err) => { console.error('Error streaming ZIP file:', err); if (!res.headersSent) { sendError(res, 500, `Error streaming ZIP: ${err.message}`); } }); zipStream.on('end', () => { // Clean up the temporary file and directory fs.unlink(zipFilePath, (err) => { if (err) console.error('Failed to clean up ZIP file:', err); // Try to remove the temporary directory try { fs.rmdirSync(tmpDir); } catch (e) { console.error('Failed to clean up temporary directory:', e); } }); }); // Pipe the ZIP file to the response zipStream.pipe(res); }); // Function to handle manual download if zip command fails function sendManualDownload(res, files) { console.log('Falling back to manual file concatenation'); // Signal the start of streaming if (!res.headersSent) { res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="files_${timestamp}.dat"` }); } // Process each file sequentially let currentFileIndex = 0; const processNextFile = () => { if (currentFileIndex >= files.length) { // All files processed, end the response return res.end(); } const fileInfo = files[currentFileIndex]; const readStream = fs.createReadStream(fileInfo.path); // Write file header with metadata const headerStr = JSON.stringify({ name: fileInfo.name, size: fileInfo.size, index: currentFileIndex }); const headerBuf = Buffer.from(headerStr); const headerLenBuf = Buffer.alloc(4); headerLenBuf.writeUInt32BE(headerBuf.length, 0); // Write header length and header res.write(headerLenBuf); res.write(headerBuf); // Stream the file content readStream.on('data', chunk => res.write(chunk)); readStream.on('end', () => { currentFileIndex++; processNextFile(); }); readStream.on('error', err => { console.error(`Error reading file ${fileInfo.name}:`, err); currentFileIndex++; processNextFile(); }); }; // Start processing files processNextFile(); } } catch (err) { console.error('Multi-download error:', err); sendError(res, 500, `Server error: ${err.message}`); } } // ------------------------------------------- // 12) Fallback: serve static from ./public // ------------------------------------------- else if (req.method === 'GET') { let filePath = '.' + pathname; if (filePath === './') { filePath = './public/index.html'; } else if (!filePath.startsWith('./public')) { filePath = './public' + pathname; } fs.readFile(filePath, (err, data) => { if (err) { if (err.code === 'ENOENT') { res.writeHead(404); return res.end('Not found'); } res.writeHead(500); return res.end('Server error'); } // Basic content type detection let contentType = 'text/html'; const ext = path.extname(filePath).toLowerCase(); if (ext === '.js') contentType = 'text/javascript'; else if (ext === '.css') contentType = 'text/css'; else if (ext === '.json') contentType = 'application/json'; else if (ext === '.png') contentType = 'image/png'; else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg'; else if (ext === '.svg') contentType = 'image/svg+xml'; res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); } // ------------------------------------------- // 13) 404 Not Found // ------------------------------------------- else { res.writeHead(404); res.end('Not Found'); } }); // Graceful shutdown process.on('SIGINT', () => { for (const [fileId, upload] of uploadTracker) { if (upload.writeStream) { upload.writeStream.end(); } clearTimeout(upload.timeout); } uploadTracker.clear(); uploadErrors.clear(); process.exit(0); }); // Start server const PORT = process.env.PORT || 4000; server.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}/`); }); // Helper function to identify file types based on extension function getFileType(extension) { const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff', 'ico']; const docTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'csv', 'md', 'json']; const archiveTypes = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']; const audioTypes = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']; const videoTypes = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv']; const codeTypes = ['js', 'ts', 'html', 'css', 'scss', 'php', 'py', 'java', 'c', 'cpp', 'h', 'cs', 'rb', 'go', 'rs']; if (imageTypes.includes(extension)) return 'image'; if (docTypes.includes(extension)) return 'document'; if (archiveTypes.includes(extension)) return 'archive'; if (audioTypes.includes(extension)) return 'audio'; if (videoTypes.includes(extension)) return 'video'; if (codeTypes.includes(extension)) return 'code'; return 'other'; }