assets
POST /assets
Upload bytes. Idempotent on content.
R34, Q14. The body is the raw bytes. The daemon hashes them, writes `<name>.tmp`, fsyncs and renames into the blob store (06 §4), then makes the library hardlink. When the sha256 already exists, nothing is stored and the existing asset is returned with 200. Either way the caller gains read access to it (Q9). `pinned=true` pins; nothing unpins. Images are probed for width and height; video and audio take the hints given (Q13). A body over the upload limit is 413 `too_large` with `details.limit`; a body the disk cannot hold, with the same headroom rule as weight downloads, is 507 `disk_space` (`QuotaExceeded`: out of room, do not retry) before anything is written (M4 first review, #16, second review #9; the limit's value is still open). The check uses the request's Content-Length when it is sent. An asset stays out of reclaim until an item or item input first references it (M4 first review, #3).
Needs the assets capability.
| Go | Upload(ctx context.Context, body io.Reader, contentType string, params *AssetsUploadParams) (*Asset, error) |
|---|---|
| Python | def upload(self, body: bytes, content_type: str, *, kind: Any, filename: Optional[Any] = None, pinned: Optional[Any] = None, width: Optional[Any] = None, height: Optional[Any] = None, duration_s: Optional[Any] = None, fps: Optional[Any] = None) -> Any |
| JavaScript | upload(body, contentType, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| kind | query · required | AssetKind | |
| filename | query | string | Names the library file and the blob's extension. Defaults to the asset id plus an extension from the mime. |
| pinned | query | boolean | A user import. Pinned assets are never reclaimed; nothing unpins. |
| width | query | integer | |
| height | query | integer | |
| duration_s | query | number | |
| fps | query | number |
Request body: string (*/*)
| Status | Body | Means |
|---|---|---|
| 200 | Asset | These bytes were already stored; the existing asset. |
| 201 | Asset | A new asset. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 413 | Error | The body is over its limit. Code `too_large`, with `details.limit` in bytes. |
| 507 | Error | The disk cannot hold the body with headroom; nothing was written. Code `disk_space`, with `details.needed` and `details.free` in bytes. |
GET /assets/{id}
The asset's bytes, with Range.
R37. Readable by the rules in Q9; anything else is 404. `ETag` is the sha256. A single byte range answers 206; an unsatisfiable one 416 (`Invalid`). When the row exists and its file does not, 410 `asset_missing`. Needs the Bearer header; a studio's page loads it through the runtime SDK's same-origin proxy, which adds the header, as `/helm/api/v1/assets/{id}` (M6 Q10).
Needs the assets capability.
| Go | Read(ctx context.Context, id string, params *AssetsReadParams) (*RawResponse, error) |
|---|---|
| Python | def read(self, id: str, *, range: Optional[Any] = None) -> Any |
| JavaScript | read(id, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| Range | header | string |
| Status | Body | Means |
|---|---|---|
| 200 | string | The whole file. |
| 206 | string | The requested range. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 410 | Error | The asset's row exists and its file does not. Code `asset_missing`. |
| 416 | Error | Range not satisfiable. Code `range_not_satisfiable`. |
GET /assets/{id}/lineage
Downstream provenance — every item ever made from this asset, recursively.
02 §8, Q16. Follows `item_inputs` from the asset to the items that used it, then from each of those items' assets onward. Other studios' items are left out unless the caller holds `gallery.read_all`; the walk still passes through them, so a later own item is found. Ordered `created_at` descending. Soft-deleted items are left out of the result, and the walk passes through them too.
Needs the gallery capability.
| Go | Lineage(ctx context.Context, id string, params *AssetsLineageParams) (*ItemPage, error) |
|---|---|
| Python | def lineage(self, id: str, *, limit: Optional[Any] = None, cursor: Optional[Any] = None) -> Any |
| JavaScript | lineage(id, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| limit | query | integer | |
| cursor | query | string | The `next_cursor` of the previous page, unchanged. |
| Status | Body | Means |
|---|---|---|
| 200 | ItemPage | A page of items. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
GET /assets/{id}/thumb
A thumbnail, generated once and cached.
R36, Q13. Images only in M4; video and audio answer 501 `unsupported` until the ffmpeg decision. The only width is 320 (02 §5's `thumb-320`). Needs the Bearer header; a studio's page loads it through the runtime SDK's same-origin proxy, as `/helm/api/v1/assets/{id}/thumb` (M6 Q10).
Needs the assets capability.
| Go | Thumb(ctx context.Context, id string, params *AssetsThumbParams) (*RawResponse, error) |
|---|---|
| Python | def thumb(self, id: str, *, w: Optional[Any] = None) -> Any |
| JavaScript | thumb(id, params = {}) |
| Parameter | In | Type | About |
|---|---|---|---|
| id | path · required | ULID | |
| w | query | integer (320) |
| Status | Body | Means |
|---|---|---|
| 200 | string | The thumbnail. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 410 | Error | The asset's row exists and its file does not. Code `asset_missing`. |
| 501 | Error | This provider cannot serve this. Code `unsupported`, with a reason in `message`. |
POST /assets:adopt
Adopt a file from the stage or data directory by hardlink — no copy, no HTTP body.
R34, 07 §4, Q10 as amended by the M4 first review (#4), Q11. **Where the file may be.** `path` must resolve, with no symlink in it, inside the caller's stage directory (`HELM_STAGE_DIR`, `paths.stage`) or its data directory (`{data}`, `paths.data`). Anywhere else is 422 `outside_roots`. It must be a regular file (422 `not_a_file`). **What happens to it.** The file is hardlinked into the blob store and made read-only (0444). - From stage, the stage entry is then unlinked. - From data, the studio's file stays where it is. It is the blob's inode, so it is read-only too. **When the bytes already exist.** Nothing is stored and the existing asset returns with 200 (02 §7). A stage entry is unlinked; a data file is left alone. **Adoption never copies.** A blob hardlink that fails across volumes is 422 `cross_device`, and the file is left in place. A library hardlink that fails across volumes still adopts, with `library_path` null and a warning. An asset stays out of reclaim until an item or item input first references it (M4 first review, #3).
Needs the assets capability.
| Go | Adopt(ctx context.Context, body AdoptRequest) (*Asset, error) |
|---|---|
| Python | def adopt(self, body: Dict[str, Any]) -> Any |
| JavaScript | adopt(body) |
Request body: AdoptRequest (application/json)
| Status | Body | Means |
|---|---|---|
| 200 | Asset | These bytes were already stored; the existing asset. |
| 201 | Asset | A new asset. |
| 400 | Error | The request is malformed. Codes: `bad_request`, `bad_filter`, `bad_cursor`, `invalid_document`. |
| 401 | Error | No studio token, or a revoked or unknown one. Code `unauthenticated`. |
| 403 | Error | The token lacks a capability (`capability_required`, with `details.capability`), the job is not a task the studio may change (`not_a_task`), or the request came from another origin (`bad_origin`). |
| 404 | Error | Absent, deleted, or not the caller's to see. Code `not_found`. |
| 422 | Error | Well-formed but refused. Codes: `outside_roots`, `cross_device`, `not_a_file`, `unknown_session`, `self_handoff`, `not_launchable`, `blocked`, `not_linkable`. |