Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
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<string> {
|
|
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<void> {
|
|
const ziel = await ensureDirFor(to)
|
|
await rename(absolutePath(from), ziel)
|
|
}
|
|
|
|
export function sha256Of(content: Uint8Array): string {
|
|
return createHash('sha256').update(content).digest('hex')
|
|
}
|