const http = require("http"); const fs = require("fs"); const path = require("path"); const XLSX = require("xlsx"); const formidable = require("formidable"); const PORT = process.env.PORT || 3000; function readExcelFile(filePath) { const wb = XLSX.readFile(filePath); const sheets = {}; wb.SheetNames.forEach(function (name) { const data = XLSX.utils.sheet_to_json(wb.Sheets[name], { header: 1, defval: "" }); sheets[name] = data; }); return { name: path.basename(filePath), sheets: sheets, sheetNames: wb.SheetNames }; } function compareFiles(fileA, fileB) { const a = readExcelFile(fileA); const b = readExcelFile(fileB); var report = []; report.push("Сравнение: " + a.name + " vs " + b.name); report.push("=".repeat(60)); report.push(""); var allSheets = Array.from(new Set(a.sheetNames.concat(b.sheetNames))); allSheets.forEach(function (sheetName) { var inA = a.sheetNames.indexOf(sheetName) >= 0; var inB = b.sheetNames.indexOf(sheetName) >= 0; if (!inA) { report.push("Лист «" + sheetName + "»: есть ТОЛЬКО во втором файле (" + b.name + ")"); report.push(" Строк: " + (b.sheets[sheetName] ? b.sheets[sheetName].length : 0)); report.push(""); return; } if (!inB) { report.push("Лист «" + sheetName + "»: есть ТОЛЬКО в первом файле (" + a.name + ")"); report.push(" Строк: " + (a.sheets[sheetName] ? a.sheets[sheetName].length : 0)); report.push(""); return; } var rowsA = a.sheets[sheetName]; var rowsB = b.sheets[sheetName]; var maxRows = Math.max(rowsA.length, rowsB.length); var maxCols = 0; rowsA.forEach(function (r) { if (r.length > maxCols) maxCols = r.length; }); rowsB.forEach(function (r) { if (r.length > maxCols) maxCols = r.length; }); var changes = []; var added = 0, removed = 0, modified = 0; for (var i = 0; i < maxRows; i++) { var rowA = rowsA[i] || []; var rowB = rowsB[i] || []; var maxC = Math.max(rowA.length, rowB.length); for (var j = 0; j < maxC; j++) { var valA = String(rowA[j] != null ? rowA[j] : "").trim(); var valB = String(rowB[j] != null ? rowB[j] : "").trim(); if (valA === valB) continue; var colLetter = String.fromCharCode(65 + (j % 26)); if (j >= 26) colLetter = String.fromCharCode(64 + Math.floor(j / 26)) + colLetter; var cellRef = colLetter + (i + 1); if (valA === "" && valB !== "") { changes.push(" + " + cellRef + ": «" + valB + "»"); added++; } else if (valA !== "" && valB === "") { changes.push(" - " + cellRef + ": было «" + valA + "»"); removed++; } else { changes.push(" ~ " + cellRef + ": «" + valA + "» → «" + valB + "»"); modified++; } } } report.push("Лист «" + sheetName + "» — " + rowsA.length + "×" + rowsA[0].length + " vs " + rowsB.length + "×" + (rowsB[0] ? rowsB[0].length : 0)); if (changes.length === 0) { report.push(" Отличий нет."); } else { report.push(" Изменений: " + (added + removed + modified) + " (+" + added + " / -" + removed + " / ~" + modified + ")"); report.push(""); report.push.apply(report, changes); } report.push(""); }); return { fileNameA: a.name, fileNameB: b.name, sheetsA: a.sheetNames.length, sheetsB: b.sheetNames.length, report: report.join("\n") }; } http.createServer(function (req, res) { if (req.method === "POST" && req.url === "/api/compare") { var form = formidable({ uploadDir: __dirname, keepExtensions: true, maxFileSize: 100 * 1024 * 1024 }); form.parse(req, function (err, fields, files) { if (err) { res.writeHead(400, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: false, error: "Ошибка загрузки: " + err.message })); } var f1 = files.file1 && files.file1[0]; var f2 = files.file2 && files.file2[0]; if (!f1 || !f2) { res.writeHead(400, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: false, error: "Загрузите оба файла" })); } try { var result = compareFiles(f1.filepath, f2.filepath); try { fs.unlinkSync(f1.filepath); } catch (e) {} try { fs.unlinkSync(f2.filepath); } catch (e) {} res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, data: result })); } catch (e) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Ошибка чтения файла: " + e.message })); } }); return; } if (req.method === "POST" && req.url === "/api/analyze") { var form2 = formidable({ uploadDir: __dirname, keepExtensions: true, maxFileSize: 100 * 1024 * 1024 }); form2.parse(req, function (err, fields, files) { if (err) { res.writeHead(400, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: false, error: "Ошибка загрузки: " + err.message })); } var f = files.file && files.file[0]; if (!f) { res.writeHead(400, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: false, error: "Загрузите файл" })); } try { var wb = XLSX.readFile(f.filepath); var analysis = []; analysis.push("Анализ файла: " + path.basename(f.filepath)); analysis.push("=".repeat(60)); analysis.push("Листов: " + wb.SheetNames.length); analysis.push("Имена листов: " + wb.SheetNames.join(", ")); analysis.push(""); wb.SheetNames.forEach(function (name) { var data = XLSX.utils.sheet_to_json(wb.Sheets[name], { header: 1, defval: "" }); var rows = data.length; var cols = 0; data.forEach(function (r) { if (r.length > cols) cols = r.length; }); var nonEmpty = 0; data.forEach(function (r) { r.forEach(function (c) { if (String(c).trim() !== "") nonEmpty++; }); }); analysis.push("Лист «" + name + "»:"); analysis.push(" Размер: " + rows + " строк × " + cols + " столбцов"); analysis.push(" Заполненных ячеек: " + nonEmpty); if (rows > 0 && cols > 0) { analysis.push(" Заголовок (строка 1): " + data[0].map(function (c) { return String(c).trim(); }).filter(Boolean).join(" | ")); } analysis.push(""); }); try { fs.unlinkSync(f.filepath); } catch (e) {} res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, data: { report: analysis.join("\n") } })); } catch (e) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Ошибка чтения файла: " + e.message })); } }); return; } var file = req.url === "/" ? "/index.html" : req.url.split("?")[0]; var full = path.join(__dirname, file); if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { var ext = path.extname(full); var type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); return res.end(fs.readFileSync(full)); } res.writeHead(404); res.end("Not found"); }).listen(PORT, function () { console.log("Сервер на порту " + PORT); });