This commit was merged in pull request #59.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Grenzen und Formen der Anhaenge (#032).
|
||||
*
|
||||
* Framework-frei, weil beide Seiten davon lesen: der Dienst prueft damit, und
|
||||
* die Oberflaeche sagt damit **vorher**, was erlaubt ist. Stuenden die Grenzen
|
||||
* nur im Dienst, waere jede Abweisung eine Ueberraschung.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Was sich anhaengen laesst: Bilder und PDF.
|
||||
*
|
||||
* Bewusst eine Liste und kein Praefix-Vergleich wie `image/*`: ein Browser
|
||||
* schickt auch `image/svg+xml`, und SVG ist ein Dokument mit Skripten, das ein
|
||||
* Betrachter ausfuehrt. Ein Anhang, den jemand oeffnet, darf kein Angriffsweg
|
||||
* sein.
|
||||
*/
|
||||
export const ERLAUBTE_MEDIENTYPEN = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"application/pdf",
|
||||
] as const;
|
||||
|
||||
export type Medientyp = (typeof ERLAUBTE_MEDIENTYPEN)[number];
|
||||
|
||||
/** Die Endung, unter der eine Datei auf der Platte liegt. */
|
||||
export const ENDUNG_JE_TYP: Readonly<Record<Medientyp, string>> = {
|
||||
"image/jpeg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/webp": "webp",
|
||||
"image/heic": "heic",
|
||||
"image/heif": "heif",
|
||||
"application/pdf": "pdf",
|
||||
};
|
||||
|
||||
export function istErlaubterMedientyp(typ: string): typ is Medientyp {
|
||||
return (ERLAUBTE_MEDIENTYPEN as readonly string[]).includes(typ);
|
||||
}
|
||||
|
||||
/** Bilder werden als Miniatur gezeigt, ein PDF nur als Zeile. */
|
||||
export function istBild(medientyp: string): boolean {
|
||||
return medientyp !== "application/pdf" && istErlaubterMedientyp(medientyp);
|
||||
}
|
||||
|
||||
const STANDARD_MAX_MB = 10;
|
||||
const STANDARD_JE_LAUF = 20;
|
||||
|
||||
/**
|
||||
* Hoechstgroesse je Datei in Bytes.
|
||||
*
|
||||
* Ein Handyfoto hat heute 3–6 MB. Zehn als Vorgabe lassen Luft, ohne dass eine
|
||||
* einzelne Datei eine Sicherung sprengt. Keine serverseitige Verkleinerung:
|
||||
* sie verliert Metadaten und zieht eine Bildbibliothek ins Projekt — eine
|
||||
* ehrliche Obergrenze ist weniger Magie.
|
||||
*/
|
||||
export function maxGroesseBytes(): number {
|
||||
const roh = Number.parseInt(process.env["ANHANG_MAX_MB"]?.trim() ?? "", 10);
|
||||
const mb = Number.isFinite(roh) && roh > 0 ? roh : STANDARD_MAX_MB;
|
||||
return mb * 1024 * 1024;
|
||||
}
|
||||
|
||||
/** Hoechstzahl Anhaenge je Lauf. */
|
||||
export function maxJeLauf(): number {
|
||||
const roh = Number.parseInt(process.env["ANHAENGE_JE_LAUF"]?.trim() ?? "", 10);
|
||||
return Number.isFinite(roh) && roh > 0 ? roh : STANDARD_JE_LAUF;
|
||||
}
|
||||
|
||||
/** Wo die Dateien liegen. Ohne Angabe ein Verzeichnis im Arbeitsbaum. */
|
||||
export function anhaengePfad(): string {
|
||||
return process.env["ANHAENGE_PFAD"]?.trim() || "./daten/anhaenge";
|
||||
}
|
||||
|
||||
/**
|
||||
* Die eine gewaehlte Datei aus mehreren Formularfeldern desselben Namens.
|
||||
*
|
||||
* Der Schritt hat **zwei** Felder `datei` in einem Formular: „Foto“ mit
|
||||
* `capture="environment"` und „Datei“ mit dem Dateiwaehler. Abgeschickt wird
|
||||
* immer das ganze Formular, also kommen beide Felder mit — das leere als
|
||||
* `File` mit `size === 0`. `FormData.get("datei")` liefert das **erste**, und
|
||||
* das war in #032 das leere: wer „Datei“ nutzte, bekam „Keine Datei gewählt“,
|
||||
* waehrend „Foto“ ging. Gefunden erst in der Klickprobe am gebauten Stand, weil
|
||||
* kein Dienst-Test zwei Felder schickt.
|
||||
*
|
||||
* Darum hier die Regel, und zwar als reine Funktion: das **erste nicht leere**
|
||||
* Feld gewinnt, unabhaengig von der Reihenfolge im Formular.
|
||||
*/
|
||||
export function gewaehlteDatei(eintraege: readonly unknown[]): File | undefined {
|
||||
return eintraege.find((eintrag): eintrag is File => eintrag instanceof File && eintrag.size > 0);
|
||||
}
|
||||
|
||||
/** `4,2 MB` – fuer Meldungen und die Anzeige. */
|
||||
export function groesseText(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1).replace(".", ",")} MB`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Dateiname, der sich anzeigen laesst.
|
||||
*
|
||||
* Der Name vom Browser wird **nie** zum Pfad — der entsteht aus der `id`.
|
||||
* Trotzdem wird er beschnitten und von Steuerzeichen befreit: er landet in einer
|
||||
* Antwortkopfzeile und in der Oberflaeche.
|
||||
*/
|
||||
export function dateinameSaeubern(roh: string): string {
|
||||
const ohnePfad = roh.split(/[\\/]/).pop() ?? "";
|
||||
const sauber = ohnePfad.replace(/[\u0000-\u001f\u007f"]/g, "").trim();
|
||||
return (sauber || "anhang").slice(0, 200);
|
||||
}
|
||||
Reference in New Issue
Block a user