File store
The file store holds uploaded files under the SHA-256 of their content. A task can reference a stored file by that ID instead of sending its bytes inline, which helps when:
- the input is large:
/executebodies are capped at 1 MiB by default, uploads at 50 MiB; - the same input (test data, a library, a prebuilt archive) is used by many requests;
- the content is binary and you would rather not base64-encode it into JSON.
Upload
Section titled “Upload”curl -s http://localhost:3000/api/v1/file \ -H "Authorization: Bearer $API_KEY" \ -F "file=@tests/large-input.txt"{ "file_id": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "size": 4, "already_exists": false}The body is multipart/form-data with one part named file. Its filename and
content type are kept as metadata. Uploading the same bytes again returns the
same file_id with already_exists: true.
You can compute the ID yourself (sha256sum) and skip the upload when the
file is already there, but a re-upload is cheap and also refreshes the file’s
expiry.
Use in a task
Section titled “Use in a task”{ "cmd": "./main", "files": { "input.txt": { "file_id": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" }, "tools/helper": { "file_id": "…", "executable": true } }}The file is written into the workspace at that path before the task starts,
exactly like an inline file. If any referenced ID is not in the store, the
whole request fails with 404 and code file_not_found before anything runs.
For the step cache, a referenced file and an inline file with the same bytes are the same input.
List, download, delete
Section titled “List, download, delete”| Request | Result |
|---|---|
GET /api/v1/file | { "files": [ { "id", "filename", "content_type", "size", "created_at" } ] } |
GET /api/v1/file/{id} | The bytes, with the stored content type |
DELETE /api/v1/file/{id} | 204, or 404 if absent |
created_at is an RFC 3339 UTC timestamp.
Limits and expiry
Section titled “Limits and expiry”| Limit | Default | Variable | When exceeded |
|---|---|---|---|
| Size of one file | 50 MiB | UPLOAD_FILE_LIMIT_BYTES | 413 payload_too_large |
| Uploads in progress | 4 | MAX_CONCURRENT_UPLOADS | 503 capacity_exhausted |
| Total bytes stored | 512 MiB | FABER_STORE_MAX_TOTAL_BYTES | 503 capacity_exhausted |
| Files stored | 1000 | FABER_STORE_MAX_ENTRIES | 503 capacity_exhausted |
| Lifetime | 1 hour after the last upload or read | FABER_STORE_TTL_SECS (0 disables expiry) | The file is treated as absent and removed by the next sweep |
Expired files are swept every FABER_STORE_TTL_CHECK_SECS (60 s). Using a file
in a task counts as a read, so files in active use stay alive.
Backends
Section titled “Backends”FABER_STORE_BACKEND | Where files live |
|---|---|
memory (default) | In the Faber process. Lost on restart |
filesystem | Under FABER_STORE_PATH (/var/lib/faber/store). Mount a volume there to keep files across restarts |
hybrid | On disk, with a memory cache of up to FABER_STORE_MAX_MEMORY_ENTRIES files and FABER_STORE_MAX_MEMORY_SIZE bytes |
The production image runs with a read-only root filesystem, so the
filesystem and hybrid backends need a writable volume at
FABER_STORE_PATH.
Things to keep in mind
Section titled “Things to keep in mind”- One scope. Every API key holder can list, read and delete every stored file. Do not upload anything a key holder should not see.
- Each instance has its own store. Behind a load balancer, an upload and
the request that references it can reach different instances, and the
second gets
404. Route a client’s requests to one instance, retry the upload onfile_not_found, or send the file inline.
Go deeper
Section titled “Go deeper”Request lifecycle: when referenced files are fetched and checked, and how their bytes reach the sandbox.