const express = require('express'); const multer = require('multer'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 3000; // Ensure directories exist const UPLOADS_DIR = path.join(__dirname, 'uploads'); const CONVERTED_DIR = path.join(__dirname, 'converted'); if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR); if (!fs.existsSync(CONVERTED_DIR)) fs.mkdirSync(CONVERTED_DIR); // Configure Multer for local uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, UPLOADS_DIR); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); cb(null, uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, limits: { fileSize: 1024 * 1024 * 1024 } // 1GB limit }); // Serve static frontend files app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); // In-memory task tracking const tasks = new Map(); // File upload endpoint app.post('/api/upload', upload.single('ostFile'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } const format = req.body.format === 'mbox' ? 'mbox' : 'pst'; const combineVcf = req.body.combineVcf === 'true'; const taskId = 'task-' + Date.now() + '-' + Math.round(Math.random() * 1000); const originalName = req.file.originalname; const baseName = path.basename(originalName, path.extname(originalName)).replace(/[^a-zA-Z0-9_-]/g, '_'); const outputFileName = `${baseName}-${Date.now()}.${format === 'mbox' ? 'zip' : 'pst'}`; const outputPath = path.join(CONVERTED_DIR, outputFileName); tasks.set(taskId, { id: taskId, originalName: originalName, inputPath: req.file.path, outputPath: outputPath, outputFileName: outputFileName, format: format, combineVcf: combineVcf, status: 'uploaded', logs: [], error: null }); res.json({ taskId }); }); // SSE Streaming endpoint for real-time logs and status app.get('/api/stream/:taskId', (req, res) => { const { taskId } = req.params; const task = tasks.get(taskId); if (!task) { return res.status(404).json({ error: 'Task not found' }); } // Set headers for Server-Sent Events (SSE) res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); res.write(`data: ${JSON.stringify({ status: 'starting', message: 'Starting Java conversion engine...' })}\n\n`); task.status = 'converting'; // Spawn Java conversion process const classpath = 'aspose-email-24.12-jdk16.jar:.'; const child = spawn('java', [ '-cp', classpath, 'Convert.java', task.inputPath, task.outputPath, task.format, task.combineVcf ? 'true' : 'false' ], { cwd: __dirname }); const folderStats = []; let totalMessages = 0; let duration = 0; let buffer = ''; const sendLog = (data) => { buffer += data.toString(); const lines = buffer.split('\n'); buffer = lines.pop(); // Keep incomplete line for (const line of lines) { const trimmed = line.trim(); if (trimmed) { task.logs.push(trimmed); // Parse stats in real-time const folderMatch = trimmed.match(/Processing Folder:\s+(.+)/); if (folderMatch) { const fName = folderMatch[1]; if (!folderStats.find(item => item.folder === fName)) { folderStats.push({ folder: fName, messages: 0 }); } } const msgMatch = trimmed.match(/Successfully copied\s+(\d+)\s+messages to folder:\s+(.+)/); if (msgMatch) { const count = parseInt(msgMatch[1], 10); const fName = msgMatch[2]; const found = folderStats.find(item => item.folder === fName); if (found) { found.messages = count; } else { folderStats.push({ folder: fName, messages: count }); } totalMessages += count; } const durationMatch = trimmed.match(/Conversion completed successfully in\s+([\d\.]+)\s+seconds/); if (durationMatch) { duration = parseFloat(durationMatch[1]); } res.write(`data: ${JSON.stringify({ status: 'running', log: trimmed })}\n\n`); } } }; child.stdout.on('data', sendLog); child.stderr.on('data', sendLog); child.on('close', (code) => { // Handle remaining buffer if (buffer.trim()) { task.logs.push(buffer.trim()); res.write(`data: ${JSON.stringify({ status: 'running', log: buffer.trim() })}\n\n`); } // Clean up uploaded input OST file immediately to save disk space try { if (fs.existsSync(task.inputPath)) { fs.unlinkSync(task.inputPath); } } catch (err) { console.error('Failed to delete input file:', err); } if (code === 0) { task.status = 'completed'; // Send completed state with parsed statistics res.write(`data: ${JSON.stringify({ status: 'completed', downloadUrl: `/api/download/${taskId}`, fileName: task.outputFileName, summary: { folders: folderStats.filter(f => f.messages > 0), totalMessages: totalMessages, duration: duration || 0 } })}\n\n`); } else { task.status = 'failed'; task.error = 'Conversion process exited with code ' + code; res.write(`data: ${JSON.stringify({ status: 'failed', error: task.error })}\n\n`); } res.end(); }); req.on('close', () => { // If client closes connection prematurely, terminate the conversion process if (task.status === 'converting') { child.kill(); } }); }); // Download endpoint app.get('/api/download/:taskId', (req, res) => { const { taskId } = req.params; const task = tasks.get(taskId); if (!task || task.status !== 'completed') { return res.status(404).send('PST file not found or task not completed.'); } if (!fs.existsSync(task.outputPath)) { return res.status(404).send('PST file has expired or was removed.'); } res.setHeader('Content-Type', task.format === 'mbox' ? 'application/zip' : 'application/octet-stream'); res.setHeader('Content-Disposition', `attachment; filename="${task.outputFileName}"`); res.sendFile(task.outputPath, (err) => { if (err) { console.error('Download error:', err); } // Clean up output file after download (or set a timeout) setTimeout(() => { try { if (fs.existsSync(task.outputPath)) { fs.unlinkSync(task.outputPath); } tasks.delete(taskId); } catch (cleanupErr) { console.error('Failed to clean up output file:', cleanupErr); } }, 60000); // Wait 1 minute before removing to ensure download succeeds }); }); app.listen(PORT, () => { console.log(`OST to PST Converter running at http://localhost:${PORT}`); });