- GET /pdf/preview/:fileId — renders first page as PNG (72 DPI) - GET /pdf/preview/:fileId/:page — specific page preview - GET /pdf/thumbnails/:fileId — list of all page preview URLs - File cards show actual PDF thumbnail instead of icon - Cached previews (30 min cleanup) - Fallback to PDF icon if preview fails
568 lines
20 KiB
JavaScript
568 lines
20 KiB
JavaScript
const express = require('express');
|
||
const multer = require('multer');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const { spawn } = require('child_process');
|
||
const rateLimit = require('express-rate-limit');
|
||
const archiver = require('archiver');
|
||
const log = require('../lib/logger');
|
||
|
||
const router = express.Router();
|
||
|
||
const UPLOADS_DIR = path.join(__dirname, '..', 'uploads');
|
||
const DOWNLOADS_DIR = path.join(__dirname, '..', 'downloads');
|
||
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
||
|
||
const upload = multer({
|
||
dest: UPLOADS_DIR,
|
||
limits: { fileSize: MAX_FILE_SIZE },
|
||
fileFilter: (req, file, cb) => {
|
||
if (file.mimetype === 'application/pdf' || file.originalname.endsWith('.pdf')) cb(null, true);
|
||
else if (file.mimetype.startsWith('image/')) cb(null, true); // for fromImages
|
||
else cb(new Error('Только PDF и изображения'));
|
||
},
|
||
});
|
||
|
||
const limiter = rateLimit({ windowMs: 60000, max: 30, message: { error: 'Слишком много запросов' } });
|
||
|
||
// Fix multer filename encoding (Latin-1 → UTF-8 for Cyrillic)
|
||
function fixFilename(str) {
|
||
try {
|
||
const buf = Buffer.from(str, 'latin1');
|
||
const decoded = buf.toString('utf8');
|
||
if (/[а-яёА-ЯЁ]/.test(decoded)) return decoded;
|
||
} catch {}
|
||
return str;
|
||
}
|
||
|
||
// File storage for uploaded PDFs
|
||
const pdfFiles = new Map();
|
||
|
||
function registerFile(multerFile, pageCount) {
|
||
const id = `pdf_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||
const entry = {
|
||
id,
|
||
path: multerFile.path,
|
||
name: fixFilename(multerFile.originalname),
|
||
size: multerFile.size,
|
||
pages: pageCount || 0,
|
||
};
|
||
pdfFiles.set(id, entry);
|
||
setTimeout(() => {
|
||
try { fs.existsSync(entry.path) && fs.unlinkSync(entry.path); } catch {}
|
||
pdfFiles.delete(id);
|
||
}, 30 * 60 * 1000);
|
||
return entry;
|
||
}
|
||
|
||
function getFile(id) {
|
||
const f = pdfFiles.get(id);
|
||
if (!f || !fs.existsSync(f.path)) return null;
|
||
return f;
|
||
}
|
||
|
||
function saveResult(buffer, ext) {
|
||
const name = `result_${Date.now()}_${Math.random().toString(36).slice(2, 6)}${ext}`;
|
||
const outPath = path.join(DOWNLOADS_DIR, name);
|
||
fs.writeFileSync(outPath, buffer);
|
||
setTimeout(() => { try { fs.unlinkSync(outPath); } catch {} }, 30 * 60 * 1000);
|
||
return `/pdf/download/${name}`;
|
||
}
|
||
|
||
// Page
|
||
router.get('/', (req, res) => {
|
||
res.sendFile(path.join(__dirname, '..', 'public', 'pdf.html'));
|
||
});
|
||
|
||
// Upload
|
||
router.post('/upload', limiter, upload.array('files', 10), async (req, res) => {
|
||
if (!req.files || !req.files.length) return res.status(400).json({ error: 'Файлы не загружены' });
|
||
|
||
const results = [];
|
||
|
||
for (const file of req.files) {
|
||
let pages = 0;
|
||
try {
|
||
if (file.mimetype === 'application/pdf' || file.originalname.endsWith('.pdf')) {
|
||
const buf = fs.readFileSync(file.path);
|
||
// Use pdf-lib for page count (more reliable than pdf-parse)
|
||
const { PDFDocument } = await import('pdf-lib');
|
||
const doc = await PDFDocument.load(buf, { ignoreEncryption: true });
|
||
pages = doc.getPageCount();
|
||
}
|
||
} catch (e) {
|
||
log.warn('PDF page count failed', { file: file.originalname, error: e.message });
|
||
}
|
||
const entry = registerFile(file, pages);
|
||
results.push({ id: entry.id, name: entry.name, size: entry.size, pages });
|
||
}
|
||
|
||
log.info(`PDF upload: ${results.length} files`);
|
||
res.json({ files: results });
|
||
});
|
||
|
||
// Preview — render PDF page as PNG thumbnail via Ghostscript
|
||
router.get('/preview/:fileId', (req, res) => renderPreview(req, res, 1));
|
||
router.get('/preview/:fileId/:page', (req, res) => renderPreview(req, res, parseInt(req.params.page) || 1));
|
||
|
||
async function renderPreview(req, res, pageNum) {
|
||
const f = getFile(req.params.fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
const cacheKey = `${f.id}_p${pageNum}`;
|
||
const cachePath = path.join(DOWNLOADS_DIR, `preview_${cacheKey}.png`);
|
||
|
||
// Return cached if exists
|
||
if (fs.existsSync(cachePath)) {
|
||
return res.type('png').sendFile(cachePath);
|
||
}
|
||
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
const proc = spawn('gs', [
|
||
'-sDEVICE=png16m', '-r72', '-dNOPAUSE', '-dBATCH', '-dQUIET',
|
||
`-dFirstPage=${pageNum}`, `-dLastPage=${pageNum}`,
|
||
'-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||
`-sOutputFile=${cachePath}`, f.path,
|
||
], { timeout: 15000 });
|
||
proc.on('close', code => code === 0 ? resolve() : reject(new Error('Preview failed')));
|
||
proc.on('error', reject);
|
||
});
|
||
|
||
// Cleanup after 30 min
|
||
setTimeout(() => { try { fs.unlinkSync(cachePath); } catch {} }, 30 * 60 * 1000);
|
||
|
||
res.type('png').sendFile(cachePath);
|
||
} catch (err) {
|
||
res.status(500).json({ error: 'Не удалось создать превью' });
|
||
}
|
||
}
|
||
|
||
// Thumbnails — return preview URLs for all pages
|
||
router.get('/thumbnails/:fileId', async (req, res) => {
|
||
const f = getFile(req.params.fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
const urls = [];
|
||
for (let i = 1; i <= Math.min(f.pages, 50); i++) {
|
||
urls.push(`/pdf/preview/${f.id}/${i}`);
|
||
}
|
||
res.json({ pages: f.pages, thumbnails: urls });
|
||
});
|
||
|
||
// Info
|
||
router.get('/info/:fileId', async (req, res) => {
|
||
const f = getFile(req.params.fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const pdfParse = require('pdf-parse');
|
||
const buf = fs.readFileSync(f.path);
|
||
const data = await pdfParse(buf);
|
||
res.json({
|
||
pages: data.numpages,
|
||
title: data.info?.Title || '',
|
||
author: data.info?.Author || '',
|
||
creator: data.info?.Creator || '',
|
||
size: f.size,
|
||
});
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Merge
|
||
router.post('/merge', express.json(), async (req, res) => {
|
||
const { fileIds } = req.body;
|
||
if (!fileIds || fileIds.length < 2) return res.status(400).json({ error: 'Нужно минимум 2 файла' });
|
||
|
||
try {
|
||
const { PDFDocument } = await import('pdf-lib');
|
||
const merged = await PDFDocument.create();
|
||
|
||
for (const id of fileIds) {
|
||
const f = getFile(id);
|
||
if (!f) return res.status(404).json({ error: `Файл ${id} не найден` });
|
||
const buf = fs.readFileSync(f.path);
|
||
const doc = await PDFDocument.load(buf);
|
||
const pages = await merged.copyPages(doc, doc.getPageIndices());
|
||
pages.forEach(p => merged.addPage(p));
|
||
}
|
||
|
||
const result = await merged.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Split
|
||
router.post('/split', express.json(), async (req, res) => {
|
||
const { fileId, ranges } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const { PDFDocument } = await import('pdf-lib');
|
||
const buf = fs.readFileSync(f.path);
|
||
const srcDoc = await PDFDocument.load(buf);
|
||
const totalPages = srcDoc.getPageCount();
|
||
|
||
// Parse ranges: "1-3,5,7-9"
|
||
const pageNums = [];
|
||
for (const part of ranges.split(',')) {
|
||
const trimmed = part.trim();
|
||
if (trimmed.includes('-')) {
|
||
const [start, end] = trimmed.split('-').map(Number);
|
||
for (let i = start; i <= Math.min(end, totalPages); i++) pageNums.push(i);
|
||
} else {
|
||
const n = parseInt(trimmed);
|
||
if (n >= 1 && n <= totalPages) pageNums.push(n);
|
||
}
|
||
}
|
||
|
||
if (pageNums.length === 0) return res.status(400).json({ error: 'Нет валидных страниц' });
|
||
|
||
// Single output PDF with selected pages
|
||
const newDoc = await PDFDocument.create();
|
||
const indices = pageNums.map(n => n - 1); // 0-based
|
||
const copiedPages = await newDoc.copyPages(srcDoc, indices);
|
||
copiedPages.forEach(p => newDoc.addPage(p));
|
||
|
||
const result = await newDoc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length, pages: pageNums.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Rotate
|
||
router.post('/rotate', express.json(), async (req, res) => {
|
||
const { fileId, pages, angle } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const { PDFDocument, degrees } = await import('pdf-lib');
|
||
const buf = fs.readFileSync(f.path);
|
||
const doc = await PDFDocument.load(buf);
|
||
const total = doc.getPageCount();
|
||
|
||
const targetPages = pages === 'all'
|
||
? Array.from({ length: total }, (_, i) => i)
|
||
: pages.split(',').map(n => parseInt(n.trim()) - 1).filter(i => i >= 0 && i < total);
|
||
|
||
for (const idx of targetPages) {
|
||
const page = doc.getPage(idx);
|
||
const current = page.getRotation().angle;
|
||
page.setRotation(degrees(current + (angle || 90)));
|
||
}
|
||
|
||
const result = await doc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Delete pages
|
||
router.post('/delete', express.json(), async (req, res) => {
|
||
const { fileId, pages } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const { PDFDocument } = await import('pdf-lib');
|
||
const buf = fs.readFileSync(f.path);
|
||
const srcDoc = await PDFDocument.load(buf);
|
||
const total = srcDoc.getPageCount();
|
||
|
||
const deleteSet = new Set(pages.split(',').map(n => parseInt(n.trim()) - 1));
|
||
const keepIndices = Array.from({ length: total }, (_, i) => i).filter(i => !deleteSet.has(i));
|
||
|
||
if (keepIndices.length === 0) return res.status(400).json({ error: 'Нельзя удалить все страницы' });
|
||
|
||
const newDoc = await PDFDocument.create();
|
||
const copied = await newDoc.copyPages(srcDoc, keepIndices);
|
||
copied.forEach(p => newDoc.addPage(p));
|
||
|
||
const result = await newDoc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length, pages: keepIndices.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Reorder
|
||
router.post('/reorder', express.json(), async (req, res) => {
|
||
const { fileId, order } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const { PDFDocument } = await import('pdf-lib');
|
||
const buf = fs.readFileSync(f.path);
|
||
const srcDoc = await PDFDocument.load(buf);
|
||
const indices = order.map(n => n - 1); // 1-based to 0-based
|
||
|
||
const newDoc = await PDFDocument.create();
|
||
const copied = await newDoc.copyPages(srcDoc, indices);
|
||
copied.forEach(p => newDoc.addPage(p));
|
||
|
||
const result = await newDoc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Watermark
|
||
router.post('/watermark', express.json(), async (req, res) => {
|
||
const { fileId, text, fontSize = 48, opacity = 0.3, color = '#888888' } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const { PDFDocument, rgb, StandardFonts } = await import('pdf-lib');
|
||
const buf = fs.readFileSync(f.path);
|
||
const doc = await PDFDocument.load(buf);
|
||
const font = await doc.embedFont(StandardFonts.Helvetica);
|
||
|
||
const r = parseInt(color.slice(1, 3), 16) / 255;
|
||
const g = parseInt(color.slice(3, 5), 16) / 255;
|
||
const b = parseInt(color.slice(5, 7), 16) / 255;
|
||
|
||
for (let i = 0; i < doc.getPageCount(); i++) {
|
||
const page = doc.getPage(i);
|
||
const { width, height } = page.getSize();
|
||
const textWidth = font.widthOfTextAtSize(text, fontSize);
|
||
|
||
page.drawText(text, {
|
||
x: (width - textWidth) / 2,
|
||
y: height / 2,
|
||
size: fontSize,
|
||
font,
|
||
color: rgb(r, g, b),
|
||
opacity,
|
||
rotate: { type: 'degrees', angle: -45 },
|
||
});
|
||
}
|
||
|
||
const result = await doc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Page numbers
|
||
router.post('/pagenumbers', express.json(), async (req, res) => {
|
||
const { fileId, position = 'bottom-center', startFrom = 1 } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const { PDFDocument, rgb, StandardFonts } = await import('pdf-lib');
|
||
const buf = fs.readFileSync(f.path);
|
||
const doc = await PDFDocument.load(buf);
|
||
const font = await doc.embedFont(StandardFonts.Helvetica);
|
||
const total = doc.getPageCount();
|
||
|
||
for (let i = 0; i < total; i++) {
|
||
const page = doc.getPage(i);
|
||
const { width } = page.getSize();
|
||
const numText = String(i + startFrom);
|
||
const textWidth = font.widthOfTextAtSize(numText, 10);
|
||
|
||
let x = (width - textWidth) / 2; // center
|
||
let y = 30;
|
||
|
||
if (position.includes('left')) x = 40;
|
||
if (position.includes('right')) x = width - 40 - textWidth;
|
||
if (position.includes('top')) y = page.getSize().height - 30;
|
||
|
||
page.drawText(numText, { x, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
|
||
}
|
||
|
||
const result = await doc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Compress (Ghostscript)
|
||
router.post('/compress', express.json(), async (req, res) => {
|
||
const { fileId, quality = 'ebook' } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
const outName = `compressed_${Date.now()}.pdf`;
|
||
const outPath = path.join(DOWNLOADS_DIR, outName);
|
||
const settings = { screen: '/screen', ebook: '/ebook', printer: '/printer' };
|
||
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
const proc = spawn('gs', [
|
||
'-sDEVICE=pdfwrite', '-dCompatibilityLevel=1.4',
|
||
`-dPDFSETTINGS=${settings[quality] || '/ebook'}`,
|
||
'-dNOPAUSE', '-dBATCH', '-dQUIET',
|
||
`-sOutputFile=${outPath}`, f.path,
|
||
], { timeout: 120000 });
|
||
proc.on('close', code => code === 0 ? resolve() : reject(new Error('Ghostscript error')));
|
||
proc.on('error', reject);
|
||
});
|
||
|
||
const stat = fs.statSync(outPath);
|
||
const savings = f.size > 0 ? Math.round((1 - stat.size / f.size) * 100) : 0;
|
||
setTimeout(() => { try { fs.unlinkSync(outPath); } catch {} }, 30 * 60 * 1000);
|
||
|
||
res.json({ downloadUrl: `/pdf/download/${outName}`, size: stat.size, savings });
|
||
} catch (err) {
|
||
try { fs.unlinkSync(outPath); } catch {}
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Protect (add password)
|
||
router.post('/protect', express.json(), async (req, res) => {
|
||
const { fileId, password } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
if (!password) return res.status(400).json({ error: 'Пароль обязателен' });
|
||
|
||
try {
|
||
const { PDFDocument } = await import('pdf-lib');
|
||
const buf = fs.readFileSync(f.path);
|
||
const doc = await PDFDocument.load(buf);
|
||
doc.encrypt({ userPassword: password, ownerPassword: password });
|
||
|
||
const result = await doc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Extract text
|
||
router.post('/extract-text', express.json(), async (req, res) => {
|
||
const { fileId } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
try {
|
||
const pdfParse = require('pdf-parse');
|
||
const buf = fs.readFileSync(f.path);
|
||
const data = await pdfParse(buf);
|
||
res.json({
|
||
text: data.text,
|
||
pages: data.numpages,
|
||
metadata: data.info || {},
|
||
});
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// PDF to Images (Ghostscript)
|
||
router.post('/toImages', express.json(), async (req, res) => {
|
||
const { fileId, format = 'png', dpi = 150 } = req.body;
|
||
const f = getFile(fileId);
|
||
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||
|
||
const tmpDir = path.join(DOWNLOADS_DIR, `img_${Date.now()}`);
|
||
fs.mkdirSync(tmpDir, { recursive: true });
|
||
const device = format === 'jpg' ? 'jpeg' : 'png16m';
|
||
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
const proc = spawn('gs', [
|
||
`-sDEVICE=${device}`, `-r${dpi}`,
|
||
'-dNOPAUSE', '-dBATCH', '-dQUIET',
|
||
`-sOutputFile=${tmpDir}/page_%03d.${format === 'jpg' ? 'jpg' : 'png'}`,
|
||
f.path,
|
||
], { timeout: 120000 });
|
||
proc.on('close', code => code === 0 ? resolve() : reject(new Error('Ghostscript error')));
|
||
proc.on('error', reject);
|
||
});
|
||
|
||
// ZIP the images
|
||
const zipName = `pages_${Date.now()}.zip`;
|
||
const zipPath = path.join(DOWNLOADS_DIR, zipName);
|
||
const output = fs.createWriteStream(zipPath);
|
||
const archive = archiver('zip', { zlib: { level: 6 } });
|
||
archive.pipe(output);
|
||
archive.directory(tmpDir, false);
|
||
await archive.finalize();
|
||
await new Promise(r => output.on('close', r));
|
||
|
||
// Cleanup temp dir
|
||
fs.readdirSync(tmpDir).forEach(f => fs.unlinkSync(path.join(tmpDir, f)));
|
||
fs.rmdirSync(tmpDir);
|
||
|
||
setTimeout(() => { try { fs.unlinkSync(zipPath); } catch {} }, 30 * 60 * 1000);
|
||
const stat = fs.statSync(zipPath);
|
||
res.json({ downloadUrl: `/pdf/download/${zipName}`, size: stat.size });
|
||
} catch (err) {
|
||
try { fs.readdirSync(tmpDir).forEach(f => fs.unlinkSync(path.join(tmpDir, f))); fs.rmdirSync(tmpDir); } catch {}
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Images to PDF
|
||
router.post('/fromImages', upload.array('images', 50), async (req, res) => {
|
||
if (!req.files || !req.files.length) return res.status(400).json({ error: 'Изображения не загружены' });
|
||
|
||
try {
|
||
const { PDFDocument } = await import('pdf-lib');
|
||
const doc = await PDFDocument.create();
|
||
|
||
for (const file of req.files) {
|
||
const imgBytes = fs.readFileSync(file.path);
|
||
let img;
|
||
if (file.mimetype === 'image/png') img = await doc.embedPng(imgBytes);
|
||
else img = await doc.embedJpg(imgBytes);
|
||
|
||
const page = doc.addPage([img.width, img.height]);
|
||
page.drawImage(img, { x: 0, y: 0, width: img.width, height: img.height });
|
||
|
||
try { fs.unlinkSync(file.path); } catch {}
|
||
}
|
||
|
||
const result = await doc.save();
|
||
const url = saveResult(Buffer.from(result), '.pdf');
|
||
res.json({ downloadUrl: url, size: result.length, pages: req.files.length });
|
||
} catch (err) {
|
||
req.files.forEach(f => { try { fs.unlinkSync(f.path); } catch {} });
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Download
|
||
router.get('/download/:filename', (req, res) => {
|
||
const filename = path.basename(req.params.filename);
|
||
const filePath = path.join(DOWNLOADS_DIR, filename);
|
||
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Файл не найден' });
|
||
res.download(filePath);
|
||
});
|
||
|
||
// Multer error handler
|
||
router.use((err, req, res, next) => {
|
||
if (err instanceof multer.MulterError) {
|
||
if (err.code === 'LIMIT_FILE_SIZE') return res.status(413).json({ error: 'Файл слишком большой. Максимум 50MB.' });
|
||
return res.status(400).json({ error: err.message });
|
||
}
|
||
if (err) return res.status(400).json({ error: err.message });
|
||
next();
|
||
});
|
||
|
||
module.exports = router;
|