Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.3 KiB
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
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.