# Upload flow

A PUT to the bucket is not enough. file.rocks lists files from its **index** (shadow file), not from a live S3 listing. After PUT, you must register the object.

## MCP (use this in agents)

Do **not** call REST `POST .../files/upload-complete` from an agent HTTP client. Cloudflare Bot Fight Mode may return error **1010**. MCP `complete_upload` runs on the file.rocks server and does not hit that path.

1. `presign_upload` — scope `files:upload`. Returns a short-lived PUT URL.
2. **PUT** the bytes to that URL (the storage bucket, not file.rocks).
3. `complete_upload` — same scope `files:upload`, same `key`. Registers the object. After this, `list_files` returns it.

The **Upload + read** API key preset already includes `files:upload`. Do **not** request `files:write` to register a file.

If the object is already in the bucket but missing from `list_files`, skip steps 1–2 and call `complete_upload` with the object key.

`presign_upload` returns a `next` object that names `complete_upload`. Follow it.

## Display and download

After `complete_upload` (and on `list_files` / `get_file`), each file includes:

- `url` — public CDN URL when the bucket has `publicDomain`. **This does not expire.** Use it to **display** images, embed in markdown, or pass to another tool. The browser treats it as a view (inline), not a file download.
- `thumbnailUrl` — public URL of the thumbnail when one exists.

Do not invent a URL from the key. Read `url` from the file object.

### View vs download

These are different:

| Goal | Tool / field | Browser behavior |
|------|----------------|------------------|
| Display / embed (image, markdown, iframe) | `file.url` if set, else `presign_view` | Inline — shows the file |
| Save as a file (attachment) | `presign_download` | Attachment — downloads the file |

`presign_view` and `presign_download` both require `files:download`.

Public CDN URLs **cannot** force a download. Even when `file.url` exists, call `presign_download` for an attachment. That signed URL expires.

### Lifetime when there is no public domain (or for attachments)

S3-compatible **presigns cannot be non-expiring** (SigV4 max is 7 days).

| Intent | `purpose` | Lifetime |
|--------|------------|----------|
| Show a person now | `preview` (default) | 1 hour |
| Place in another tool / page | `embed` | 7 days (max) |

Returns `url`, `as`, `disposition`, `expiresAt`, `durable`.

Optional `expiresIn` (seconds, 60–604800) overrides the purpose default.

A true non-expiring **view** URL requires `publicDomain` on the bucket (`file.url`). There is no non-expiring attachment URL. Do not tell the user that a 7-day presign is permanent.

## Tags and description (optional)

`update_file` sets tags/description. It requires `files:write` and only works on files that are already in the index. It does **not** register an upload.

Upload + read keys cannot tag. Leave tags off, or use a key that includes `files:write`.

## REST (scripts only)

Prefer MCP in agents. For curl/scripts:

1. `POST .../files/presign-upload` · `files:upload`
2. PUT to the returned URL
3. `POST .../files/upload-complete` · `files:upload` with `{ key, size, etag?, contentType?, contentHash? }`

Details: [Files](/docs/api/files).

## Common mistakes

| Mistake | What to do |
|---------|------------|
| PUT succeeded, `list_files` is empty | Call `complete_upload`. The index is not a live bucket listing. |
| Ask the user for `files:write` to register | Use `complete_upload` (`files:upload`). Upload + read is enough. |
| Call REST `upload-complete` from an agent | Use MCP `complete_upload`. REST may be blocked (Cloudflare 1010). |
| Call `update_file` to create the row | `update_file` only patches an indexed file. Register first. |
| Expect tags on Upload + read | Tags need `files:write`. Registration does not. |
| Invent a URL from the key | Use `file.url` to display. If it is null, call `presign_view`. For a save-as-file link, call `presign_download`. |
| Use `presign_download` to show an image | That is an attachment. Use `file.url` or `presign_view` to display. |
| Treat an embed presign as permanent | It lasts at most 7 days. Non-expiring **view** URLs need `publicDomain`. |

## Scopes

| Action | Scope | Tool / route |
|--------|-------|----------------|
| Presign + register | `files:upload` | `presign_upload`, `complete_upload` |
| List after register | `files:read` | `list_files` (includes `url` when public) |
| Show / embed without public CDN | `files:download` | `presign_view` (`preview` or `embed`) |
| Save as attachment | `files:download` | `presign_download` (`preview` or `embed`) |
| Tags / description | `files:write` | `update_file` |
