Files

177 lines
9.3 KiB
Markdown

# Fileserver configuration
Every key below lives under `app.fileserver-platform` (environment form
`APP_FILESERVER_PLATFORM_*`). That namespace is the HTTP platform's alone: `app.fileserver.*`
belongs to the R2 tabular publication capability and `app.file-export.*` to the R1 CSV export, and
the three are deliberately separate so switching one on cannot switch on another.
While `app.fileserver-platform.enabled` is false none of these keys is bound at all — the
auto-configuration that binds them is not processed — so a malformed value in a block nobody
enabled cannot fail a startup. Once enabled, binding is strict: an unknown key under the prefix is
refused rather than ignored. The defaults are the conservative ones: the
capability is off, the admin plane is off, background reclamation is off, and there is no permissive
authorization fallback. Turning the capability on is a deliberate act, and so is every surface it
exposes.
## Minimum to start
```yaml
ca-skeleton:
fileserver:
enabled: true
instance-id: ${HOSTNAME} # writer-lease owner; must be unique per node
storage:
root: /var/lib/backend/files # absolute, outside any webroot or config dir
security:
access-policy: role-based # or supply your own FileAccessPolicy bean
observability:
fingerprint-key: ${FILESERVER_FINGERPRINT_KEY}
```
Startup fails, rather than degrading, when any of these is missing or unsafe:
| Condition | Why it is fatal |
| --- | --- |
| `security.access-policy` left at `required` with no `FileAccessPolicy` bean | a file capability that authorizes by default is worse than one that refuses to start |
| `observability.fingerprint-key` unset while metrics are on | an unkeyed digest of an enumerable identifier is reversible |
| the storage root fails a mandatory capability probe | a volume that cannot create atomically, keep staging and content on one FileStore, or refuse symlinks is unsafe, not degraded |
| `storage.publish-mode: atomic-move-required` on a volume where the probe could not prove an atomic move | the configured guarantee cannot be delivered |
| `security.access-policy: unenforced` under a `prod` profile | a value that was convenient in development must not survive promotion |
## Authorization — `security`
| Key | Default | Meaning |
| --- | --- | --- |
| `access-policy` | `required` | `required` (supply your own bean), `role-based`, or `unenforced` |
| `read-roles` | `ROLE_FILE_READ` | grants metadata read and download |
| `write-roles` | `ROLE_FILE_WRITE` | grants create, append, finalize, delete, copy, move |
| `admin-roles` | `ROLE_FILE_ADMIN` | grants reverify and force-delete, and gates `/internal/fileserver/**` at the servlet chain |
There is no anonymous-read switch. Every Fileserver route is authenticated by the servlet chain
before any application policy is consulted, so such a setting could only ever have described a
permission the transport had already refused — a configuration that reads as if it grants access
and does not.
The three tiers do not inherit. An admin role cannot delete through the data plane, and a write role
cannot reach the management plane — a role model where "can delete" implied "can force-delete" would
make the audited plane reachable through the unaudited one.
`unenforced` authorizes everything and exists so a developer can exercise upload and download before
deciding on a role model. It is refused under a production profile.
## Storage — `storage`
| Key | Default | Meaning |
| --- | --- | --- |
| `root` | `/var/lib/backend/files` | absolute path; the only place a path exists |
| `publish-mode` | `atomic-move-preferred` | `atomic-move-required`, `atomic-move-preferred`, `metadata-pointer` |
| `buffer-size` | `128KB` | bounds every transfer allocation; memory never scales with file size |
| `forbidden-root-ancestors` | `/app,/etc,/usr/share/nginx/html` | roots the storage root must not live under (webroot, config dirs) |
`root` must be absolute. A relative root resolves against the process working directory, which is
one path in a container and another in a test, so it is refused at binding time.
Three former keys are gone, pinned as constants instead: staging and content share one FileStore,
symbolic links are never followed, and the object and its directory are synced before READY. Each
is an invariant the atomic publish and the namespace boundary are built on — a deployment that
could switch one off would be running a different capability under the same name and the same
tests.
The storage provider has no selector either. There is exactly one implementation, and a `type` key
with one legal value is a promise of pluggability that nothing keeps.
## Upload, download, transfer
| Key | Default | Meaning |
| --- | --- | --- |
| `upload.max-file-size` | `100MB` | hard ceiling; also drives `spring.servlet.multipart.max-file-size` |
| `upload.max-request-size` | `110MB` | request envelope; must be at least `max-file-size` |
| `upload.initial-reservation` | `8MB` | quota reserved when the length is unknown |
| `upload.ttl` | `1h` | how long a resumable upload stays claimable |
| `upload.reservation-ttl` | `24h` | how long an unsettled quota reservation survives |
| `upload.lease-duration` | `30s` | writer lease; renewed at one third of this |
| `upload.max-parts` | `16` | multipart part ceiling |
| `upload.require-content-length` | `false` | refuse chunked raw uploads |
| `download.cache-control` | `private, no-store` | emitted on every content response |
| `download.inline-allowed` | `false` | scriptable content is always an attachment regardless |
| `download.max-ranges` | `1` | multi-range responses are opt-in |
| `download.max-range-bytes` | `100MB` | total bytes one ranged response may cover |
| `download.zero-copy-enabled` | `true` | hand large plaintext responses to the kernel |
| `download.zero-copy-minimum-bytes` | `16MB` | below this the syscall setup costs more than it saves |
| `transfer.core-size` / `max-size` / `queue-capacity` | `8` / `32` / `64` | blocking transfer pool bounds |
| `transfer.await-seconds` | `300` | how long a transfer may occupy a pool thread |
Zero copy changes no header and no status. When storage declines it — an unreadable region, an
unsupported backend — the response is streamed instead and is byte-identical.
## Verification — `verification`
| Key | Default | Meaning |
| --- | --- | --- |
| `timeout` | `5s` | per-verifier ceiling |
| `require-media-type-verdict` | `false` | refuse a file whose type could not be determined |
| `inline-safe-profile` | `false` | accept scriptable content instead of quarantining it |
Set `inline-safe-profile: true` only when downloads are never served inline from a trusted origin.
## Quota and admission — `quota`
| Key | Default | Meaning |
| --- | --- | --- |
| `instance-upload-permits` | `16` | concurrent uploads this node admits |
| `scope-upload-permits` | `4` | concurrent uploads one namespace admits |
| `direct-download-permits` | `64` | concurrent non-delegated downloads |
| `soft-high-water` | `0.70` | storage fraction at which pressure is reported |
| `hard-high-water` | `0.85` | storage fraction at which uploads are refused |
When the storage fraction cannot be read, admission treats it as unknown and does not apply the
high-water rule — a synthetic `0` would silently disable the guard, and a synthetic `1` would take
the capability down over a failed syscall.
## Background reclamation — `cleanup`
| Key | Default | Meaning |
| --- | --- | --- |
| `enabled` | `false` | run the cleanup worker on this node |
| `interval` | `60s` | fixed delay between batches, not fixed rate |
| `max-items` | `100` | items one batch may claim |
| `max-bytes` | `1GB` | bytes one batch may reclaim |
| `retry-backoff` | `5m` | how long a failed item waits before it is due again |
The worker deletes physical objects, so it is off until a deployment decides otherwise. A node
without it still queues cleanup items; another node or an operator reclaims them. An item that fails
eight times is abandoned rather than retried forever — it stays visible to an operator, parked
rather than discarded.
## Management plane — `admin`
| Key | Default | Meaning |
| --- | --- | --- |
| `enabled` | `false` | expose `/internal/fileserver/**` |
| `orphan-minimum-age` | `1h` | how long an unreferenced object must exist before a scan may name it |
Publishing content and committing its record are two steps. Anything younger than
`orphan-minimum-age` is assumed to be mid-commit rather than abandoned; shortening this makes
concurrent uploads look like orphans.
## Front-proxy delegation — `nginx`
| Key | Default | Meaning |
| --- | --- | --- |
| `enabled` | `false` | emit `X-Accel-Redirect` instead of a body |
| `internal-prefix` | `/__files/` | must be an `internal` location resolving to the content root |
| `object-suffix` | `.bin` | layout suffix the proxy appends |
| `minimum-size` | `16MB` | below this the application serves the transfer itself |
Delegation is decided only after authorization and the READY gate, so an internal redirect can only
ever name content the caller was already allowed to read.
## Protocols — `tus`
| Key | Default | Meaning |
| --- | --- | --- |
| `enabled` | `false` | expose the tus 1.0 endpoints |
The HTTPbis resumable-upload draft-12 surface is experimental and documented in
[support-matrix.md](support-matrix.md).