diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 1ded45d1..41278bc6 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -11,6 +11,7 @@ "error_file_not_found": "Die Datei konnte im Speicher nicht gefunden werden.", "error_file_upload_failed": "Die Datei konnte nicht hochgeladen werden.", "error_unsupported_file_type": "Dieses Dateiformat wird nicht unterstützt.", + "error_file_too_large": "Die Datei ist zu groß (max. 50 MB).", "error_user_not_found": "Der Benutzer wurde nicht gefunden.", "error_import_already_running": "Ein Import läuft bereits. Bitte warten Sie, bis dieser abgeschlossen ist.", "error_unauthorized": "Sie sind nicht angemeldet.", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 98e0faab..1c6e4b6e 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -11,6 +11,7 @@ "error_file_not_found": "The file could not be found in storage.", "error_file_upload_failed": "The file could not be uploaded.", "error_unsupported_file_type": "This file format is not supported.", + "error_file_too_large": "The file is too large (max. 50 MB).", "error_user_not_found": "User not found.", "error_import_already_running": "An import is already running. Please wait for it to finish.", "error_unauthorized": "You are not logged in.", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 1a4e11df..254e17cd 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -11,6 +11,7 @@ "error_file_not_found": "El archivo no pudo encontrarse en el almacenamiento.", "error_file_upload_failed": "No se pudo subir el archivo.", "error_unsupported_file_type": "Este formato de archivo no está admitido.", + "error_file_too_large": "El archivo es demasiado grande (máx. 50 MB).", "error_user_not_found": "Usuario no encontrado.", "error_import_already_running": "Ya hay una importación en curso. Por favor, espere a que finalice.", "error_unauthorized": "No ha iniciado sesión.", diff --git a/frontend/src/lib/components/document/DocumentEditLayout.svelte b/frontend/src/lib/components/document/DocumentEditLayout.svelte index 61024d00..04bb203c 100644 --- a/frontend/src/lib/components/document/DocumentEditLayout.svelte +++ b/frontend/src/lib/components/document/DocumentEditLayout.svelte @@ -6,6 +6,7 @@ import type { Snippet } from 'svelte'; import { createFileLoader } from '$lib/hooks/useFileLoader.svelte'; import { m } from '$lib/paraglide/messages.js'; import { countRequiredFilled } from '$lib/utils/requiredFields'; +import { validateFile } from '$lib/utils/validateFile'; import DocumentViewer from '$lib/components/DocumentViewer.svelte'; import UploadZone from '$lib/components/document/UploadZone.svelte'; import WhoWhenSection from '$lib/components/document/WhoWhenSection.svelte'; @@ -113,6 +114,15 @@ function cancelUpload() { async function handleReplaceFile(e: Event) { const file = (e.currentTarget as HTMLInputElement).files?.[0]; if (!file) return; + const validationError = validateFile(file); + if (validationError === 'type') { + uploadError = m.error_unsupported_file_type(); + return; + } + if (validationError === 'size') { + uploadError = m.error_file_too_large(); + return; + } await handleFile(file); } diff --git a/frontend/src/lib/utils/validateFile.spec.ts b/frontend/src/lib/utils/validateFile.spec.ts new file mode 100644 index 00000000..7ec2838b --- /dev/null +++ b/frontend/src/lib/utils/validateFile.spec.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { validateFile, MAX_SIZE_BYTES } from './validateFile'; + +function makeFile(type: string, size: number): File { + return new File(['x'.repeat(Math.min(size, 100))], 'test.file', { type }); +} + +describe('validateFile', () => { + it('returns null for a valid PDF under 50 MB', () => { + const file = makeFile('application/pdf', 1024); + expect(validateFile(file)).toBeNull(); + }); + + it('returns null for a valid JPEG', () => { + expect(validateFile(makeFile('image/jpeg', 1024))).toBeNull(); + }); + + it('returns null for a valid PNG', () => { + expect(validateFile(makeFile('image/png', 1024))).toBeNull(); + }); + + it('returns null for a valid TIFF', () => { + expect(validateFile(makeFile('image/tiff', 1024))).toBeNull(); + }); + + it('returns "type" for an unsupported MIME type', () => { + const file = makeFile('text/plain', 100); + expect(validateFile(file)).toBe('type'); + }); + + it('returns "size" for a file exceeding 50 MB', () => { + const oversized = new File(['x'], 'big.pdf', { type: 'application/pdf' }); + Object.defineProperty(oversized, 'size', { value: MAX_SIZE_BYTES + 1 }); + expect(validateFile(oversized)).toBe('size'); + }); +}); diff --git a/frontend/src/lib/utils/validateFile.ts b/frontend/src/lib/utils/validateFile.ts new file mode 100644 index 00000000..77d8c570 --- /dev/null +++ b/frontend/src/lib/utils/validateFile.ts @@ -0,0 +1,10 @@ +export const ALLOWED_TYPES = new Set(['application/pdf', 'image/jpeg', 'image/png', 'image/tiff']); +export const MAX_SIZE_BYTES = 50 * 1024 * 1024; + +export type FileValidationError = 'type' | 'size'; + +export function validateFile(file: File): FileValidationError | null { + if (!ALLOWED_TYPES.has(file.type)) return 'type'; + if (file.size > MAX_SIZE_BYTES) return 'size'; + return null; +}