server.js
Rama master · réplica de lectura del repositorio original.
/**
* 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';
}