Excel_managment/server.js

317 lines
9.0 KiB
JavaScript

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const xlsx = require('xlsx');
const app = express();
const PORT = process.env.PORT || 3000;
// Ensure uploads folder exists
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
const metadataFile = path.join(uploadsDir, 'metadata.json');
function loadMetadata() {
try {
if (fs.existsSync(metadataFile)) {
const data = fs.readFileSync(metadataFile, 'utf8');
return JSON.parse(data);
}
} catch (err) {
console.error('Failed reading metadata:', err);
}
return [];
}
function saveMetadata(metadata) {
try {
fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2), 'utf8');
} catch (err) {
console.error('Failed saving metadata:', err);
}
}
// Multer storage configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadsDir);
},
filename: (req, file, cb) => {
const timestamp = Date.now();
const safeName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
cb(null, `${timestamp}-${safeName}`);
}
});
// File filter: accept only .xlsx and .xls
const fileFilter = (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
if (ext === '.xlsx' || ext === '.xls') {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only Excel files (.xlsx and .xls) are allowed.'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 100 * 1024 * 1024 // 100MB limit
}
});
// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// 1. Get list of Excel files
app.get('/api/files', (req, res) => {
try {
const metadata = loadMetadata();
// Filter to ensure files actually exist on disk
const existing = metadata.filter(item => {
const filePath = path.join(uploadsDir, item.id);
return fs.existsSync(filePath);
});
if (existing.length !== metadata.length) {
saveMetadata(existing);
}
res.json({ files: existing });
} catch (err) {
res.status(500).json({ error: 'Failed to retrieve files: ' + err.message });
}
});
// 2. Upload an Excel file
app.post('/api/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(400).json({ error: err.message || 'File upload failed.' });
}
if (!req.file) {
return res.status(400).json({ error: 'No Excel file was uploaded.' });
}
const filePath = req.file.path;
let workbook;
try {
// Validate that it is a valid, readable Excel file
workbook = xlsx.readFile(filePath, { cellDates: true });
if (!workbook || !workbook.SheetNames || workbook.SheetNames.length === 0) {
throw new Error('Excel file contains no worksheets.');
}
} catch (parseErr) {
// Remove invalid/corrupt uploaded file
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (e) {}
return res.status(400).json({
error: 'Invalid or corrupted Excel file. Please upload a valid .xlsx or .xls file.'
});
}
const fileRecord = {
id: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
uploadedAt: new Date().toISOString(),
sheetNames: workbook.SheetNames
};
const metadata = loadMetadata();
metadata.unshift(fileRecord);
saveMetadata(metadata);
res.json({ success: true, file: fileRecord });
});
});
// 3. Get Excel file details and worksheets
app.get('/api/files/:id', (req, res) => {
const fileId = req.params.id;
const filePath = path.join(uploadsDir, fileId);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found on server.' });
}
const metadata = loadMetadata();
const fileRecord = metadata.find(f => f.id === fileId);
try {
const workbook = xlsx.readFile(filePath, { bookSheets: true });
res.json({
id: fileId,
originalName: fileRecord ? fileRecord.originalName : fileId,
size: fileRecord ? fileRecord.size : fs.statSync(filePath).size,
uploadedAt: fileRecord ? fileRecord.uploadedAt : null,
sheetNames: workbook.SheetNames
});
} catch (err) {
res.status(500).json({ error: 'Failed to read Excel workbook: ' + err.message });
}
});
// 4. Get specific sheet data from an Excel file
app.get('/api/files/:id/sheet', (req, res) => {
const fileId = req.params.id;
const sheetNameParam = req.query.name;
const filePath = path.join(uploadsDir, fileId);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found on server.' });
}
try {
const workbook = xlsx.readFile(filePath, {
cellDates: true,
cellText: true,
raw: false
});
const targetSheetName = sheetNameParam || workbook.SheetNames[0];
if (!workbook.SheetNames.includes(targetSheetName)) {
return res.status(404).json({ error: `Sheet "${targetSheetName}" not found.` });
}
const worksheet = workbook.Sheets[targetSheetName];
// Convert sheet to array of rows (header: 1 gives 2D array: [ [cellA1, cellB1...], ... ])
const rows = xlsx.utils.sheet_to_json(worksheet, {
header: 1,
defval: '',
blankrows: true,
raw: false
});
// Determine max columns across all rows
let maxCols = 0;
for (let i = 0; i < rows.length; i++) {
if (rows[i].length > maxCols) {
maxCols = rows[i].length;
}
}
// Standardize all rows to have the same number of columns for grid consistency
const normalizedRows = rows.map(row => {
const newRow = new Array(maxCols);
for (let c = 0; c < maxCols; c++) {
newRow[c] = (row[c] !== undefined && row[c] !== null) ? String(row[c]) : '';
}
return newRow;
});
res.json({
sheetName: targetSheetName,
sheetNames: workbook.SheetNames,
totalRows: normalizedRows.length,
totalCols: maxCols,
rows: normalizedRows
});
} catch (err) {
res.status(500).json({ error: 'Failed to parse sheet data: ' + err.message });
}
});
// 5. Save edited sheet data back to the Excel file
app.post('/api/files/:id/sheet', (req, res) => {
const fileId = req.params.id;
const { sheetName, rows } = req.body;
const filePath = path.join(uploadsDir, fileId);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found on server.' });
}
if (!sheetName || !Array.isArray(rows)) {
return res.status(400).json({ error: 'Invalid sheet data. sheetName and rows array are required.' });
}
try {
const workbook = xlsx.readFile(filePath);
if (!workbook.SheetNames.includes(sheetName)) {
return res.status(404).json({ error: `Sheet "${sheetName}" not found in workbook.` });
}
// Convert values to numbers if numeric, or strings
const formattedRows = rows.map(row => {
if (!Array.isArray(row)) return [];
return row.map(cell => {
if (cell === null || cell === undefined) return '';
const str = String(cell).trim();
if (str === '') return '';
// If string represents a clean number, parse it
if (/^-?\d+(\.\d+)?$/.test(str) && !isNaN(Number(str))) {
return Number(str);
}
return cell;
});
});
const newWorksheet = xlsx.utils.aoa_to_sheet(formattedRows);
workbook.Sheets[sheetName] = newWorksheet;
xlsx.writeFile(workbook, filePath);
// Update metadata file size and timestamp
const stats = fs.statSync(filePath);
const metadata = loadMetadata();
const fileRecord = metadata.find(f => f.id === fileId);
if (fileRecord) {
fileRecord.size = stats.size;
fileRecord.updatedAt = new Date().toISOString();
saveMetadata(metadata);
}
res.json({
success: true,
message: `Worksheet "${sheetName}" saved successfully!`,
size: stats.size
});
} catch (err) {
res.status(500).json({ error: 'Failed to save worksheet: ' + err.message });
}
});
// 5. Download original Excel file
app.get('/api/files/:id/download', (req, res) => {
const fileId = req.params.id;
const filePath = path.join(uploadsDir, fileId);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found on server.' });
}
const metadata = loadMetadata();
const fileRecord = metadata.find(f => f.id === fileId);
const downloadName = fileRecord ? fileRecord.originalName : fileId;
// Use res.download to preserve exact original file and content
res.download(filePath, downloadName, (err) => {
if (err && !res.headersSent) {
res.status(500).json({ error: 'Failed downloading file: ' + err.message });
}
});
});
// Fallback to index.html
app.use((req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Start server
app.listen(PORT, () => {
console.log(`Excel Management Server running at http://localhost:${PORT}`);
});