Upload and Download Files
A member of staff uploads a product picture; the shop’s pages show it. A route can move a file
between a client and a storage directly: the body goes into the storage as it
arrives, or out of it to the client, and no operation handles the bytes. This is declared with
the octets directives.
§Upload and download
The storage is declared in the Context, beside the tmp that toa create wrote:
# context.toa.yaml
storages:
tmp:
provider: tmp
directory: store
pictures:
provider: tmp
directory: store-pictures Three declarations make a working file resource. pictures is a component of its own, and
this manifest is all it needs:
# components/pictures/manifest.toa.yaml
name: pictures
storages: [pictures]
exposition:
/:
octets:context: pictures
io:output: true
POST:
octets:put: ~
/*:
GET:
octets:get: ~
DELETE:
octets:delete: ~ octets:contextnames the storage theoctetsdirectives under this node work with.octets:putstores the request body.~(null) takes the defaults.octets:getserves a stored file;octets:deletedeletes one. The/*route stands for the file’s identifier.
Access rules are omitted here; a file route needs one like any other route, see Access. For a first run, anonymous: true on / opens the routes to
everyone.
Uploading sends the bytes as the body:
POST /pictures/ HTTP/1.1
content-type: image/jpeg
accept: application/yaml
<bytes> HTTP/1.1 201 Created
content-type: application/yaml
id: eecd837c9f2b4a1d8e7c6b5a4f3e2d1c
type: image/jpeg
size: 48211
checksum: 10cf16b458f759e0d617f2f3d83599ff
created: 2026-03-01T10:12:44.071Z The reply is the file’s entry. Its id is what the file is fetched
by:
GET /pictures/eecd837c9f2b4a1d8e7c6b5a4f3e2d1c HTTP/1.1 HTTP/1.1 200 OK
content-type: image/jpeg
content-length: 48211
etag: "10cf16b458f759e0d617f2f3d83599ff"
<bytes> The path of a file has no trailing slash. Collections end with a slash, files do not: POST /pictures/ uploads into the collection, GET /pictures/eecd…2d1c reads one
file. A file path with a slash is answered 404 with Trailing slash is redundant.
With curl, once the application is restarted:
$ npm run restart
$ curl -i http://localhost:8000/pictures/
-H 'content-type: image/jpeg' -H 'accept: application/yaml'
--data-binary @kettle.jpg
$ curl -o kettle.jpg http://localhost:8000/pictures/eecd837c9f2b4a1d8e7c6b5a4f3e2d1c §Where a file is stored
A file is stored in the storage under the path of the request it was uploaded with. Uploaded to /pictures/products/kettle/, it is at /pictures/products/kettle/<id> in the
storage, and is read back at the same URL plus its identifier.
Route variables therefore organize a storage:
exposition:
/products/:product:
octets:context: pictures
io:output: true
POST:
octets:put: ~
/*:
GET:
octets:get: ~ POST /pictures/products/kettle/ and GET /pictures/products/kettle/<id>.
§Storing elsewhere
location stores the file under another path. It is resolved against the request path like a
relative URL:
POST:
octets:put:
location: ./originals # /pictures/products/kettle/originals/<id> POST:
octets:put:
location: /pictures/ # /pictures/<id>, whatever the request path An absolute location lets several routes, of several components, upload into one shared directory that a single route serves.
§Uploads
octets:put takes these options, all optional:
| Option | Meaning | Default |
|---|---|---|
accept | A media type or a list of them the route takes. | any |
limit | The largest body. | 64MiB |
location | Where to store, relative to the request path. | the request path |
trust | Origins the route may download from, see below. | none |
workflow | Operations to call once the file is stored, see Process an Uploaded File. | none |
§Media types
POST:
octets:put:
accept: image/* POST:
octets:put:
accept:
- image/jpeg
- image/png
- video/* The type of an upload is detected from its content, for the types a storage knows. The request’s content-type is the uploader’s
claim:
| Situation | Response |
|---|---|
| The detected type is not acceptable | 415 Unsupported Media Type |
The detected type contradicts content-type | 400 Bad Request |
The content cannot be identified and content-type is acceptable | stored as content-type |
No content-type, and the content is identified and acceptable | stored as detected |
So a client may omit content-type for the image types a storage detects, and has to state it
for anything else — an SVG, a PDF, a video.
§Size limit
POST:
octets:put:
limit: 10MiB A larger body is cut off and refused:
HTTP/1.1 413 Request Entity Too Large
Size limit is 10MiB The value is a number of bytes or a number with a unit. Decimal and binary prefixes are both
understood: 1KB is 1000 bytes, 1KiB is 1024. A unit written loosely, such as 1kb or 1mb, is read as binary: 1kb is 1024 bytes.
§Naming and describing an upload
Two request headers let the client say more about the file.
content-id sets the entry’s identifier instead of a generated one. It must match ^[a-zA-Z0-9-_]{1,16}$; anything else is 400 with Invalid Content-ID.
POST /pictures/products/kettle/ HTTP/1.1
content-type: image/jpeg
content-id: front The file is then at /pictures/products/kettle/front.
content-attributes sets the entry’s initial attributes: comma-separated key=value pairs, in
one header or several. A key without a value gets the string true.
POST /pictures/products/kettle/ HTTP/1.1
content-type: image/jpeg
content-attributes: primary, angle=front
content-attributes: shot=2026-03 attributes:
primary: 'true'
angle: front
shot: 2026-03 If the entry already exists, content-attributes is ignored.
§Uploading by URL
Instead of sending bytes, a client can name a URL for the gateway to download. The route lists the origins it trusts:
POST:
octets:put:
accept: image/*
trust:
- https://images.supplier.example
- ^https://cdn.supplier.example/[a-z0-9/]+.jpe?g$ An item of trust is an origin, or a regular expression matched against the whole URL.
POST /pictures/products/kettle/ HTTP/1.1
content-location: https://images.supplier.example/kettle/front.jpg
content-length: 0 The reply is the entry, as for an ordinary upload. The URL is kept in the entry as origin.
| Situation | Response |
|---|---|
| The URL is not trusted, or is not a URL | 403 Location is not trusted |
| The request has a body | 400 Content-Length must be 0 when Content-Location is used |
| The URL cannot be fetched | 404 Location is not available |
The content exceeds limit | 413 |
Without trust a route downloads nothing.
§Downloads
octets:get streams the file at the request path with its content-type and content-length. The request’s accept is not consulted: a file is served as what it is.
- The
etagis the checksum of the content. A request with a matchingif-none-matchis answered304. - A
rangerequest is answered206 Partial Content, where the storage supports ranges. - A path with a query string is
404. - A variant is requested by extension, where the storage’s provider has them:
GET /pictures/products/kettle/front.200x200.webp. See Cloudinary.
Add cache:control to file routes. A stored file never changes under its
identifier unless the application replaces it, so a long max-age is usually right.
§Reading the entry
A client can ask for a file’s entry instead of its content. The route has to allow it:
/*:
GET:
octets:get:
meta: true and the client asks with a media type of the toa vendor tree, in the format it wants:
GET /pictures/products/kettle/front HTTP/1.1
accept: application/vnd.toa.octets.entry+yaml HTTP/1.1 200 OK
content-type: application/yaml
id: front
type: image/jpeg
size: 48211
checksum: 10cf16b458f759e0d617f2f3d83599ff
attributes:
primary: 'true'
angle: front Without meta: true such a request is 403 with Metadata is not accessible.
§Headers only
octets:head answers a HEAD request with the headers a GET would carry and no body:
/*:
HEAD:
octets:head: ~ §Deleting
octets:delete deletes the entry at the request path and answers 204:
/*:
DELETE:
octets:delete: ~ It may run a workflow first, see Process an Uploaded File.
§Doing something with a stored file
Storing a file is usually the start of something: the picture is attached to a product, checked,
resized. octets:put, octets:delete and octets:workflow take a workflow: a list of
operations called with a reference to the file. See Process an Uploaded File.
§What clients are told
A route with octets:put says what an upload takes in the answer to OPTIONS:
POST:
octets:
accept: image/jpeg,image/png
limit: 10MiB
stream: true accept is absent where any type may be sent. stream: true says a workflow runs and the reply
arrives as parts.
A browser on another origin may send both headers, see CORS.
§Summary
| Directive | Value | Does |
|---|---|---|
octets:context | storage name | Selects the storage for the node and everything under it. |
octets:put | ~ or { accept, limit, location, trust, workflow } | Stores the body; answers the entry. |
octets:get | ~ or { meta } | Serves the file, or its entry. |
octets:head | ~ | Serves the file’s headers. |
octets:delete | ~ or { workflow } | Deletes the entry, after the workflow. |
octets:workflow | a workflow | Runs a workflow on the file. |