import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import multer from 'multer'; import { env } from '../config/env.js'; const ALLOWED_MIME = new Set([ 'application/pdf', 'image/png', 'image/jpeg', 'image/webp', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain', 'text/markdown', 'application/zip', ]); // El navegador no manda un mimetype confiable para .md (varia segun SO: text/markdown, // application/octet-stream, vacio...) -- se normaliza por extension para no rechazar // documentos validos ni depender de esa adivinanza. const MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown']); const uploadsDir = path.join(env.storageRoot, 'attachments'); fs.mkdirSync(uploadsDir, { recursive: true }); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadsDir), // Nombre generado por el servidor: evita path traversal y colisiones. filename: (req, file, cb) => { const ext = path.extname(file.originalname).slice(0, 10); cb(null, `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`); }, }); export const upload = multer({ storage, limits: { fileSize: 15 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (MARKDOWN_EXTENSIONS.has(path.extname(file.originalname).toLowerCase())) { file.mimetype = 'text/markdown'; return cb(null, true); } if (!ALLOWED_MIME.has(file.mimetype)) { return cb(new Error('Tipo de archivo no permitido')); } cb(null, true); }, }); export { uploadsDir };