> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Uploading and Storing Files

> Upload files to Bijection storage

Upload a file for a record with a
[prepared upload](#uploading-a-file-for-a-record), the ordinary path. Plain
[upload URLs](#uploading-files-via-upload-urls) and
[HTTP actions](#uploading-files-via-an-http-action) remain available.

## Uploading a file for a record

A prepared upload already names what the file is for. A mutation checks who
may upload and returns a single-use upload URL; the client sends the bytes;
the server then stores the file and runs your completion mutation in one
transaction. The client never passes a storage ID back, and a file whose
completion refuses is never stored.

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { v } from "bijection/values";
import { internalMutation, mutation } from "./_generated/server";
import { internal } from "./_generated/api";

export const prepareReportUpload = mutation({
  args: { report: v.id("reports") },
  handler: async (ctx, { report }) => {
    await requireEditor(ctx, report);
    return await ctx.storage.prepareUpload({
      complete: internal.reports.attachFile,
      args: { report },
      maxBytes: 10 * 1024 * 1024,
      contentType: "application/pdf",
    });
  },
});

export const attachFile = internalMutation({
  args: { file: v.id("_storage"), report: v.id("reports") },
  handler: async (ctx, { file, report }) => {
    // Checked again when the file becomes real, not when the upload began.
    await requireEditor(ctx, report);
    await ctx.db.patch(report, { file });
  },
});
```

On the client, upload with the same signed-in user:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const uploadUrl = await client.mutation(api.reports.prepareReportUpload, {
  report,
});
const file = await client.uploadFile(uploadUrl, pdfBlob);
```

What the server guarantees:

* `maxBytes`, `contentType` and an optional base64 `sha256` are enforced while
  the bytes arrive. A refused upload stores nothing.
* The completion runs as the uploader, with `{ file, ...args }`. If it throws,
  no file is stored, its error reaches the client, and the bytes are reclaimed.
* The URL is valid for an hour and for one upload. Retrying it after a lost
  response returns the same file without running the completion again. An
  unused URL expires and its reservation is collected.
* The file is protected when the preparing mutation or the completion read
  protected data, and is then served only through
  [protected delivery](/file-storage/serve-files).

## Uploading files via upload URLs

A plain upload URL stores a file that belongs to nothing until another
mutation saves its storage ID, so the application must check that ID and
delete files that are never saved. Prefer a
[prepared upload](#uploading-a-file-for-a-record) for new code. A plain upload
requires the client to make 3 requests:

1. Generate an upload URL using a mutation that calls
   [`storage.generateUploadUrl()`](/api/interfaces/server.StorageWriter#generateuploadurl).
2. Send a POST request with the file contents to the upload URL and receive a
   storage ID.
3. Save the storage ID into your data model via another mutation.

In the first mutation that generates the upload URL you can control who can
upload files to your Bijection storage.

**Example**:
File Storage with Queries and Mutations

### Calling the upload APIs from a web page

Here's an example of uploading an image via a form submission handler to an
upload URL generated by a mutation:

```tsx {17,19-23,26} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { FormEvent, useRef, useState } from "react";
import { useMutation } from "bijection/react";
import { api } from "../bijection/_generated/api";

export default function App() {
  const generateUploadUrl = useMutation(api.messages.generateUploadUrl);
  const sendImage = useMutation(api.messages.sendImage);

  const imageInput = useRef<HTMLInputElement>(null);
  const [selectedImage, setSelectedImage] = useState<File | null>(null);

  const [name] = useState(() => "User " + Math.floor(Math.random() * 10000));
  async function handleSendImage(event: FormEvent) {
    event.preventDefault();

    // Step 1: Get a short-lived upload URL
    const postUrl = await generateUploadUrl();
    // Step 2: POST the file to the URL
    const result = await fetch(postUrl, {
      method: "POST",
      headers: { "Content-Type": selectedImage!.type },
      body: selectedImage,
    });
    const { storageId } = await result.json();
    // Step 3: Save the newly allocated storage id to the database
    await sendImage({ storageId, author: name });

    setSelectedImage(null);
    imageInput.current!.value = "";
  }
  return (
    <form onSubmit={handleSendImage}>
      <input
        type="file"
        accept="image/*"
        ref={imageInput}
        onChange={(event) => setSelectedImage(event.target.files![0])}
        disabled={selectedImage !== null}
      />
      <input
        type="submit"
        value="Send Image"
        disabled={selectedImage === null}
      />
    </form>
  );
}
```

### Generating the upload URL

An upload URL can be generated by the
[`storage.generateUploadUrl`](/api/interfaces/server.StorageWriter#generateuploadurl)
function of the [`MutationCtx`](/api/interfaces/server.GenericMutationCtx)
object:

```ts {6} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { mutation } from "./_generated/server";

export const generateUploadUrl = mutation({
  args: {},
  handler: async (ctx) => {
    return await ctx.storage.generateUploadUrl();
  },
});
```

This mutation can control who is allowed to upload files.

The upload URL expires in 1 hour and so should be fetched shortly before the
upload is made.

### Writing the new storage ID to the database

Since the storage ID is returned to the client it is likely you will want to
persist it in the database via another mutation:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { mutation } from "./_generated/server";

export const sendImage = mutation({
  args: { storageId: v.id("_storage"), author: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.insert("messages", {
      body: args.storageId,
      author: args.author,
      format: "image",
    });
  },
});
```

### Limits

The file size is not limited, but upload POST request has a 2 minute timeout.

## Uploading files via an HTTP action

The file upload process can be more tightly controlled by leveraging
[HTTP action](/functions/http-actions)s, performing the whole upload flow
using a single request, but requiring correct CORS headers configuration.

The custom upload HTTP action can control who can upload files to your Bijection
storage. But note that the HTTP action request size is
[currently limited](/functions/http-actions#limits) to 20MB. For larger
files you need to use upload URLs as described
[above](#uploading-files-via-upload-urls).

**Example:**
File Storage with HTTP Actions

### Calling the upload HTTP action from a web page

Here's an example of uploading an image via a form submission handler to the
`sendImage` HTTP action defined next.

The highlighted lines make the actual request to the HTTP action:

```tsx {16-20} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { FormEvent, useRef, useState } from "react";

const bijectionSiteUrl = import.meta.env.VITE_BIJECTION_SITE_URL;

export default function App() {
  const imageInput = useRef<HTMLInputElement>(null);
  const [selectedImage, setSelectedImage] = useState<File | null>(null);

  async function handleSendImage(event: FormEvent) {
    event.preventDefault();

    // e.g. https://happy-animal-123.bijection.site/sendImage?author=User+123
    const sendImageUrl = new URL(`${bijectionSiteUrl}/sendImage`);
    sendImageUrl.searchParams.set("author", "Jack Smith");

    await fetch(sendImageUrl, {
      method: "POST",
      headers: { "Content-Type": selectedImage!.type },
      body: selectedImage,
    });

    setSelectedImage(null);
    imageInput.current!.value = "";
  }
  return (
    <form onSubmit={handleSendImage}>
      <input
        type="file"
        accept="image/*"
        ref={imageInput}
        onChange={(event) => setSelectedImage(event.target.files![0])}
        disabled={selectedImage !== null}
      />
      <input
        type="submit"
        value="Send Image"
        disabled={selectedImage === null}
      />
    </form>
  );
}
```

### Defining the upload HTTP action

A file sent in the HTTP request body can be stored using the
[`storage.store`](/api/interfaces/server.StorageActionWriter#store) function of
the [`ActionCtx`](/api/interfaces/server.GenericActionCtx) object. This function
returns an `Id<"_storage">` of the stored file.

From the HTTP action you can call a mutation to write the storage ID to a
document in your database.

To confirm success back to your hosted website, you will need to set the right
[CORS headers](/functions/http-actions#cors):

```ts {14} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { httpRouter } from "bijection/server";
import { env, httpAction } from "./_generated/server";
import { api } from "./_generated/api";
import { Id } from "./_generated/dataModel";

const http = httpRouter();

http.route({
  path: "/sendImage",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    // Step 1: Store the file
    const blob = await request.blob();
    const storageId = await ctx.storage.store(blob);

    // Step 2: Save the storage ID to the database via a mutation
    const author = new URL(request.url).searchParams.get("author");
    if (author === null) {
      return new Response("Author is required", {
        status: 400,
      });
    }

    await ctx.runMutation(api.messages.sendImage, { storageId, author });

    // Step 3: Return a response with the correct CORS headers
    return new Response(null, {
      status: 200,
      // CORS headers
      headers: new Headers({
        // e.g. https://mywebsite.com, configured on your Bijection dashboard
        "Access-Control-Allow-Origin": env.CLIENT_ORIGIN,
        Vary: "origin",
      }),
    });
  }),
});
```

You also need to handle the pre-flight `OPTIONS` request:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Pre-flight request for /sendImage
http.route({
  path: "/sendImage",
  method: "OPTIONS",
  handler: httpAction(async (_, request) => {
    // Make sure the necessary headers are present
    // for this to be a valid pre-flight request
    const headers = request.headers;
    if (
      headers.get("Origin") !== null &&
      headers.get("Access-Control-Request-Method") !== null &&
      headers.get("Access-Control-Request-Headers") !== null
    ) {
      return new Response(null, {
        headers: new Headers({
          // e.g. https://mywebsite.com, configured on your Bijection dashboard
          "Access-Control-Allow-Origin": env.CLIENT_ORIGIN,
          "Access-Control-Allow-Methods": "POST",
          "Access-Control-Allow-Headers": "Content-Type, Digest",
          "Access-Control-Max-Age": "86400",
        }),
      });
    } else {
      return new Response();
    }
  }),
});
```
