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.
Optionen
Abschnitt betitelt „Optionen“| Builder-Methode | Feld | Default |
|---|---|---|
withIndexFiles(...n) | indexFiles | ['index.html'] ([] deaktiviert) |
withBrowse(flag?) | browse | false |
withCacheControl(v) | cacheControl | Header entfällt |
withEtag(flag) | etag | true (schwach, size+mtime) |
withLastModified(flag) | lastModified | true |
withRanges(flag) | ranges | true |
withDotfiles('deny' | 'allow') | dotfiles | 'deny' |
withSymlinks('within-root' | 'follow') | symlinks | 'within-root' |
withContentTypes(map) | contentTypes | — (ext → type Overrides) |
withMaxFileSize(bytes) | maxFileSize | 50 MiB (größer → 413) |
withStreamThreshold(bytes) | streamThreshold | — (nicht gesetzt: nichts streamt) |
Verhalten
Abschnitt betitelt „Verhalten“- Conditional Requests — ein schwaches
ETag(size + mtime) undLast-Modified; passendesIf-None-Match/If-Modified-Since→304. - Range — ein einzelner
bytes=-Range →206(oder416, wenn unerfüllbar);Accept-Ranges: byteswird 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) oder404. Unterwithin-rootgilt eine Index-Datei, die außerhalb der Wurzel landet, als nicht vorhanden. - HEAD — volle Header, leerer Body, kein Dateilesen.
MIME-Typen
Abschnitt betitelt „MIME-Typen“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.
Sicherheitsmodell
Abschnitt betitelt „Sicherheitsmodell“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
413entfällt.maxFileSizebegrenzt, was gepuffert wird, und ein gestreamter Body wird es nicht.streamThresholdmuss kleiner oder gleichmaxFileSizesein (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-Lengthwird 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.serveframet einen Stream chunked,node:httpundDeno.servenicht).
HEAD liest in beiden Fällen nichts.
Wie weiter
Abschnitt betitelt „Wie weiter“- Route-DSL — wie Direktiven komponieren.
- HTML & XSS — das Listing escapet Dateinamen mit denselben Helfern.
- Sicherheit — der empfohlene Stack.
