Push V1 app
This commit is contained in:
+217
@@ -0,0 +1,217 @@
|
||||
---
|
||||
title: File Uploads
|
||||
---
|
||||
|
||||
# File Uploads
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
_Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) on which this doc is based. You can refer to it for even more examples._
|
||||
|
||||
## Basic File Upload
|
||||
|
||||
### 1. Setup some routes
|
||||
|
||||
You can setup your routes however you like. This example uses the following structure:
|
||||
|
||||
```ts filename=routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
// ... other routes
|
||||
route("user/:id", "pages/user-profile.tsx", [
|
||||
route("avatar", "api/avatar.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
### 2. Add the form data parser
|
||||
|
||||
`form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads.
|
||||
|
||||
```shellscript
|
||||
npm i @remix-run/form-data-parser
|
||||
```
|
||||
|
||||
[See the `form-data-parser` docs for more information][form-data-parser]
|
||||
|
||||
### 3. Create a route with an upload action
|
||||
|
||||
The `parseFormData` function takes an `uploadHandler` function as an argument. This function will be called for each file upload in the form.
|
||||
|
||||
<docs-warning>
|
||||
|
||||
You must set the form's `enctype` to `multipart/form-data` for file uploads to work.
|
||||
|
||||
</docs-warning>
|
||||
|
||||
```tsx filename=pages/user-profile.tsx
|
||||
import {
|
||||
type FileUpload,
|
||||
parseFormData,
|
||||
} from "@remix-run/form-data-parser";
|
||||
import type { Route } from "./+types/user-profile";
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
const uploadHandler = async (fileUpload: FileUpload) => {
|
||||
if (fileUpload.fieldName === "avatar") {
|
||||
// process the upload and return a File
|
||||
}
|
||||
};
|
||||
|
||||
const formData = await parseFormData(
|
||||
request,
|
||||
uploadHandler,
|
||||
);
|
||||
// 'avatar' has already been processed at this point
|
||||
const file = formData.get("avatar");
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<form method="post" encType="multipart/form-data">
|
||||
<input type="file" name="avatar" />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Local Storage Implementation
|
||||
|
||||
### 1. Add the storage package
|
||||
|
||||
`file-storage` is a key/value interface for storing [File objects][file] in JavaScript. Similar to how `localStorage` allows you to store key/value pairs of strings in the browser, file-storage allows you to store key/value pairs of files on the server.
|
||||
|
||||
```shellscript
|
||||
npm i @remix-run/file-storage
|
||||
```
|
||||
|
||||
[See the `file-storage` docs for more information][file-storage]
|
||||
|
||||
### 2. Create a storage configuration
|
||||
|
||||
Create a file that exports a `LocalFileStorage` instance to be used by different routes.
|
||||
|
||||
```ts filename=avatar-storage.server.ts
|
||||
import { LocalFileStorage } from "@remix-run/file-storage/local";
|
||||
|
||||
export const fileStorage = new LocalFileStorage(
|
||||
"./uploads/avatars",
|
||||
);
|
||||
|
||||
export function getStorageKey(userId: string) {
|
||||
return `user-${userId}-avatar`;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement the upload handler
|
||||
|
||||
Update the form's `action` to store files in the `fileStorage` instance.
|
||||
|
||||
```tsx filename=pages/user-profile.tsx
|
||||
import {
|
||||
type FileUpload,
|
||||
parseFormData,
|
||||
} from "@remix-run/form-data-parser";
|
||||
import {
|
||||
fileStorage,
|
||||
getStorageKey,
|
||||
} from "~/avatar-storage.server";
|
||||
import type { Route } from "./+types/user-profile";
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
params,
|
||||
}: Route.ActionArgs) {
|
||||
async function uploadHandler(fileUpload: FileUpload) {
|
||||
if (
|
||||
fileUpload.fieldName === "avatar" &&
|
||||
fileUpload.type.startsWith("image/")
|
||||
) {
|
||||
let storageKey = getStorageKey(params.id);
|
||||
|
||||
// FileUpload objects are not meant to stick around for very long (they are
|
||||
// streaming data from the request.body); store them as soon as possible.
|
||||
await fileStorage.set(storageKey, fileUpload);
|
||||
|
||||
// Return a File for the FormData object. This is a LazyFile that knows how
|
||||
// to access the file's content if needed (using e.g. file.stream()) but
|
||||
// waits until it is requested to actually read anything.
|
||||
return fileStorage.get(storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = await parseFormData(
|
||||
request,
|
||||
uploadHandler,
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserPage({
|
||||
actionData,
|
||||
params,
|
||||
}: Route.ComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>User {params.id}</h1>
|
||||
<form
|
||||
method="post"
|
||||
// The form's enctype must be set to "multipart/form-data" for file uploads
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
<input type="file" name="avatar" accept="image/*" />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
|
||||
<img
|
||||
src={`/user/${params.id}/avatar`}
|
||||
alt="user avatar"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add a route to serve the uploaded file
|
||||
|
||||
Create a [resource route][resource-route] that streams the file as a response.
|
||||
|
||||
```tsx filename=api/avatar.tsx
|
||||
import {
|
||||
fileStorage,
|
||||
getStorageKey,
|
||||
} from "~/avatar-storage.server";
|
||||
import type { Route } from "./+types/avatar";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const storageKey = getStorageKey(params.id);
|
||||
const file = await fileStorage.get(storageKey);
|
||||
|
||||
if (!file) {
|
||||
throw new Response("User avatar not found", {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(file.stream(), {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Content-Disposition": `attachment; filename=${file.name}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
[form-data-parser]: https://www.npmjs.com/package/@remix-run/form-data-parser
|
||||
[file-storage]: https://www.npmjs.com/package/@remix-run/file-storage
|
||||
[file]: https://developer.mozilla.org/en-US/docs/Web/API/File
|
||||
[resource-route]: ../how-to/resource-routes
|
||||
Reference in New Issue
Block a user