Files
TaskMate/backend/middleware/upload.js
2026-01-04 00:24:11 +00:00

293 Zeilen
8.8 KiB
JavaScript

/**
* 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
};