import { createHash } from 'node:crypto' import { mkdir, rename } from 'node:fs/promises' import path from 'node:path' import { getEnv } from '@/lib/env' /** * Ablage der Dateien. * * Zwei Verzeichnisse unter `FILES_DIR`: `quarantine/` nimmt alles auf, was * gerade erst angekommen ist, `store/` nur, was die Prüfkette bestanden hat. * Ausgeliefert wird ausschliesslich aus `store/`. Siehe docs/07. * * Der Dateiname ist die Kennung des Anhangs, nie der Name, den der Nutzer * mitgeschickt hat - der steht in der Datenbank und wird erst beim Ausliefern * wieder gesetzt. Ein Pfad, der aus einer Eingabe entsteht, ist eine * Einladung. */ export const QUARANTINE = 'quarantine' export const STORE = 'store' /** * Zwei Ebenen aus den ersten vier Zeichen der Kennung. Ein Verzeichnis mit * zehntausend Einträgen ist auf keinem Dateisystem eine gute Idee, und die * Kennungen sind UUIDv7, also am Anfang zeitlich sortiert. */ export function storageKeyFor(bereich: typeof QUARANTINE | typeof STORE, id: string): string { const sauber = id.replace(/[^a-zA-Z0-9-]/g, '') return path.posix.join(bereich, sauber.slice(0, 2), sauber.slice(2, 4), sauber) } /** Absoluter Pfad zu einem Ablageschlüssel. Verlässt `FILES_DIR` nie. */ export function absolutePath(storageKey: string): string { const wurzel = path.resolve(getEnv().FILES_DIR) const ziel = path.resolve(wurzel, storageKey) // Doppelt gemoppelt: der Schlüssel kommt aus der Datenbank, nicht aus einer // Eingabe. Aber ein Verzeichniswechsel wäre der teuerste Fehler hier. if (ziel !== wurzel && !ziel.startsWith(wurzel + path.sep)) { throw new Error('Ablageschlüssel zeigt aus dem Dateiverzeichnis heraus') } return ziel } export async function ensureDirFor(storageKey: string): Promise { const ziel = absolutePath(storageKey) await mkdir(path.dirname(ziel), { recursive: true }) return ziel } /** Verschiebt aus der Quarantäne in die Ablage. */ export async function moveToStore(from: string, to: string): Promise { const ziel = await ensureDirFor(to) await rename(absolutePath(from), ziel) } export function sha256Of(content: Uint8Array): string { return createHash('sha256').update(content).digest('hex') }