293 Zeilen
8.8 KiB
JavaScript
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
|
|
};
|