Rechnungen mit Positionen: gleiche Nummer ergibt eine Sammelrechnung ueber mehrere Laufzeiten. PDF-Upload mit Inhaltspruefung und Ablage unter der Pruefsumme, Haekchen direkt in der Tabelle, Filter nach Status, Kunde und Jahr. 78 Tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
54 lines
1.9 KiB
TypeScript
54 lines
1.9 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { prisma } from "./db";
|
|
|
|
/** Rechnungs-PDFs liegen auf der Platte, nur die Metadaten in der Datenbank. */
|
|
export const UPLOAD_VERZEICHNIS = process.env.UPLOAD_DIR ?? path.join(process.cwd(), "data", "uploads");
|
|
export const MAX_GROESSE = 10 * 1024 * 1024;
|
|
|
|
export class UploadFehler extends Error {}
|
|
|
|
/**
|
|
* Nimmt ein PDF entgegen und legt es unter seiner Prüfsumme ab.
|
|
* Gleiche Datei zweimal hochgeladen heißt: derselbe Anhang, kein zweites Exemplar auf der Platte.
|
|
*/
|
|
export async function anhangSpeichern(datei: File): Promise<string> {
|
|
if (datei.size === 0) throw new UploadFehler("Die Datei ist leer.");
|
|
if (datei.size > MAX_GROESSE) {
|
|
throw new UploadFehler(`Die Datei ist größer als ${MAX_GROESSE / 1024 / 1024} MB.`);
|
|
}
|
|
|
|
const inhalt = Buffer.from(await datei.arrayBuffer());
|
|
|
|
// Nicht auf den gemeldeten Typ verlassen, sondern in die Datei sehen.
|
|
if (inhalt.subarray(0, 5).toString("latin1") !== "%PDF-") {
|
|
throw new UploadFehler("Nur PDF-Dateien sind erlaubt.");
|
|
}
|
|
|
|
const sha256 = createHash("sha256").update(inhalt).digest("hex");
|
|
const vorhanden = await prisma.anhang.findFirst({ where: { sha256 } });
|
|
if (vorhanden) return vorhanden.id;
|
|
|
|
await mkdir(UPLOAD_VERZEICHNIS, { recursive: true });
|
|
const dateiname = `${sha256}.pdf`;
|
|
await writeFile(path.join(UPLOAD_VERZEICHNIS, dateiname), inhalt, { mode: 0o640 });
|
|
|
|
const anhang = await prisma.anhang.create({
|
|
data: {
|
|
dateiname: datei.name || dateiname,
|
|
pfad: dateiname,
|
|
mimeTyp: "application/pdf",
|
|
groesse: inhalt.length,
|
|
sha256,
|
|
},
|
|
});
|
|
|
|
return anhang.id;
|
|
}
|
|
|
|
export function anhangPfad(pfad: string): string {
|
|
// Nur der Dateiname aus der Datenbank wird verwendet; Pfadangaben darin werden verworfen.
|
|
return path.join(UPLOAD_VERZEICHNIS, path.basename(pfad));
|
|
}
|