Reference · Files and processes
file
file(str, map | null=) -> responseWhat it does
A response that sends a file from the program's folder, for a route to answer with:
route GET /report = file("exports/report.pdf"). It serves the one file it is given,
whatever its name, so a path from the visitor goes to files
instead, which does not serve .env, .git/, the program's own files or a database:
route GET /assets/*path = files("public", path). The content type comes from the
extension, the response carries an ETag and Last-Modified, and a browser that asks
with a current copy gets a 304 with no body. A file that is not there, a folder, or a
path with .. or starting at / gives a 404 response, not an error. The options map
takes cache: seconds, or a duration like "10m", "12h", "30d", "1y", which lets
the browser keep the file that long without asking; without it the answer says
no-cache (keep it, but ask each time). It also takes download, a file name, and
type, a content type in place of the extension's. The file goes out as it is on disk:
an HTML file gets none of the scripts Quillo adds to the pages a route makes, so its
ETag and a Range of it are of the file's own bytes. For a whole folder served as a
static host serves it (index.html, /about for about.html, 404.html), see
files.
Examples
route GET /report = file("exports/report.pdf")route GET /favicon.svg = file("public/favicon.svg", { cache: "30d" })route GET /assets/*path = files("public", path) // a path from the visitorroute GET /build/*path = files("dist", path, { cache: "1y" }) // names that change with contentThe value can be looked at like any response: its status, headers and body.
write_file("public/app.css", "body { margin: 0 }")let r = file("public/app.css")print(r)print(r.status, r.headers["content-type"], r.headers["cache-control"])print(r.body)print(file("public/app.css", { cache: "1y" }).headers["cache-control"])<response 200>200 text/css; charset=utf-8 no-cachebody { margin: 0 }public, max-age=31536000, immutableEdge cases
file("public/" + path)with the visitor'spathgives/assets/.envand/assets/main.qweaway (the guide's example until 0.7).quillo checkwarns about afile()whose path is made from a route's:nameor*path:`file` serves the one file it is given, whatever its name: with a path from the request, `files("public", path)` keeps `.env`, `.git` and the program's own files back.- A missing file is a
404response whose body isnot found; so is a folder, a path with..or one starting at/. It is never an error value, so??andis_errdo not see it. - Links are followed, and where they lead is what counts: a link inside the folder
that leads inside it works as its target would, and one that leads out of the
folder, or leads nowhere, is the same
404. The first such path is said once in the log, with where it leads and--allow-path FOLDER, the option ofquillo runthat lets the program reach a folder outside on purpose (quillo run main.qwe --allow-path /srv/media; see the options). - An extension the list does not know is sent as
application/octet-stream. The list:html htm css js mjs json map webmanifest xml xsl txt text md markdown csv tsv svg png jpg jpeg gif webp avif bmp tif tiff heic jxl ico mp4 m4v mov webm m4a aac flac opus mp3 ogg oga wav wasm woff2 woff ttf otf pdf zip, and for feeds, calendars, subtitles and streamsrss atom jsonld geojson yaml yml toml ics vtt srt m3u8 mpd ts m4s. A.mdfile istext/markdown; charset=utf-8. - A request's conditions are read as RFC 9110 has them:
If-None-Match, elseIf-Modified-Since, gives a304to aGETorHEADwhen the file has not changed since the date given, a later date included, in any of HTTP's three date forms (Sun, 06 Nov 1994 08:49:37 GMT,Sunday, 06-Nov-94 08:49:37 GMT,Sun Nov 6 08:49:37 1994); another method whoseIf-None-Matchmatches gets a412.If-Matchthat names another version, orIf-Unmodified-Sincebefore the file changed, gives a412with no body. The file's own weak tag, with itsW/or without, passesIf-MatchandIf-Range, as nginx has it, so a download resumed or a video sought gets its part. A date that does not parse is not a condition. Range: bytes=...gets a206with those bytes: the unit in any case (Bytes=0-1), the numbers digits only (bytes=+2-4is no range, and gets the whole file), an end past the file read as the file's end. A range that starts past the end gets a416, and several ranges at once the whole file.cachecan be a number of seconds; the header is thenpublic, max-age=N, immutable.- Files up to 1 MiB are kept in memory and re-read when they change; larger ones are
streamed from disk. A file changed in the last moment (50 ms, or 2 s where the file
system keeps whole seconds) is read but not kept, so one being written is never kept
half written. A file rewritten in place while it is read is sent as it was read; only
a large one replaced at every try is answered
503withRetry-After. A busy file is never a404. - A text file gzipped for browsers that take it says
Vary: accept-encodingin its plain copy too, so a shared cache keeps both; aHEADanswers as aGETwould, gzip included. - A SQLite database is sent when it is named here on purpose (
file("backup.db")), ranges included;files()andquillo servenever send one.
write_file("data.bin", "x")print(file("nope.txt").status, file("nope.txt").body)print(file("../secret").status, file("/etc/hosts").status, file("public").status)print(is_err(file("nope.txt")))print(file("data.bin").headers["content-type"])print(file("data.bin", { cache: 60 }).headers["cache-control"])404 not found404 404 404falseapplication/octet-streampublic, max-age=60, immutableErrors
A path that is not a string, options that are not a map, a cache that is neither
seconds nor a duration, an empty type or download, and a type with a line break or
another control character are runtime errors, not error values. The options are only
looked at for a file that exists: file("nope", { cache: "soon" }) is a plain 404.
A download name is written so a name in any script arrives whole; its control
characters are not sent as they are.
write_file("a.txt", "a")print(file("a.txt", { cache: "soon" }))line 2: `file`'s cache is seconds or a duration like "30d", got "soon"write_file("a.txt", "a")print(file("a.txt", { type: "" }))line 2: `file`'s type is empty: give a content type, like "text/csv", or leave it outChaining
In a pipeline the path comes first; the options follow in the call:
write_file("public/app.js", "1")let path = "app.js"print("public/" + path | file)print(("public/" + path | file({ cache: "1y" })).headers["cache-control"])<response 200>public, max-age=31536000, immutableSee also
read_file, write_file, respond, redirect, image.convert
Thread 0 posts
On file(): questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.
Edit this page on GitHub · every example's output was made by running it (tools/refcheck.mjs) · questions on Discord or r/quillo