curl -s -X POST $SUBSTRIO_URL/v1/_files -H "Authorization: Bearer $SUBSTRIO_KEY" \
-H 'content-type: application/json' \
-d '{"path":"docs/report.pdf","data":"SGVsbG8=","encoding":"base64"}'
# {"path":"docs/report.pdf","size":5,"content_type":"application/pdf","cache_control":"","public":false,"hash":"...","created_at":1750000000,"updated_at":1750000000}
A path is 1 to 65535 bytes of valid UTF-8, with no leading or trailing /,
and no empty, . or .. segment; there is no folder to create first.
Uploading
| Operation | Route | Scope |
|---|---|---|
file.upload |
POST /v1/_files |
curator |
Send the file two ways: as JSON (path, data, and the fields below), or as
raw bytes with the path in an X-Filename header and the type in
Content-Type.
| Field | Type | Default | Description |
|---|---|---|---|
path |
string | required | Where to store the file |
data |
string | required | The file's bytes, as text or base64 |
encoding |
string | text |
text or base64 |
content_type |
string | guessed from path |
The stored MIME type |
cache_control |
string | none | The stored Cache-Control value |
public |
boolean | false |
Serve without authentication |
curl -s -X POST $SUBSTRIO_URL/v1/_files -H "Authorization: Bearer $SUBSTRIO_KEY" \
-H "X-Filename: assets/logo.png" -H "Content-Type: image/png" \
--data-binary @./logo.png
Both calls answer the file's metadata: {"path", "size", "content_type", "cache_control", "public", "hash", "created_at", "updated_at"} (hash is
its content hash, created_at/updated_at are Unix seconds). A second
upload to the same path replaces it.
Listing
| Operation | Route | Scope |
|---|---|---|
file.list |
GET /v1/_files |
read-only |
curl -s "$SUBSTRIO_URL/v1/_files?prefix=assets/&limit=50" -H "Authorization: Bearer $SUBSTRIO_KEY"
# {"files":[{"path":"assets/logo.png","size":2048, ...}],"next_cursor":null,"count":1}
prefix narrows the listing to paths starting with it; cursor (from a
prior page's next_cursor) continues a listing; limit caps the page size.
Downloading and deleting
| Operation | Route | Scope |
|---|---|---|
file.download |
GET /v1/_files/{path} |
read-only |
file.delete |
DELETE /v1/_files/{path} |
operator |
curl -s $SUBSTRIO_URL/v1/_files/docs/report.pdf -H "Authorization: Bearer $SUBSTRIO_KEY" -o report.pdf
curl -s -X DELETE $SUBSTRIO_URL/v1/_files/docs/report.pdf -H "Authorization: Bearer $SUBSTRIO_KEY"
# {"deleted":true,"path":"docs/report.pdf"}
file.download answers the file's bytes with its stored content type.
Both take the path as the route's tail; an unknown path answers 404
FILE_NOT_FOUND.
Calling files from a function
A function reaches the same store directly through substrio.files, with
no HTTP, JSON or base64 between the call and the stored bytes:
export default async function (req) {
const meta = substrio.files.put("reports/" + req.params.id + ".json", JSON.stringify(req.body));
const stat = substrio.files.stat(meta.path);
const bytes = substrio.files.get(meta.path);
return substrio.files.response(meta.path) ?? new Response("not found", { status: 404 });
}
put(path, data, options?) stores data (an ArrayBuffer, typed array,
string or Blob), replacing the path if it exists, and returns its
metadata. stat(path) and get(path) return null for a path that does
not exist, rather than throwing. remove(path) deletes a file and returns
whether it existed. response(path, init?) returns the file as a Response
streamed from storage, honoring an init.range byte range; slice(path, offset, length, init?) returns one byte range of it as a whole response, with
no Content-Range header.
Size limits and errors
A raw upload (X-Filename plus a body of bytes) is limited only by the
app's storage quota. A JSON upload ({"path", "data"}) counts against the
request body limit, 16 MiB; base64
data is checked against the same limit after decoding. Either form past
the limit answers 413.
An invalid path (too long, leading or trailing /, an empty, . or ..
segment, invalid UTF-8, or a control byte) answers 400 INVALID_ARGUMENT
(or KEY_TOO_LARGE past 65535 bytes), naming the field that carried it
(path or X-Filename). A storage quota refusal on upload answers with
the quota's own error.
Files versus a site release
Files here are stored and replaced one path at a time, with no history. A static site is released as a whole, versioned, content-addressed deploy instead: see operations/static-sites.md.