# Upload Hooks (https://v2.s3.dimah.dev/docs/server/hooks/upload)

Enable upload operations on a [named route](https://v2.s3.dimah.dev/docs/server/routes) with `upload: true` or an `UploadConfig` object.

```ts title="lib/s3.ts"
import { dimahS3, route, s3Error } from "@dimah-s3/server";
import { awsS3 } from "./aws-s3";

export const s3 = dimahS3({
  client: awsS3,
  bucket: process.env.S3_BUCKET!,
  routes: {
    avatar: route({
      upload: {
        fileTypes: ["image/*"],
        maxFileSize: 2 * 1024 * 1024,
        guard: async ({ request }) => {
          const session = await getSession(request);
          if (!session) throw s3Error("UNAUTHORIZED");
        },
        onConfirmed: async ({ key, object, request }) => {
          // [!code ++]
          const session = await getSession(request); // [!code ++]
          await db.user.update({
            // [!code ++]
            where: { id: session.userId }, // [!code ++]
            data: { avatarKey: key, avatarSize: object.size }, // [!code ++]
          }); // [!code ++]
        }, // [!code ++]
      },
    }),
  },
});
```

---

## Upload lifecycle flow [#upload-lifecycle-flow]

<Flow
  label="Upload lifecycle"
  steps={[
    { name: "route.guard", kind: "hook" },
    { name: "object", kind: "hook" },
    { name: "upload.guard", kind: "hook" },
    { name: "onStart", kind: "hook" },
    { name: "Upload to S3", kind: "client" },
    { name: "HeadObject", kind: "s3" },
    { name: "confirmGuard", kind: "hook" },
    { name: "onConfirmed", kind: "hook" },
  ]}
/>

1. **`route.guard`**: Runs after the instance guard, before the key is chosen.
2. **`object`**: Chooses the key folder or key, S3 metadata, ACL, storage class, cache control, and tags.
3. **`upload.guard`**: Authorizes the upload once the server-owned key is known.
4. **`onStart`**: Runs once the upload can begin, with its `uploadToken` and confirm deadline. The presign response, with its [upload ticket](https://v2.s3.dimah.dev/docs/server/upload-tickets), is returned after it.
5. **Confirm**: HeadObject reads the stored object. The server checks the upload marker, the size against the declared size (`UPLOAD_SIZE_MISMATCH`), and the route limits. With `verifyContent`, it checks the first bytes too.
6. **`confirmGuard`**: Authorizes the stored object. It sees the verified `object` from HeadObject next to the declared `file`, so check `object`. A rejection deletes it.
7. **`onConfirmed`**: Receives the verified `object` from HeadObject, and the `previousKey` the server deletes next. Persist the record here.

`upload.guard` and `onStart` also run for multipart init and `s3.put`. `kind`
tells them apart: `"put"` or `"post"` for a presigned upload, `"multipart"`,
or `"server"` for `s3.put`. `s3.put` skips `confirmGuard`.

---

## Tracking an upload [#tracking-an-upload]

`onStart` is the place to record a pending upload. It receives the absolute
deadline after which the upload can never be confirmed:

```ts
upload: {
  onStart: async ({ uploadToken, key, file, expiresAt, locals }) => {
    await db.upload.create({
      data: {
        token: uploadToken,
        key,
        name: file.name,
        userId: locals.userId,
        expiresAt, // [!code ++]
      },
    });
  },
  onConfirmed: async ({ uploadToken, object }) => {
    await db.upload.updateMany({
      where: { token: uploadToken, status: "pending" },
      data: { status: "active", size: object.size },
    });
  },
},
```

`expiresAt` is the ticket expiry, or 15 minutes from now for `s3.put`.
Presigned uploads also get `url` and `urlExpiresAt`, and multipart uploads
get `uploadId`.

---

## Failure rules [#failure-rules]

| Hook                  | When it throws                                                                  |
| --------------------- | ------------------------------------------------------------------------------- |
| `upload.guard`        | The error is returned. Nothing exists in S3.                                    |
| `onStart`             | The error is returned. A multipart upload is aborted; `s3.put` skips PutObject. |
| `confirmGuard`        | A rejection (`4xx`) deletes the object, or aborts the multipart upload.         |
| `onConfirmed`, plugin | A rejection (`4xx`) deletes the object. Any other throw keeps it for a retry.   |
| `onConfirmed`, yours  | The object is kept and the call returns `500 INTERNAL_ERROR`.                   |

A confirm that fails for an infrastructure reason, such as an S3 network
error or a guard whose database is down, keeps the object so the client can
retry.

`onConfirmed` runs **at least once** per upload. Confirm is stateless, so a
retried confirm runs every check and hook again with the same
`uploadToken`. Make the hook safe to replay, and reject content in
`confirmGuard`, not in `onConfirmed`.

---

## Type reference [#type-reference]

```ts
import type {
  UploadGuardContext,
  UploadOnStartContext,
  UploadConfirmGuardContext,
  UploadOnConfirmedContext,
  UploadObjectContext,
  VerifiedObject,
} from "@dimah-s3/server";
```

### UploadGuardContext [#uploadguardcontext]

Runs after `upload.object` assigns the key (and after the instance and route guards).

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="UploadGuardContext"
/>

---

### UploadOnStartContext [#uploadonstartcontext]

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="UploadOnStartContext"
/>

---

### UploadConfirmGuardContext [#uploadconfirmguardcontext]

Runs on confirm and multipart complete, after HeadObject and the size checks.

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="UploadConfirmGuardContext"
/>

---

### UploadOnConfirmedContext [#uploadonconfirmedcontext]

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="UploadOnConfirmedContext"
/>

---

### VerifiedObject [#verifiedobject]

The `object` in `onConfirmed`, as HeadObject reports it.

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="VerifiedObject"
/>

---

### UploadObjectContext [#uploadobjectcontext]

Input passed to `upload.object` to generate custom destination keys and metadata.

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="UploadObjectContext"
/>

## Frequently asked questions [#frequently-asked-questions]

<Accordions>

<Accordion title="Why should I record file size after confirmation rather than presigning?">

Record it in `onConfirmed`, where `object.size` comes from S3 HeadObject.
`file` in the earlier hooks holds the values the browser declared. Confirm
also rejects an object whose stored size differs from the declared one.

</Accordion>

<Accordion title="What happens if onConfirmed throws?">

Your hook's error is logged and passed to `onError`, and the client gets
`500 INTERNAL_ERROR`. The object is kept, so a retried confirm runs the hook
again. To refuse an object, throw from `confirmGuard`: the object is deleted
and the client gets your error.

</Accordion>

<Accordion title="How do I attach custom S3 user metadata?">

Return `metadata` from `upload.object`. What the client sends as `metadata`
arrives as `clientMetadata` and is never written to S3 unless you copy it:

```ts
avatar: route({
  upload: {
    object: async ({ file, clientMetadata }) => ({
      keyFolder: "avatars",
      metadata: {
        // [!code ++]
        "original-name": file.name, // [!code ++]
        album: clientMetadata?.album ?? "default", // [!code ++]
      }, // [!code ++]
    }),
  },
}),
```

Metadata keys and values share a 2 KB budget. Non-ASCII values are encoded
for you. In `onConfirmed`, `object.metadata` returns them decoded.

</Accordion>

</Accordions>
