feat: initial commit — OST to PST/MBOX converter with Docker support
Some checks failed
Build & Push Multi-Arch Image / build-and-push (push) Has been cancelled
Some checks failed
Build & Push Multi-Arch Image / build-and-push (push) Has been cancelled
This commit is contained in:
234
server.js
Normal file
234
server.js
Normal file
@@ -0,0 +1,234 @@
|
||||
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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user