/** * TASKMATE - File Upload * ====================== * Multer-Konfiguration für Datei-Uploads */ const multer = require('multer'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const { v4: uuidv4 } = require('uuid'); const logger = require('../utils/logger'); // Upload-Verzeichnis const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', 'uploads'); // Verzeichnis erstellen falls nicht vorhanden if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR, { recursive: true }); } // Mapping: Dateiendung -> erlaubte MIME-Types const EXTENSION_TO_MIME = { // Dokumente 'pdf': ['application/pdf'], 'doc': ['application/msword'], 'docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'], 'xls': ['application/vnd.ms-excel'], 'xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], 'ppt': ['application/vnd.ms-powerpoint'], 'pptx': ['application/vnd.openxmlformats-officedocument.presentationml.presentation'], 'odt': ['application/vnd.oasis.opendocument.text'], 'ods': ['application/vnd.oasis.opendocument.spreadsheet'], 'odp': ['application/vnd.oasis.opendocument.presentation'], 'rtf': ['application/rtf', 'text/rtf'], // Text 'txt': ['text/plain'], 'csv': ['text/csv', 'application/csv', 'text/comma-separated-values'], 'md': ['text/markdown', 'text/x-markdown', 'text/plain'], 'json': ['application/json', 'text/json'], 'xml': ['application/xml', 'text/xml'], 'html': ['text/html'], 'log': ['text/plain'], // Bilder 'jpg': ['image/jpeg'], 'jpeg': ['image/jpeg'], 'png': ['image/png'], 'gif': ['image/gif'], 'webp': ['image/webp'], 'svg': ['image/svg+xml'], 'bmp': ['image/bmp'], 'ico': ['image/x-icon', 'image/vnd.microsoft.icon'], // Archive 'zip': ['application/zip', 'application/x-zip-compressed'], 'rar': ['application/x-rar-compressed', 'application/vnd.rar'], '7z': ['application/x-7z-compressed'], 'tar': ['application/x-tar'], 'gz': ['application/gzip', 'application/x-gzip'], // Code/Skripte (als text/plain akzeptiert) 'sql': ['application/sql', 'text/plain'], 'js': ['text/javascript', 'application/javascript', 'text/plain'], 'css': ['text/css', 'text/plain'], 'py': ['text/x-python', 'text/plain'], 'sh': ['application/x-sh', 'text/plain'] }; // Standard-Werte (Fallback) let MAX_FILE_SIZE = (parseInt(process.env.MAX_FILE_SIZE_MB) || 15) * 1024 * 1024; let ALLOWED_EXTENSIONS = ['pdf', 'docx', 'txt']; /** * Lädt Upload-Einstellungen aus der Datenbank */ function loadUploadSettings() { try { // Lazy-Load um zirkuläre Abhängigkeiten zu vermeiden const { getUploadSettings } = require('../routes/admin'); const settings = getUploadSettings(); if (settings) { MAX_FILE_SIZE = (settings.maxFileSizeMB || 15) * 1024 * 1024; // Erlaubte Endungen aus den Einstellungen if (Array.isArray(settings.allowedExtensions) && settings.allowedExtensions.length > 0) { ALLOWED_EXTENSIONS = settings.allowedExtensions; } } } catch (error) { // Bei Fehler Standard-Werte beibehalten logger.warn('Upload-Einstellungen konnten nicht geladen werden, verwende Standards'); } } /** * Aktuelle Einstellungen abrufen (für dynamische Prüfung) */ function getCurrentSettings() { loadUploadSettings(); return { maxFileSize: MAX_FILE_SIZE, allowedExtensions: ALLOWED_EXTENSIONS }; } /** * Storage-Konfiguration */ const storage = multer.diskStorage({ destination: (req, file, cb) => { // Task-ID aus URL oder Body const taskId = req.params.taskId || req.body.taskId; if (taskId) { // Unterordner pro Task const taskDir = path.join(UPLOAD_DIR, `task_${taskId}`); if (!fs.existsSync(taskDir)) { fs.mkdirSync(taskDir, { recursive: true }); } cb(null, taskDir); } else { cb(null, UPLOAD_DIR); } }, filename: (req, file, cb) => { // Eindeutiger Dateiname mit Original-Extension const ext = path.extname(file.originalname).toLowerCase(); const uniqueName = `${uuidv4()}${ext}`; cb(null, uniqueName); } }); /** * Gefährliche Dateinamen prüfen */ function isSecureFilename(filename) { // Null-Bytes, Pfad-Traversal, Steuerzeichen blocken const dangerousPatterns = [ /\x00/, // Null-Bytes /\.\./, // Path traversal /[<>:"\\|?*]/, // Windows-spezifische gefährliche Zeichen /[\x00-\x1F]/, // Steuerzeichen /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i, // Windows reservierte Namen ]; return !dangerousPatterns.some(pattern => pattern.test(filename)); } /** * Datei-Filter: Erweiterte Sicherheitsprüfungen */ const fileFilter = (req, file, cb) => { // Aktuelle Einstellungen laden const settings = getCurrentSettings(); // Sicherheitsprüfungen für Dateinamen if (!isSecureFilename(file.originalname)) { logger.warn(`Unsicherer Dateiname abgelehnt: ${file.originalname}`); cb(new Error('Dateiname enthält nicht erlaubte Zeichen'), false); return; } // Dateiname-Länge prüfen if (file.originalname.length > 255) { logger.warn(`Dateiname zu lang: ${file.originalname}`); cb(new Error('Dateiname ist zu lang (max. 255 Zeichen)'), false); return; } // Dateiendung extrahieren (ohne Punkt, lowercase) const ext = path.extname(file.originalname).toLowerCase().replace('.', ''); // Doppelte Dateiendungen verhindern (z.B. script.txt.exe) const nameWithoutExt = path.basename(file.originalname, path.extname(file.originalname)); if (path.extname(nameWithoutExt)) { logger.warn(`Doppelte Dateiendung abgelehnt: ${file.originalname}`); cb(new Error('Dateien mit mehreren Endungen sind nicht erlaubt'), false); return; } // Prüfen ob Endung erlaubt ist if (!settings.allowedExtensions.includes(ext)) { logger.warn(`Abgelehnter Upload (Endung): ${file.originalname} (.${ext})`); cb(new Error(`Dateityp .${ext} nicht erlaubt`), false); return; } // Executable Dateien zusätzlich blocken const executableExtensions = [ 'exe', 'bat', 'cmd', 'com', 'scr', 'pif', 'vbs', 'vbe', 'js', 'jar', 'app', 'deb', 'pkg', 'dmg', 'run', 'bin', 'msi', 'gadget' ]; if (executableExtensions.includes(ext)) { logger.warn(`Executable Datei abgelehnt: ${file.originalname}`); cb(new Error('Ausführbare Dateien sind nicht erlaubt'), false); return; } // MIME-Type gegen bekannte Typen prüfen const expectedMimes = EXTENSION_TO_MIME[ext]; if (expectedMimes && !expectedMimes.includes(file.mimetype)) { logger.warn(`MIME-Mismatch: ${file.originalname} (erwartet: ${expectedMimes.join('/')}, bekommen: ${file.mimetype})`); // Bei kritischen Mismatches ablehnen if (file.mimetype === 'application/octet-stream' || file.mimetype.startsWith('application/x-')) { cb(new Error('Verdächtiger Dateityp erkannt'), false); return; } } cb(null, true); }; /** * Dynamische Multer-Instanz erstellen */ function createUpload() { const settings = getCurrentSettings(); return multer({ storage, fileFilter, limits: { fileSize: settings.maxFileSize, files: 10 // Maximal 10 Dateien pro Request } }); } // Standard-Instanz für Rückwärtskompatibilität const upload = createUpload(); /** * Datei löschen */ function deleteFile(filePath) { try { const fullPath = path.join(UPLOAD_DIR, filePath); if (fs.existsSync(fullPath)) { fs.unlinkSync(fullPath); logger.info(`Datei gelöscht: ${filePath}`); return true; } return false; } catch (error) { logger.error(`Fehler beim Löschen: ${filePath}`, { error: error.message }); return false; } } /** * Dateigröße formatieren */ function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Ist Bild? */ function isImage(mimeType) { return mimeType && mimeType.startsWith('image/'); } /** * Datei-Icon basierend auf MIME-Type */ function getFileIcon(mimeType) { if (mimeType.startsWith('image/')) return 'image'; if (mimeType === 'application/pdf') return 'pdf'; if (mimeType.includes('word')) return 'word'; if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'excel'; if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'powerpoint'; if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('7z')) return 'archive'; if (mimeType.startsWith('text/')) return 'text'; return 'file'; } module.exports = { upload, createUpload, deleteFile, formatFileSize, isImage, getFileIcon, getCurrentSettings, UPLOAD_DIR, MAX_FILE_SIZE, ALLOWED_EXTENSIONS, EXTENSION_TO_MIME };