File Storage
NukeBase stores files on disk alongside the JSON tree, addressed by the same path arrays and governed by the same rules.js. A post's record lives at ["posts","p1"]; its attachments live at ["posts","p1","attachments","hero.png"], and both sets of rules sit in one block.
Why files don't go in the JSON tree: the data tree is held entirely in memory and re-serialized on every flush. A 20 MB image stored as base64 would be re-encoded on every write to its subtree. Files live outside the tree, are streamed rather than buffered, and never participate in a flush.
This page covers how the store is addressed and reached. The rest of the file API is split across six more:
| Page | Covers |
|---|---|
| File Client API | Uploading and downloading from the browser — setFile, getFile, listFiles, removeFile, progress and cancellation |
| File Server API | The async server-side operations, plus fileStat and fileUrlFor |
| File Rules | fileRead, fileWrite and fileValidate — who can reach what, and what an upload is allowed to be |
| File Triggers | addFileTrigger and its four points: reserve, release, write, remove |
| File Quotas | A per-user storage limit built from triggers and increment |
| File Limits & Safety | Path resolution, atomic uploads, and the size and rate caps |
Paths and the file store root
Files live under server/files/ (override with the FILES_DIR environment variable). The directory is created at startup. Nothing is registered per directory — rules alone decide what is reachable, and file rules default-deny, so a path with no fileRead/fileWrite at any level simply does not exist as far as the server is concerned.
Path segment rules (stricter than data paths, because these become real filenames):
- Non-empty strings ≤ 256 characters, or non-negative integers ≤ 1,000,000 (a filesystem has no array/object distinction — integers are simply directory or file names)
- Maximum 64 segments; the joined relative path must be ≤ 200 characters (on Windows, the resolved absolute path must also be ≤ 250 characters)
- May not contain
/\:*?"<>|or any control character - May not be
"."or"..", and may not end with a dot or a space (Windows silently strips those, which would make two distinct rule paths collide on one file) - May not be a Windows reserved device name —
CON,PRN,AUX,NUL,COM1–COM9,LPT1–LPT9, with or without an extension - May not start with
$— reserved for engine markers and in-flight uploads - May not be a prototype-pollution key (
__proto__,constructor,prototype, …)
Case collisions are refused, not silently merged. On Windows and macOS Pic.png and pic.png are one file but two distinct rule paths — so a write through the permissive spelling could land on the file governed by the stricter one. Creating a name that differs from an existing entry only by case fails with 409 Conflict. Overwriting with the exact same name is unaffected.
HTTP routes
Every file operation is reachable over HTTP under /_file/. Path segments are URL-encoded, and the encoding round-trips exactly — / and \ are illegal inside a segment, so splitting the URL back on / can only ever rebuild the array that was sent.
| Method | URL | Does |
|---|---|---|
GET | /_file/posts/p1/hero.png | Download the file (streamed with backpressure) |
GET | /_file/posts/p1?list=1 | List one directory level as JSON |
HEAD | /_file/posts/p1/hero.png | Headers only, no body |
PUT / POST | /_file/posts/p1/hero.png | Upload (raw request body is the file) |
DELETE | /_file/posts/p1/hero.png | Delete; add ?recursive=1 to delete a directory |
Download response headers:
Content-Type— resolved from the file's leading bytes first, then its extension. The upload checks resolve it the same way, so a rule never enforces one type while the download serves another.X-Content-Type-Options: nosniff— on every response.Content-Disposition—inlinenormally, so images and PDFs behave as expected. Forced toattachmentfor types a browser would execute in your origin:text/html,application/xhtml,image/svg,application/xml,text/xml.Cache-Control: private, no-cache— contents can change and rules can be revoked, so a shared cache must never hold these.
Denied and missing both answer 404. This is deliberate: a distinguishable 403 would let anyone map which paths exist behind a rule that refuses them. The same applies to directory listings.
Status codes
| Code | Meaning |
|---|---|
400 | Malformed path — bad percent-encoding, illegal segment, too long, too deep. Also a directory delete without ?recursive=1. |
403 | Upload refused by fileWrite or fileValidate |
404 | Not found or not permitted (download, listing, delete) |
409 | On upload: a file differing only by case already exists at this path, or the path is an existing directory |
413 | Body exceeds FILE_MAX_BYTES |
429 | Upload/delete rate limit exceeded for this IP |
500 | Disk error, or a reserve file trigger threw |