Uploads: abgewiesene Dateien melden statt still zu verwerfen, Anlagen bis 30 MB
Eine zu große oder nicht erlaubte Datei wurde von multer abgewiesen, der Fehler
nur auf die Server-Konsole geloggt und der Browser kommentarlos auf /vorlagen
zurückgeleitet. Für den Benutzer sah das aus, als sei der Upload durchgelaufen,
die Anlage tauchte nur nirgends auf ("Upload error: File too large" im Log, die
Teildatei räumt multer selbst wieder weg). Betroffen waren alle drei Uploads der
Vorlagen-Seite: Anlagen, Unterschrift und Bewerbungsfoto.
Die Routen leiten jetzt mit ?fehler=… um – dasselbe Muster wie auf der Jobsuche –
und die Seite zeigt den Grund als Banner: zu groß (mit Limit), Dateityp nicht
erlaubt, oder keine Datei gewählt. Das Limit steht zusätzlich am Datei-Feld,
bevor jemand eine zu große Datei auswählt.
Limit für Anlagen von 15 auf 30 MB angehoben; gescannte Zeugnisse liegen
regelmäßig darüber. Die Größen sind Konstanten (ANHANG_MAX_MB / BILD_MAX_MB),
aus denen sowohl multer als auch die Meldung und der Hinweis im Formular lesen –
eine Meldung, die ein anderes Limit nennt als das durchgesetzte, wäre schlimmer
als gar keine. Der Bild-Filter für Unterschrift/Foto lehnt jetzt ebenfalls mit
einem Fehler ab statt die Datei still zu verwerfen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -417,9 +417,61 @@ if (!fs.existsSync(basisAnhaengeDir)) {
|
||||
const ATTACHMENT_FILTER = (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const ok = /^\.(pdf|png|jpe?g|gif|webp|docx?|rtf|odt|ods|txt|csv|xlsx?)$/.test(ext);
|
||||
cb(ok ? null : new Error('Dateityp nicht erlaubt'), ok);
|
||||
cb(ok ? null : dateitypFehler(), ok);
|
||||
};
|
||||
|
||||
// Upload size limits. The numbers live here (not inline in the multer configs)
|
||||
// because the message shown to the user quotes them — a limit that says one
|
||||
// thing and enforces another is worse than no message at all.
|
||||
const ANHANG_MAX_MB = 30;
|
||||
const BILD_MAX_MB = 5;
|
||||
|
||||
function dateitypFehler() {
|
||||
const err = new Error('Dateityp nicht erlaubt');
|
||||
err.code = 'DATEITYP';
|
||||
return err;
|
||||
}
|
||||
|
||||
// Multer reports a rejected upload through its callback's `err`: a blown size
|
||||
// limit, a blocked file type, a failed write. Those used to be logged to the
|
||||
// server console and otherwise swallowed — the browser was redirected back to a
|
||||
// page that simply did not list the file, which to the user looks exactly like
|
||||
// "it uploaded but it isn't shown". Turn the error into a sentence they can act
|
||||
// on. Returns null when the upload was fine.
|
||||
function uploadFehlerText(err, maxMb) {
|
||||
if (!err) return null;
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return `Die Datei ist zu groß. Erlaubt sind maximal ${maxMb} MB — bitte komprimiere sie (z. B. den Scan als PDF verkleinern) und lade sie erneut hoch.`;
|
||||
}
|
||||
if (err.code === 'DATEITYP') {
|
||||
return 'Dieser Dateityp ist nicht erlaubt. Möglich sind PDF, Bilder (PNG, JPG, GIF, WebP), Word, ODT, RTF, Text, CSV und Excel.';
|
||||
}
|
||||
if (err.code === 'BILDTYP') {
|
||||
return 'Für dieses Bild sind nur PNG- oder JPG-Dateien möglich.';
|
||||
}
|
||||
return `Die Datei konnte nicht hochgeladen werden: ${err.message || 'unbekannter Fehler'}`;
|
||||
}
|
||||
|
||||
// Bilder (Unterschrift, Bewerbungsfoto): nur PNG/JPG, und der Dateiname muss zur
|
||||
// MIME-Art passen. Wie oben mit einem Fehler ablehnen statt still zu verwerfen.
|
||||
const BILD_FILTER = (req, file, cb) => {
|
||||
const ok = /^image\/(png|jpe?g)$/.test(file.mimetype)
|
||||
&& /^\.(png|jpe?g)$/.test(path.extname(file.originalname).toLowerCase());
|
||||
cb(ok ? null : bildtypFehler(), ok);
|
||||
};
|
||||
|
||||
function bildtypFehler() {
|
||||
const err = new Error('Nur PNG oder JPG');
|
||||
err.code = 'BILDTYP';
|
||||
return err;
|
||||
}
|
||||
|
||||
// Ein Ziel plus Fehlermeldung als Query-Parameter — dasselbe Muster wie auf der
|
||||
// Jobsuche-Seite (?fehler=…), das die Views bereits als Banner rendern.
|
||||
function mitFehler(pfad, text) {
|
||||
return `${pfad}${pfad.includes('?') ? '&' : '?'}fehler=${encodeURIComponent(text)}`;
|
||||
}
|
||||
|
||||
const uploadBasisAnhang = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, userStorageDirForReq(basisAnhaengeDir, req)),
|
||||
@@ -428,7 +480,7 @@ const uploadBasisAnhang = multer({
|
||||
cb(null, `${Date.now()}_${safe}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 15 * 1024 * 1024 }, // 15 MB
|
||||
limits: { fileSize: ANHANG_MAX_MB * 1024 * 1024 },
|
||||
fileFilter: ATTACHMENT_FILTER,
|
||||
}).single('datei');
|
||||
|
||||
@@ -447,7 +499,7 @@ const uploadInterneAnhang = multer({
|
||||
cb(null, `${Date.now()}_${safe}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 15 * 1024 * 1024 }, // 15 MB
|
||||
limits: { fileSize: ANHANG_MAX_MB * 1024 * 1024 },
|
||||
fileFilter: ATTACHMENT_FILTER,
|
||||
}).single('datei');
|
||||
|
||||
@@ -466,10 +518,8 @@ const uploadSignatur = multer({
|
||||
cb(null, `signatur_${Date.now()}${ext}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
|
||||
fileFilter: (req, file, cb) => cb(null,
|
||||
/^image\/(png|jpe?g)$/.test(file.mimetype)
|
||||
&& /^\.(png|jpe?g)$/.test(path.extname(file.originalname).toLowerCase())),
|
||||
limits: { fileSize: BILD_MAX_MB * 1024 * 1024 },
|
||||
fileFilter: BILD_FILTER,
|
||||
}).single('signatur');
|
||||
|
||||
// The single stored signature file, if any (in the current user's subdir).
|
||||
@@ -511,10 +561,8 @@ const uploadFoto = multer({
|
||||
cb(null, `foto_${Date.now()}${ext}`);
|
||||
},
|
||||
}),
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
|
||||
fileFilter: (req, file, cb) => cb(null,
|
||||
/^image\/(png|jpe?g)$/.test(file.mimetype)
|
||||
&& /^\.(png|jpe?g)$/.test(path.extname(file.originalname).toLowerCase())),
|
||||
limits: { fileSize: BILD_MAX_MB * 1024 * 1024 },
|
||||
fileFilter: BILD_FILTER,
|
||||
}).single('foto');
|
||||
|
||||
// The single stored photo file, if any (in the current user's subdir).
|
||||
@@ -3008,6 +3056,10 @@ initializeDatabase().then(async () => {
|
||||
basisTypOptions: BASIS_TYP_OPTIONS,
|
||||
hasApiKey: Boolean(config.get('OLLAMA_API_KEY')),
|
||||
hideSettings: true,
|
||||
// Set by the upload routes when multer rejected a file (too large, wrong
|
||||
// type) — without this the page just came back without the attachment.
|
||||
fehler: req.query.fehler ? String(req.query.fehler) : null,
|
||||
anhangMaxMb: ANHANG_MAX_MB,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading vorlagen:', error);
|
||||
@@ -3206,9 +3258,11 @@ initializeDatabase().then(async () => {
|
||||
try {
|
||||
if (err) {
|
||||
console.error('Upload error:', err.message);
|
||||
return res.redirect('/vorlagen');
|
||||
return res.redirect(mitFehler('/vorlagen', uploadFehlerText(err, ANHANG_MAX_MB)));
|
||||
}
|
||||
if (!req.file) {
|
||||
return res.redirect(mitFehler('/vorlagen', 'Es wurde keine Datei ausgewählt.'));
|
||||
}
|
||||
if (req.file) {
|
||||
const original = req.file.originalname || req.file.filename;
|
||||
const name = (req.body.name && req.body.name.trim())
|
||||
? sanitizeInput(req.body.name.trim())
|
||||
@@ -3217,7 +3271,6 @@ initializeDatabase().then(async () => {
|
||||
'INSERT INTO basis_anhaenge (user_id, name, dateiname, mime, pfad) VALUES (?, ?, ?, ?, ?)',
|
||||
[uidFromReq(req), name, sanitizeInput(original), req.file.mimetype || 'application/octet-stream', req.file.filename]
|
||||
);
|
||||
}
|
||||
res.redirect('/vorlagen');
|
||||
} catch (error) {
|
||||
console.error('Error saving attachment:', error);
|
||||
@@ -3269,7 +3322,10 @@ initializeDatabase().then(async () => {
|
||||
app.post('/unterschrift', (req, res) => {
|
||||
uploadSignatur(req, res, (err) => {
|
||||
try {
|
||||
if (err) console.error('Signature upload error:', err.message);
|
||||
if (err) {
|
||||
console.error('Signature upload error:', err.message);
|
||||
return res.redirect(mitFehler('/vorlagen', uploadFehlerText(err, BILD_MAX_MB)));
|
||||
}
|
||||
if (req.file) {
|
||||
// keep only the newly uploaded file (in the user's subdir). Resolve the
|
||||
// dir from req.user (not currentUserId()) — this runs in the multer
|
||||
@@ -3309,7 +3365,10 @@ initializeDatabase().then(async () => {
|
||||
app.post('/bewerbungsfoto', (req, res) => {
|
||||
uploadFoto(req, res, (err) => {
|
||||
try {
|
||||
if (err) console.error('Photo upload error:', err.message);
|
||||
if (err) {
|
||||
console.error('Photo upload error:', err.message);
|
||||
return res.redirect(mitFehler('/vorlagen', uploadFehlerText(err, BILD_MAX_MB)));
|
||||
}
|
||||
if (req.file) {
|
||||
// keep only the newly uploaded file (in the user's subdir). Resolve the
|
||||
// dir from req.user — see /unterschrift handler for why (multer callback).
|
||||
|
||||
+11
-1
@@ -15,6 +15,14 @@
|
||||
Zurück zur Übersicht
|
||||
</a>
|
||||
|
||||
<!-- Abgewiesener Upload (zu groß, falscher Dateityp): ohne diesen Banner
|
||||
käme die Seite kommentarlos ohne die Datei zurück. -->
|
||||
<% if (typeof fehler !== 'undefined' && fehler) { %>
|
||||
<div class="mb-6 rounded-lg border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20 p-4" role="alert">
|
||||
<p class="text-sm text-red-800 dark:text-red-200"><%= fehler %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Basis-Unterlagen (Vorlagen)</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
Diese Unterlagen dienen der KI als Faktengrundlage. Beim Import einer Stelle über die Browser-Erweiterung
|
||||
@@ -240,7 +248,9 @@
|
||||
<form action="/anlagen" method="POST" enctype="multipart/form-data" class="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4 space-y-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Datei (PDF)</label>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Datei (PDF) · max. <%= typeof anhangMaxMb !== 'undefined' ? anhangMaxMb : 15 %> MB
|
||||
</label>
|
||||
<input type="file" name="datei" accept="application/pdf,image/*" required
|
||||
class="w-full text-sm text-gray-700 dark:text-gray-300 file:mr-3 file:py-2 file:px-3 file:rounded-md file:border-0 file:bg-blue-600 file:text-white hover:file:bg-blue-700">
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user