# Download Hooks (https://v2.s3.dimah.dev/docs/server/hooks/download)

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

The key must sit under this route's `keyPrefix`. That is not ownership — a
session-only `guard` still lets any signed-in user download any namespaced
key they know. See
[Security](https://v2.s3.dimah.dev/docs/server/security#route-namespace-vs-ownership).

```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({
      download: {
        disposition: "inline",
        guard: async ({ key, request }) => {
          // [!code ++]
          const session = await getSession(request); // [!code ++]
          if (!session) throw s3Error("UNAUTHORIZED"); // [!code ++]
        }, // [!code ++]
      },
    }),
  },
});
```

---

## Download lifecycle flow [#download-lifecycle-flow]

<Flow
  label="Download flow"
  steps={[
    { name: "route.guard", kind: "hook" },
    { name: "download.resolve", kind: "hook" },
    { name: "download.guard", kind: "hook" },
    { name: "HeadObject", kind: "s3" },
    { name: "onPresigned", kind: "hook" },
    { name: "Fetch from S3", kind: "client" },
  ]}
/>

1. **`resolve`**: Optionally rewrites the download `name`, `disposition` (`inline` vs `attachment`), `expiresIn`, or, in proxy mode, `cacheControl`. A returned `expiresIn` replaces the route cap and any client request, then is capped by the instance `maxExpiresIn`.
2. **`guard`**: Authorizes the request for this `key`, with the values that will be used. `expiresIn` is the TTL that will be signed, and `undefined` in proxy mode.
3. **HeadObject**: Reads the stored type, to decide whether `inline` is allowed.
4. **`onPresigned`**: Receives the URL and the disposition that was signed.

A client may send `expiresIn` on the download query. The signed TTL is the
shorter of that request and `download.maxExpiresIn` (default 10 minutes)
unless `resolve` replaces it. `GET /routes` reports the cap as
`download.maxExpiresIn`.

In proxy mode (`mode: "proxy"`), the URL points to
[`GET /file`](https://v2.s3.dimah.dev/docs/server/file-proxy), the response has no `expiresIn`, and
every request to that URL runs the guards again.

---

## Open in the browser only for known types [#open-in-the-browser-only-for-known-types]

A client can ask for `disposition: "inline"`. The server keeps it only for
types on the route's allow-list, and signs everything else as `attachment`.
The default list is `DEFAULT_INLINE_TYPES` from `@dimah-s3/core`:

- `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/avif`, `image/bmp`
- `application/pdf` (presign mode only)
- `audio/*`, `video/*`
- `text/plain`

Set `inlineTypes` to change it, or `false` to never open inline:

```ts title="lib/s3.ts"
files: route({
  download: {
    inlineTypes: ["image/*", "application/pdf", "video/mp4"], // [!code ++]
  },
}),
```

After the guards, the server reads the object's type from HeadObject:

- On the list (`image/*` matches the same way as `upload.fileTypes`): the
  requested `inline` is kept.
- Anything else is signed as `attachment`, whatever the client asked for.
- A wildcard never matches types that run as a page: HTML, any XML
  (including `image/svg+xml`), and JavaScript. List one exactly to allow it.
- An object stored as `application/octet-stream` is typed from its file name.
  When that type is allowed, it is signed as `ResponseContentType` so the
  browser renders it.

`GET /routes` returns the effective list as `download.inlineTypes`. A client
can pass it to `resolveInlineDisposition` from `@dimah-s3/core` to choose
Preview or Download with the server's rule. The same helper works inside
`resolve`.

---

## Type reference [#type-reference]

```ts
import type {
  DownloadGuardContext,
  DownloadOnPresignedContext,
  DownloadResolveContext,
  DownloadResolveInfo,
} from "@dimah-s3/server";
```

### DownloadGuardContext [#downloadguardcontext]

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

---

### DownloadOnPresignedContext [#downloadonpresignedcontext]

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

---

### DownloadResolveContext [#downloadresolvecontext]

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

---

### DownloadResolveInfo [#downloadresolveinfo]

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

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

<Accordions>

<Accordion title="Can the client choose how long a download link lasts?">

Yes, up to the route cap. `download.maxExpiresIn` is the maximum. A shorter
`expiresIn` on the query is signed as requested. Return `expiresIn` from
`resolve` when the server should choose a different TTL, including one
longer than the route cap. That value is still capped by the instance
`maxExpiresIn` (7 days at most).

```ts
files: route({
  download: {
    maxExpiresIn: 60 * 60 * 24, // [!code ++]
  },
}),
```

</Accordion>

<Accordion title="How do I change the download filename dynamically?">

Use `download.resolve` to set the downloaded file name per request:

```ts
avatar: route({
  download: {
    resolve: async ({ key }) => {
      const user = await getUserByKey(key);
      return {
        name: `${user.username}-avatar.png`, // [!code ++]
        disposition: "attachment", // [!code ++]
      };
    },
  },
}),
```

</Accordion>

<Accordion title="How do I stream downloads through my server?">

Set `mode: "proxy"`. The browser receives a URL on your API
(`/api/s3/file?route=…&key=…`), and your server streams the object instead of
returning a direct bucket URL. See [File Proxy](https://v2.s3.dimah.dev/docs/server/file-proxy).

```ts
avatar: route({
  download: {
    mode: "proxy", // [!code ++]
  },
}),
```

</Accordion>

</Accordions>
