Zum Inhalt springen
Deutsch

Statische Dateien

Dateien ausliefern, ohne zu einem backend-spezifischen Plugin zu greifen. getFromFile liefert eine Datei; getFromDirectory bildet eine URL auf einen Verzeichnisbaum ab; getFromBrowseableDirectory ergänzt HTML-Listings.

import { concat, getFromFile, getFromBrowseableDirectory, path } from 'actor-ts/http';
const routes = concat(
path('logo.svg', getFromFile('./assets/logo.svg')),
getFromBrowseableDirectory('static', './public'), // mountet unter /static
);

getFromDirectory([routePrefix,] fsRoot, options?) — mit routePrefix mountet es darunter (z. B. /static/...), ohne mountet es an der Wurzel.

Builder-MethodeFeldDefault
withIndexFiles(...n)indexFiles['index.html'] ([] deaktiviert)
withBrowse(flag?)browsefalse
withCacheControl(v)cacheControlHeader entfällt
withEtag(flag)etagtrue (schwach, size+mtime)
withLastModified(flag)lastModifiedtrue
withRanges(flag)rangestrue
withDotfiles('deny' | 'allow')dotfiles'deny'
withSymlinks('within-root' | 'follow')symlinks'within-root'
withContentTypes(map)contentTypes— (ext → type Overrides)
withMaxFileSize(bytes)maxFileSize50 MiB (größer → 413)
withStreamThreshold(bytes)streamThreshold— (nicht gesetzt: nichts streamt)
  • Conditional Requests — ein schwaches ETag (size + mtime) und Last-Modified; passendes If-None-Match / If-Modified-Since → 304.
  • Range — ein einzelner bytes=-Range → 206 (oder 416, wenn unerfüllbar); Accept-Ranges: bytes wird angekündigt. Es wird nur das angefragte Fenster gelesen, nie die ganze Datei.
  • Verzeichnisse — ein Request ohne Trailing-Slash 301-redirectet, um ihn zu ergänzen (Query erhalten); mit Slash werden Index-Dateien der Reihe nach probiert, dann ein Listing (falls Browsing) oder 404. Unter within-root gilt eine Index-Datei, die außerhalb der Wurzel landet, als nicht vorhanden.
  • HEAD — volle Header, leerer Body, kein Dateilesen.

Content-Types kommen von contentTypeFor(pathOrExt, overrides?) — einer kleinen Registry von ~45 gängigen Web-Typen. Text-artige Typen erhalten ; charset=utf-8; unbekannte Endungen fallen auf application/octet-stream zurück. Pro Route mit withContentTypes({ ext: 'type' }) überschreiben oder contentTypeFor direkt aufrufen, wenn du Responses von Hand baust.

Der URL-Rest wird vor der Validierung vollständig dekodiert (jede Encoding-Schicht abgetragen), dann wird jedes Pfad-Segment abgelehnt, wenn es .., leer, ein NUL, ein Backslash oder ein :-Segment ist (Windows-Laufwerk / NTFS Alternate Data Stream); absolute Formen werden verweigert, der zusammengesetzte Pfad wird auf die Wurzel eingegrenzt, und ein Symlink, der die Wurzel verlässt, wird verweigert (within-root- Default). Dotfiles sind standardmäßig verboten. Jede Ablehnung ist ein einheitlicher 404 — kein Informationsleck über Existierendes.

Die Eingrenzung wird bei jedem Dateisystem-Sprung eines Requests geprüft, nicht nur auf dem Pfad, den die URL aufgelöst hat: auch die Index-Datei eines Verzeichnisses und jeder Eintrag eines browsebaren Listings werden kanonisiert. Ein Link, der die Wurzel verlässt, ist ein 404 — egal ob er direkt angefragt oder als index.html erreicht wird — und er taucht im Listing gar nicht erst auf: Name, Größe und mtime sind genau das, was die Policy zurückhält. Auf withSymlinks('follow') umschalten, wenn der Baum absichtlich nach außen verlinkt (ein Build-Output, eine Link-Farm eines Paketmanagers).

Speicher: standardmäßig gepuffert, auf Wunsch gestreamt

Abschnitt betitelt „Speicher: standardmäßig gepuffert, auf Wunsch gestreamt“

Ein Body wird standardmäßig in den Speicher gelesen, begrenzt durch maxFileSize (50 MiB → eine größere Datei bekommt 413). Ein Range liest nur die angefragten Bytes, ein bytes=0-0 auf einer 50-MiB-Datei kostet also ein Byte und nicht 50 MiB.

Setze streamThreshold, und ein Body ab dieser Größe wird als Stream ausgeliefert, in 64-KiB-Häppchen gelesen — eine Datei, die größer als der Speicher des Prozesses ist, kostet damit so viel wie eine kleine:

import { getFromDirectory, StaticFilesOptions } from 'actor-ts/http';
const largeDownloads = StaticFilesOptions.create()
.withStreamThreshold(8 * 1024 * 1024)
.withMaxFileSize(8 * 1024 * 1024);
const routes = getFromDirectory('downloads', '/srv/releases', largeDownloads);

Daraus folgen zwei Dinge:

  • Der 413 entfällt. maxFileSize begrenzt, was gepuffert wird, und ein gestreamter Body wird es nicht. streamThreshold muss kleiner oder gleich maxFileSize sein (ein größerer Wert wird als Widerspruch abgelehnt) — damit kann nichts über die Schwelle hinaus gepuffert werden, die Grenze wird also unerreichbar statt aufgegeben, und es geht keine Schranke verloren.
  • Content-Length wird für dich gesetzt. Ein Backend hat bei einem Stream nichts zu messen, deshalb setzt der Handler den Header selbst. Fastify und Express schreiben ihn auf die Leitung; bei Hono hängt es am Server der Runtime ab (Bun.serve framet einen Stream chunked, node:http und Deno.serve nicht).

HEAD liest in beiden Fällen nichts.

  • Route-DSL — wie Direktiven komponieren.
  • HTML & XSS — das Listing escapet Dateinamen mit denselben Helfern.
  • Sicherheit — der empfohlene Stack.