# upload and quickcrop

> Send a file you supply to the host, and crop images before they are uploaded.

Source: https://clayjs.com/docs/plugins/upload/

Two plugins for getting files into a page: `upload` sends a file you supply to the host and gives you its URL, and `quickcrop` crops an image before it is uploaded.

## upload

`clay.upload(file)` sends a file you supply (from a file input, a drop or a paste) to the host and resolves with its URL, which you write into the page. It opens no picker itself. The CMS uses it for image fields.

```html
<script src="https://clayjs.com/v1/clay.js?plugins=upload"></script>
```

```js
const result = await clay.upload(file);
// { ok, msg, msgType, code, uploads }
const url = result.ok ? result.uploads[0].url : null;
```

The result never rejects, so one shape covers every ending: the host stored the file, this host cannot store files at all (`code: "unsupported"`), or the request failed. A host that could not be reached for its metadata is a failure, `code: "network"`, never `unsupported`: embed the file instead only on `unsupported`. `clay.upload` is one attempt of one upload and has nothing to cancel or retry. Use a task for that.

### Tasks

`clay.upload.start(file, options)` is the same transport and the same checks as `clay.upload`, plus an identity, so progress, cancellation and an explicit retry belong to one upload rather than to whichever callback fires.

```js
const task = clay.upload.start(file, { requiredAccess: "document-or-local" });
const { unmount } = await clay.upload.mount(task, container);
const unsubscribe = task.subscribe(({ state, percent }) => draw(state, percent));

const result = await task.result;
task.cancel();
await task.retry();
unsubscribe();
unmount();
task.dispose();
```

`start` begins the first attempt before it returns, so `subscribe` calls back at once with the state the task is in. A snapshot is `{ id, attempt, state, loaded, total, percent, result }`, where `id` stays the same for the life of the task, `attempt` increases only on an explicit `retry()`, and `loaded`, `total` and `percent` describe the attempt now running. Four states: `uploading`, `succeeded`, `failed` and `cancelled`.

- `task.result` is the promise of the current attempt and has the same shape as `clay.upload`'s. It never rejects.
- `task.subscribe(fn)` returns the function that stops the snapshots.
- `task.cancel()` ends the attempt at once, even while the host's metadata is still loading, and deletes nothing: the host may already hold the bytes.
- `task.retry()` starts a new attempt of the same file and returns its promise, asking the host for fresh metadata first. It starts one only from `cancelled`, or from `failed` with a code a second try could change (`network`, `timeout`, `error`, `bad-response`, `aborted`); otherwise it returns the current result and sends nothing. So a second click while an attempt runs joins the attempt already going, and a refusal such as `privacy-unsupported`, `forbidden` or `too-large` is never sent again.
- `task.dispose()` cancels and releases the file. A failed or cancelled task keeps its file, so `retry()` does not ask the person to pick it again.

A percent of 100 means the bytes were sent, not that the file is stored: the attempt stays `uploading` and the shared row reads "Finishing upload" until the host answers. Nothing retries automatically, and nothing falls back to a data URL, so a failed or cancelled attempt inserts nothing.

`clay.upload.mount(task, container)` draws the shared row for one task: the file name, a progress bar, Cancel while it runs, Retry where a retry could answer differently, and Dismiss once it has settled. `container` is an element or a selector. It resolves `{ unmount }`, and the row and its stylesheet load on the first call, so a page that never uploads never downloads them. Everything it draws is marked `clay="editor-ui"`, so none of it reaches a saved file or a sync frame.

### Private media

`requiredAccess` is how a caller says it cannot use a publicly readable URL. It takes three names:

| `requiredAccess` | Accepted from the host |
|---|---|
| `"document"` | `document`: a host that binds each returned URL to that document's live read policy |
| `"local"` | `local`: a local host, where the bytes follow its own file-serving rules |
| `"document-or-local"` | either of the two |

The requirement is checked twice: against `document.upload.access` in the host's metadata before any bytes are sent, and against the `access` on every upload the host returns. A host that promises neither, or an answer that drops the access it promised, settles as `{ ok: false, code: "privacy-unsupported" }`, and the global event for that attempt is `clay:upload-error`, never `clay:upload-done`. The uploads stay in that result on purpose, so a person can still copy or download the URL deliberately; a caller that asked for privacy must not insert it on its own. An unknown name is a typo, not a host answer, and `clay.upload` and `clay.upload.start` throw for it before a request exists.

Read `document.upload.access` from the host's `/_/meta` before offering private pictures. A missing value means public, and so does any value you do not recognize. Access is never inferred from the page URL, the plan or the filename.

## Across a mode change

`upload` is a participant of the mode lifecycle, with the id `clay:upload` and order 340. [Switching without a reload](https://clayjs.com/docs/guide/edit-mode/#switching-without-a-reload-clay-mode-switch-live) describes how a participant is prepared, and [Leaving edit](https://clayjs.com/docs/guide/edit-mode/#leaving-edit) how an exit refuses.

A pending upload refuses the exit until it finishes, and the exit flushes it: it waits on every attempt in flight and, while one is still uploading, refuses `pending-input` with `pending: ["upload"]`. A refused exit hands the same lease back, and uploads go on.

Admission is closed from the start of an exit until the commit, and while it is closed a start answers `{ ok: false, code: "aborted" }`. A retry made then sends nothing: the task keeps its ending and its file, and the next entry asks again. With no mode lifecycle running, which is every page that never uses one, admission is open.

## quickcrop

A crop modal for image uploads: pick a file, drag a box, confirm. It resolves a cropped, re-encoded blob and a data URL. The CMS uses it for any image field marked `data-hcms-crop`, so `?plugins=cms` loads it for you; ask for it by name to call it from your own code.

```html
<img class="avatar" data-hcms-crop="1:1" src="avatar.jpg">
```

```js
const shot = await clay.quickcrop(file, { aspect: 16/9 });
// { blob, dataURL, width, height }, or null if they cancelled
```

Aspects are `1:1`, `16:9` or `free` on the attribute, a plain number in code. The modal and its stylesheet are marked `save-remove`, the legacy alias for `no-save`, so cropping chrome never reaches the saved file.

## On Hyperclay

On Hyperclay, uploads go to the host rather than into the file. A file uploaded from inside a document lands in that document's own folder, `uploads/assets-document-<document id>/`, and is linked at `/_/uploads/assets-document-<document id>/<file>`. The folder is named after the document's id, so renaming or moving the document keeps its pictures working. Per-file and total limits depend on the plan: see [Uploads](https://hyperclay.com/docs/uploads/).

That folder also binds the upload to the document. On hosts that advertise `access: "document"` in `document.upload.access`, the bytes of a bound upload follow the document's current read permission on every route that serves them: give a teammate access to the document and its pictures answer to that too; make it private and they stop answering for everyone who is not on its access list. The permission is checked on each request, so no URL has to be rewritten when it changes, and a request without access gets the same 404 as a file that does not exist. Bound files are sent `Cache-Control: private, no-store`, so no shared cache keeps a copy from a time when the document was readable by more people.

Two limits on that. Uploads made before this binding existed stay reachable by URL until the account's protection migration has run, and Hyperclay advertises `document` only after the deployment setting and that account's migration both say yes, so a missing `access` field means those uploads are still public. And a copy someone already downloaded while a URL was public cannot be taken back by any later change.
