Some checks failed
Build & Push Multi-Arch Image / build-and-push (push) Has been cancelled
235 lines
7.7 KiB
JavaScript
235 lines
7.7 KiB
JavaScript
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}`);
|
|
});
|