Storage backend
A storage backend is a plain object satisfying the exported StorageBackend type, passed as the
storage option. DirectoryStorage is one; your own is an object literal, or one half of
a reference backend with the other half written yourself.
type StorageBackend = { metadata: MetadataSource; content: ContentSource };
type MetadataSource = ( entity: Entity, context: RequestContext,) => MetadataDelivery | null | Promise<MetadataDelivery | null>;
type ContentSource = ( file: File, entity: Entity, context: ContentContext,) => ContentDelivery | null | Promise<ContentDelivery | null>;
type ContentContext = RequestContext & { contentDisposition: string };The contract has no lifecycle methods: you own any client and its connections, and pass them in your backend’s
options. A backend that needs more than the entity, such as its parent’s meta, can take prisma that way.
arocapi loads the Record, checks visibility and then access (ADR 0002),
and only then calls the matching half, once per request. HEAD and GET make the same call, so their headers can’t
disagree.
metadata(entity, context)receives theEntityrow, includingmeta.content(file, entity, context)receives theFilerow and its file entity, both includingmeta, so a location stored at index time needs no database lookup.context.contentDispositionis theContent-Dispositionarocapi computed from?dispositionand?filename.
context is the request context, { request, fastify }.
Deliveries
Section titled “Deliveries”type |
Fields | Response |
|---|---|---|
stream |
open, contentLength, etag?, lastModified?, and for Content contentType |
200 with the stream open() returns. open is called only on GET, never on HEAD. |
path |
path, contentLength, etag?, lastModified?, and for Content contentType |
200 with the file at the absolute path, which arocapi opens on GET. |
redirect |
url |
302 to url for GET and HEAD. /file/:id?noRedirect=true answers 200 { "location": url } instead. |
json |
value |
Metadata only: 200 with value serialised once, and the Content-Length and a strong ETag from those bytes. |
stream and path deliveries set Content-Type, Content-Length, ETag and Last-Modified, and on /file/:id
Content-Disposition. Take them from storage, not the database, so a stale File.size can’t produce a wrong header.
Metadata is always sent as application/ld+json, so its deliveries carry no contentType. A content half returning
json is a type error.
arocapi can’t add its protective headers to a redirect target, so the
target has to protect itself; see protecting the target.
Errors
Section titled “Errors”nullis a 404NOT_FOUND. Turn your storage’s own “missing” errors intonull.- Anything thrown is a 500
INTERNAL_ERROR, logged, with nothing from the error in the response.
Example: a signed redirect
Section titled “Example: a signed redirect”path deliveries are sent whole until arocapi supports Range. For seekable Content from disk or OCFL today, a
content half can redirect to a content route of your own, which checks a short-lived signed token and serves the
file with range support. The token stands in for the access check arocapi already made, so scope it to one file and
keep the expiry short.
import type { ContentSource } from 'arocapi';
// Your token signer, e.g. a JWT library's sign()declare function sign(payload: { fileId: string; disposition: string }, options: { expiresIn: number }): string;
// Redirect to your own content route, which verifies the token and serves the file with Range supportexport const content: ContentSource = (file, _entity, { contentDisposition }) => ({ type: 'redirect', url: `/content/${sign({ fileId: file.id, disposition: contentDisposition }, { expiresIn: 60 })}`,});Mix it with another backend’s Metadata half: { metadata: DirectoryStorage({ root }).metadata, content }.