# directory
Serves static files from a directory: MIME detection, ETags, `Cache-Control`,
HTTP range requests, chunked streaming for large files, forced downloads and an
optional HTML directory index.
- **Step:** `request` (default) or `proxy_upstream` — configurable
- **Registered as:** `directory`
## Configuration
| `category` | string | — | Must be `directory`. |
| `path` | string | — | **Required.** Root directory. `~` is expanded and the path is made absolute. |
| `index` | string | `index.html` | File served for a directory, `/` or any deeper one. A leading `/` is added if missing. |
| `autoindex` | bool | `false` | Generate an HTML listing for directories. |
| `chunk_size` | bytesize | `4kb` | Streaming chunk size; also the threshold above which streaming is used. A size string or a byte count; floored at 4 KB. A value that does not parse is a configuration error. |
| `max_age` | duration | — | `Cache-Control: max-age=…`. Not applied to `text/html`. A duration that does not parse is a configuration error. |
| `private` | bool | `false` | Add `private` to `Cache-Control`. |
| `charset` | string | — | Appended to `Content-Type` for `text/*`. |
| `download` | bool | `false` | Add `Content-Disposition: attachment`. |
| `follow_symlinks` | bool | `true` | When `false`, a file must still be under `path` once symlinks are resolved. |
| `headers` | string[] | — | Extra response headers as `Name: value`. |
| `step` | string | `request` | `request` or `proxy_upstream`. |
## Examples
Serve a built SPA:
```toml
[plugins.web]
category = "directory"
path = "/var/www/app"
index = "index.html"
chunk_size = "64kb"
max_age = "1h"
charset = "utf-8"
headers = ["X-Content-Type-Options: nosniff"]
[locations.web]
path = "/"
plugins = ["web"]
```
A browsable download area:
```toml
[plugins.files]
category = "directory"
path = "~/Downloads"
autoindex = true
download = true
chunk_size = "1mb"
```
Range request:
```bash
curl -r 0-1023 -i http://127.0.0.1:6188/big.iso
# HTTP/1.1 206 Partial Content
# content-range: bytes 0-1023/734003200
# accept-ranges: bytes
```
## Behaviour
- A request whose `If-None-Match` names the file's ETag is answered `304 Not
Modified` with no body (the comparison is weak, so `W/` prefixes do not
matter, and `*` always matches).
- Every response carries `Accept-Ranges: bytes` and a weak ETag derived from size
and mtime (`W/"<size hex>-<mtime hex>"`).
- `text/html` responses are treated as non-cacheable, so `max_age` is not applied
to them — the SPA shell stays fresh while hashed assets are cached.
- `bytes=start-end`, `bytes=start-` and `bytes=-suffix` are supported; only the
first range of a multi-range request is honoured. An unsatisfiable range gets
`416` with `Content-Range: bytes */<size>`.
- Files at or below `chunk_size` are read into memory and sent in one response;
larger ones are streamed.
- `autoindex` listings are sorted by name and skip dotfiles; names are
HTML-escaped and links percent-encoded, so a file called `<script>` or
`a b.txt` is listed and linked correctly.
## Responses
| File found | `200`, or `206` for a range request |
| Path escapes `path` after normalisation | `403` |
| File missing | `404 Not Found` |
| Other IO error | `500 File access error` |
| Bad range | `416 Range Not Satisfiable` |
## Usage notes
- A directory request serves its `index` file at any depth when `autoindex` is
off (`/docs/` finds `docs/index.html`), and the listing when it is on.
- Traversal protection is lexical by default: the joined path is normalised and
must still start with `path`, which catches `../` but not a **symlink inside
the served directory that points outside it**. Set `follow_symlinks = false`
to also compare the resolved real path, at the cost of one extra `realpath`
per request. The default stays `true` so that deployments which link content
into the tree keep working; turn it off whenever the directory can contain
symlinks you did not create. A symlinked *root* works either way, since the
root is resolved once at startup.
- `autoindex` reveals file names, sizes and timestamps. Combine with
[`basic_auth`](basic_auth.md) or [`ip_restriction`](ip_restriction.md) for
anything non-public.
- Set `chunk_size` well above 4 KB when serving large media; it directly controls
the syscall rate while streaming.