diff --git a/paperless-backend/src/email/email-import.service.spec.ts b/paperless-backend/src/email/email-import.service.spec.ts index 711a77e..7fa053a 100644 --- a/paperless-backend/src/email/email-import.service.spec.ts +++ b/paperless-backend/src/email/email-import.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ConflictException } from '@nestjs/common'; +import { BadRequestException, ConflictException } from '@nestjs/common'; import { getRepositoryToken } from '@nestjs/typeorm'; import { ConfigService } from '@nestjs/config'; import { PDFDocument } from 'pdf-lib'; @@ -208,6 +208,33 @@ describe('EmailImportService – Duplikatprüfung', () => { }); }); + describe('getAttachmentPdf', () => { + beforeEach(async () => { + contentRepo.findOne.mockResolvedValue({ + Content1: await pdfMitSeiten(3), + }); + }); + + it('liefert den gewünschten Seitenbereich', async () => { + const pdf = await service.getAttachmentPdf(7, { start: 2, end: 3 }); + expect((await PDFDocument.load(pdf)).getPageCount()).toBe(2); + }); + + it.each([ + { start: 3, end: 2 }, + { start: 0, end: 2 }, + { start: 5, end: 6 }, + { start: Number.NaN, end: 2 }, + ])( + 'meldet einen ungültigen Seitenbereich $start–$end als 400', + async (pages) => { + await expect(service.getAttachmentPdf(7, pages)).rejects.toThrow( + BadRequestException, + ); + }, + ); + }); + describe('Import als Version', () => { it('hängt den Beleg als Version an, statt ein neues Dokument anzulegen', async () => { contentRepo.findOne.mockResolvedValue({ diff --git a/paperless-backend/src/email/email-import.service.ts b/paperless-backend/src/email/email-import.service.ts index 20622a5..c0fd1b2 100644 --- a/paperless-backend/src/email/email-import.service.ts +++ b/paperless-backend/src/email/email-import.service.ts @@ -4,6 +4,7 @@ import { HttpException, HttpStatus, ConflictException, + BadRequestException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; @@ -370,8 +371,19 @@ export class EmailImportService { ignoreEncryption: true, }); const total = pdfDoc.getPageCount(); - const startIdx = Math.max(1, pages.start) - 1; - const endIdx = Math.min(pages.end === 999 ? total : pages.end, total) - 1; + const ende = pages.end === 999 ? total : Math.min(pages.end, total); + if ( + !Number.isInteger(pages.start) || + !Number.isInteger(ende) || + pages.start < 1 || + pages.start > ende + ) { + throw new BadRequestException( + `Ungültiger Seitenbereich ${pages.start}–${pages.end} (Dokument hat ${total} Seiten)`, + ); + } + const startIdx = pages.start - 1; + const endIdx = ende - 1; const sliced = await PDFDocument.create(); const indices = Array.from( { length: endIdx - startIdx + 1 }, diff --git a/paperless-frontend/src/components/DuplikatVergleichModal.tsx b/paperless-frontend/src/components/DuplikatVergleichModal.tsx index c9fffaa..dae68ec 100644 --- a/paperless-frontend/src/components/DuplikatVergleichModal.tsx +++ b/paperless-frontend/src/components/DuplikatVergleichModal.tsx @@ -3,6 +3,25 @@ import { Alert, Button, Modal, Space, Spin, Tag, Typography, message } from 'ant import { paperlessApi, type BelegnummerPruefung, type VergleichKennzahlen } from '../api/paperless'; import { useIsMobile } from '../hooks/useIsMobile'; +/** Länger wird auf Seiten-/Wortzahl nicht gewartet – der Vergleich bleibt trotzdem möglich. */ +const KENNZAHLEN_TIMEOUT_MS = 30000; + +function mitTimeout(promise: Promise, ms: number): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Zeitüberschreitung')), ms); + promise.then( + (wert) => { + clearTimeout(timer); + resolve(wert); + }, + (err: unknown) => { + clearTimeout(timer); + reject(err instanceof Error ? err : new Error(String(err))); + }, + ); + }); +} + export interface DuplikatVergleichModalProps { open: boolean; belegnummer: string; @@ -70,6 +89,7 @@ export function DuplikatVergleichModal({ const [vorhandenUrl, setVorhandenUrl] = useState(null); const [neuUrl, setNeuUrl] = useState(null); const [kennzahlen, setKennzahlen] = useState(null); + const [kennzahlenFehler, setKennzahlenFehler] = useState(false); const [loading, setLoading] = useState(false); useEffect(() => { @@ -79,6 +99,7 @@ export function DuplikatVergleichModal({ let urlNeu: string | null = null; setLoading(true); setKennzahlen(null); + setKennzahlenFehler(false); (async () => { try { const [altBlob, neuBlob] = await Promise.all([paperlessApi.getDocumentPdfBlob(paperlessDocumentId), ladeNeuesPdf()]); @@ -93,12 +114,12 @@ export function DuplikatVergleichModal({ if (!cancelled) setLoading(false); } })(); - ladeKennzahlen() + mitTimeout(ladeKennzahlen(), KENNZAHLEN_TIMEOUT_MS) .then((k) => { if (!cancelled) setKennzahlen(k); }) .catch(() => { - if (!cancelled) message.warning('Seiten- und Wortzahl konnten nicht ermittelt werden'); + if (!cancelled) setKennzahlenFehler(true); }); return () => { cancelled = true; @@ -114,7 +135,11 @@ export function DuplikatVergleichModal({ const versionMoeglich = pruefung?.status === 'eindeutig'; let auffaelligkeiten: ReactNode = ; - if (kennzahlen) { + if (kennzahlenFehler) { + auffaelligkeiten = ( + + ); + } else if (kennzahlen) { const { vorhanden, neu } = kennzahlen; const alleBekannt = [vorhanden.seiten, neu.seiten, vorhanden.woerter, neu.woerter].every((w) => w !== null); const gleich = alleBekannt && vorhanden.seiten === neu.seiten && vorhanden.woerter === neu.woerter;