III The API

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:context names the storage the octets directives under this node work with.
  • octets:put stores the request body. ~ (null) takes the defaults.
  • octets:get serves a stored file; octets:delete deletes 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:

OptionMeaningDefault
acceptA media type or a list of them the route takes.any
limitThe largest body.64MiB
locationWhere to store, relative to the request path.the request path
trustOrigins the route may download from, see below.none
workflowOperations 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:

SituationResponse
The detected type is not acceptable415 Unsupported Media Type
The detected type contradicts content-type400 Bad Request
The content cannot be identified and content-type is acceptablestored as content-type
No content-type, and the content is identified and acceptablestored 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.

SituationResponse
The URL is not trusted, or is not a URL403 Location is not trusted
The request has a body400 Content-Length must be 0 when Content-Location is used
The URL cannot be fetched404 Location is not available
The content exceeds limit413

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 etag is the checksum of the content. A request with a matching if-none-match is answered 304.
  • A range request is answered 206 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

DirectiveValueDoes
octets:contextstorage nameSelects 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:workflowa workflowRuns a workflow on the file.
Search
Search the documentation