Static files¶
Serve a directory beside your API: a built front end, its scripts and stylesheets, a favicon.
Serving a directory¶
let routes = [ Spindle.Static.directory "public" ]
let () =
Spindle.Log.setup ();
Eio_main.run @@ fun env -> Spindle.serve env routes
"public" is a folder next to where you start the program. The example’s is
examples/public, so start it from examples/ and ask for a file:
HTTP/1.1 200 OK
content-type: text/css; charset=utf-8
accept-ranges: bytes
etag: "4e3597b273bb16778f1093c5780a9fe2"
cache-control: no-cache
x-content-type-options: nosniff
content-length: 88
...
/ is the directory’s index.html, and a path with no file is the
framework’s 404.
- The directory is read into memory once, when the server starts. A
request is a lookup among those files, never a path on disk, so
../reaches nothing. A file you change afterwards is served after a restart. - A directory that cannot be read stops the server: it logs why and
exits 1, instead of answering
404to everything. - It is a route like any other, over the rest of the path, so it goes in the same list as your API and every other route is matched first.
- Every file carries an
etag, so a browser revalidates cheaply (304), and oneRangeis answered with206. - Every file carries
X-Content-Type-Options: nosniff, so a browser takes the type it is given and never guesses one from the bytes. - A precompressed copy is served when the browser accepts it. Beside
app.js, anapp.js.br,app.js.zstorapp.js.gzyour build made is sent withContent-EncodingandVary: Accept-Encoding. See Compression. - Names that start with a dot are skipped, at any depth, so
.envand.gitare never served. The one exception is.well-knownat the root, which belongs to the site (RFC 8615).~dotfiles:trueserves them all.
Static files beside an API¶
A built front end at the root, its hashed assets cached for a year, and a page of its own for what is not there:
let routes =
api
@ [
Spindle.Static.directory ~not_found:"/404.html" ~immutable:[ "/_astro/" ]
"dist";
]
let () = Eio_main.run @@ fun env -> Spindle.serve env routes
The options of Static.directory. The files and prefixes they name are
request paths, starting with / ("/404.html", not "404.html"); a file
that is not in the directory stops the server at start:
~immutable– prefixes whose files are cached for a year. Every other file isno-cache; there is nothing in between, so a page never outlives the assets it names.~not_found– the file answered, with a404, for a path with no file.~shell:(file, prefixes)– answers every path under the prefixes with one document and a200, for client-side pages whose ids are made at run time:~shell:("/app/index.html", [ "/orders/" ]). A prefix ends in/, so/ordersheetis not under/orders/.~index– the file a directory answers with,"index.html"unless told.~at– serve under a prefix instead of the root:~at:Path.(s "static").~types– add content types by extension:[ (".wasm", "application/wasm") ]. An unknown extension is served as bytes.
Spindle.Test.app refuses Static.directory, which reads its files when a
real server starts. In a test, load the directory yourself and serve it with
Static.route:
match Spindle.Static.load Eio.Path.(Eio.Stdenv.cwd env / "public") with
| Ok files -> Spindle.Test.app (routes @ [ Spindle.Static.route files ])
| Error e -> Alcotest.fail e
Files that change at run time¶
For files that change while the server runs – uploads, generated reports –
use Spindle.Files, which opens the file when the request arrives and
streams it:
- It never leaves its directory. A segment that is
.., starts with a dot, or holds a/or\is404before anything is opened, and a symlink out of the directory is refused by the operating system. - Nothing lists a directory: one is
404, or its~indexwhere you name one. - An upload never runs as your site. A file a browser would open as a
page of your origin – HTML, SVG, any XML – is sent as an attachment and
carries
Content-Security-Policy: sandbox, so an uploadedx.htmlis downloaded rather than run beside your cookies.~inline_active:trueshows one in the browser instead, still sandboxed: with no origin of its own and none of its script run. ~download:truesends each file as an attachment with its name;~dotfiles:trueserves names that start with a dot.- Answers carry an
etag,Last-Modifiedandnosniff, and answer conditional requests and oneRange, asStaticdoes. - Replace a file by writing it elsewhere and renaming it over the old
one. A file rewritten in place, at the same size within the clock’s tick,
keeps its
etag.
Spindle.Test.app refuses Files.directory too. Spindle.Files.route takes
the same options over a directory you already hold as an Eio.Path.t, so a
test can serve one of its own:
Spindle.Files.route Eio.Path.(Eio.Stdenv.cwd env / "uploads").
Next: database.