diff --git a/src/lib/zip-processing.ts b/src/lib/zip-processing.ts index 8ff3c8d..b412d4c 100644 --- a/src/lib/zip-processing.ts +++ b/src/lib/zip-processing.ts @@ -1,6 +1,6 @@ -import { unzip } from "fflate"; +import { unzip } from 'fflate'; -export type ZipAttachmentKind = "pdf" | "image"; +export type ZipAttachmentKind = 'pdf' | 'image'; export type ZipAttachment = { name: string; @@ -10,25 +10,31 @@ export type ZipAttachment = { }; export type ProcessedZipArchive = { - name: string - attachments: ZipAttachment[] + name: string; + attachments: ZipAttachment[]; +}; + +type XJustizMetadata = { + sender: string; + receiver: string; + documentNames: string[]; }; const IMAGE_EXTENSIONS = new Set([ - ".avif", - ".bmp", - ".gif", - ".heic", - ".jpeg", - ".jpg", - ".jp2", - ".png", - ".tif", - ".tiff", - ".webp", + '.avif', + '.bmp', + '.gif', + '.heic', + '.jpeg', + '.jpg', + '.jp2', + '.png', + '.tif', + '.tiff', + '.webp' ]); -const ZIP_META_FILE_NAME = "xjustiz_nachricht.xml"; +const ZIP_META_FILE_NAME = 'xjustiz_nachricht.xml'; const XML_TEXT_DECODER = new TextDecoder(); const unzipArchive = (data: Uint8Array) => @@ -43,37 +49,37 @@ const unzipArchive = (data: Uint8Array) => }); }); -const getBaseName = (path: string) => path.split("/").pop() ?? path; +const getBaseName = (path: string) => path.split('/').pop() ?? path; const getExtension = (path: string) => { const baseName = getBaseName(path).toLowerCase(); - const lastDotIndex = baseName.lastIndexOf("."); + const lastDotIndex = baseName.lastIndexOf('.'); - return lastDotIndex === -1 ? "" : baseName.slice(lastDotIndex); + return lastDotIndex === -1 ? '' : baseName.slice(lastDotIndex); }; -const isPdf = (path: string) => getExtension(path) === ".pdf"; +const isPdf = (path: string) => getExtension(path) === '.pdf'; const isImage = (path: string) => IMAGE_EXTENSIONS.has(getExtension(path)); -const isZipArchive = (path: string) => getExtension(path) === ".zip"; +const isZipArchive = (path: string) => getExtension(path) === '.zip'; -const normalizeKey = (value: string) => value.replaceAll("\\", "/").toLowerCase().trim(); +const normalizeKey = (value: string) => value.replaceAll('\\', '/').toLowerCase().trim(); const isMacMetadataEntry = (path: string) => { const normalizedPath = normalizeKey(path); const baseName = getBaseName(normalizedPath); return ( - normalizedPath.startsWith("__macosx/") || - normalizedPath.includes("/__macosx/") || - baseName === ".ds_store" || - baseName.startsWith("._") + normalizedPath.startsWith('__macosx/') || + normalizedPath.includes('/__macosx/') || + baseName === '.ds_store' || + baseName.startsWith('._') ); }; const matchesElementName = (element: Element, requestedName: string) => { - const requestedLocalName = requestedName.split(".").pop() ?? requestedName; + const requestedLocalName = requestedName.split('.').pop() ?? requestedName; return ( element.localName === requestedName || @@ -88,7 +94,9 @@ const getDirectChildByLocalName = (element: Element, localName: string) => Array.from(element.children).find((child) => matchesElementName(child, localName)) ?? null; const getFirstDescendantByLocalName = (root: Document | Element, localName: string) => - Array.from(root.querySelectorAll("*")).find((element) => matchesElementName(element, localName)) ?? null; + Array.from(root.querySelectorAll('*')).find((element) => + matchesElementName(element, localName) + ) ?? null; const getTextContentByPath = (root: Element, path: string[]) => { let current: Element | null = root; @@ -106,37 +114,54 @@ const getTextContentByPath = (root: Element, path: string[]) => { return textContent ? textContent : null; }; +const getTextContentByLocalName = (root: Document | Element, localName: string) => + getFirstDescendantByLocalName(root, localName)?.textContent?.trim() ?? null; + const parseXmlDocument = (xmlBytes: Uint8Array) => { const parser = new DOMParser(); - const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), "application/xml"); + const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), 'application/xml'); - if (document.querySelector("parsererror")) { + if (document.querySelector('parsererror')) { return null; } return document; }; -const getXJustizDocumentNames = (xmlBytes: Uint8Array) => { +const parseXJustizMetadata = (xmlBytes: Uint8Array): XJustizMetadata | null => { const document = parseXmlDocument(xmlBytes); if (!document) { - return []; + return null; } - const schriftgutobjekte = getFirstDescendantByLocalName(document, "schriftgutobjekte"); + const sender = getTextContentByLocalName(document, 'aktenzeichen.absender') ?? ''; + const receiver = getTextContentByLocalName(document, 'aktenzeichen.empfaenger') ?? ''; + const schriftgutobjekte = getFirstDescendantByLocalName(document, 'schriftgutobjekte'); if (!schriftgutobjekte) { - return []; + return { + sender, + receiver, + documentNames: [] + }; } const dokumentNodes = Array.from(schriftgutobjekte.children).filter((child) => - matchesElementName(child, "dokument"), + matchesElementName(child, 'dokument') ); - return dokumentNodes - .map((dokument) => getTextContentByPath(dokument, ["xjustiz.fachspezifischeDaten", "datei", "dateiname"])) + const documentNames = dokumentNodes + .map((dokument) => + getTextContentByPath(dokument, ['xjustiz.fachspezifischeDaten', 'datei', 'dateiname']) + ) .filter((name): name is string => Boolean(name)); + + return { + sender, + receiver, + documentNames + }; }; const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentNames: string[]) => { @@ -190,7 +215,10 @@ const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentN return orderedAttachments; }; -const extractZipEntries = async (archiveBytes: Uint8Array, fallbackName: string): Promise => { +const extractZipEntries = async ( + archiveBytes: Uint8Array, + _fallbackName: string +): Promise => { const files = await unzipArchive(archiveBytes); const attachments: ZipAttachment[] = []; const nestedArchives: ProcessedZipArchive[] = []; @@ -218,8 +246,8 @@ const extractZipEntries = async (archiveBytes: Uint8Array, fallbackName: string) attachments.push({ name: baseName, path, - kind: "pdf", - data, + kind: 'pdf', + data }); continue; } @@ -228,21 +256,26 @@ const extractZipEntries = async (archiveBytes: Uint8Array, fallbackName: string) attachments.push({ name: baseName, path, - kind: "image", - data, + kind: 'image', + data }); } } - const documentNames = xjustizNachrichtXml ? getXJustizDocumentNames(xjustizNachrichtXml) : []; + const metadata = xjustizNachrichtXml ? parseXJustizMetadata(xjustizNachrichtXml) : null; + const documentNames = metadata?.documentNames ?? []; const orderedAttachments = orderAttachmentsByDocumentNames(attachments, documentNames); - const archiveName = documentNames[0] ? getBaseName(documentNames[0]) : fallbackName; + const now = new Date(); + const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + const archiveName = metadata + ? `${date}_${metadata.sender}_${metadata.receiver}.pdf` + : `${date}_unbekannt_unbekannt.pdf`; const archives: ProcessedZipArchive[] = []; if (orderedAttachments.length > 0) { archives.push({ name: archiveName, - attachments: orderedAttachments, + attachments: orderedAttachments }); }