Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(examples): add file upload to S3 example #3433

Merged
merged 6 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
- gonzoscript
- graham42
- GregBrimble
- GSt4r
- guatedude2
- guerra08
- gunners6518
Expand Down
4 changes: 4 additions & 0 deletions examples/file-and-s3-upload/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
STORAGE_ACCESS_KEY=
STORAGE_SECRET=
STORAGE_REGION=
STORAGE_BUCKET=
3 changes: 3 additions & 0 deletions examples/file-and-s3-upload/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
};
6 changes: 6 additions & 0 deletions examples/file-and-s3-upload/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
/public/build
.env
35 changes: 35 additions & 0 deletions examples/file-and-s3-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Upload images to S3

This is a simple example of using the remix built-in [uploadHandler](https://remix.run/docs/en/v1/api/remix#uploadhandler) and Form with multipart data to upload a file with the built-in local uploader and upload an image file to S3 with a custom uploader and display it. You can test it locally by running the dev server and opening the path `/s3-upload` in your browser.

The relevent files are:

```
├── app
│ ├── routes
│ │ ├── s3-upload.tsx // upload to S3
│ └── utils
│ └── s3.server.ts // init S3 client on server side
|── .env // holds AWS S3 credentails
```

## Steps to set up an S3 bucket

- Sign up for an [AWS account](https://portal.aws.amazon.com/billing/signup) - this will require a credit card
- Create an S3 bucket in your desired region
- Create an access key pair for an IAM user that has access to the bucket
- Copy the .env.sample to .env and fill in the S3 bucket, the region as well as the access key and secret key from the IAM user

Note: in order for the image to be displayed after being uploaded to your S3 bucket in this example, the bucket needs to have public access enabled, which is potentially dangerous.

> :warning: Lambda imposes a [limit of 6MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) on the invocation payload size. If you use this example with Remix running on Lambda, you can only update files with a size smaller than 6MB.

Open this example on [CodeSandbox](https://codesandbox.com):

[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/remix/tree/main/examples/file-and-s3-upload)

## Related Links

- [Handle Multiple Part Forms (File Uploads)](https://remix.run/docs/en/v1/api/remix#unstable_parsemultipartformdata-node)
- [Upload Handler](https://remix.run/docs/en/v1/api/remix#uploadhandler)
- [Custom Uploader](https://remix.run/docs/en/v1/api/remix#custom-uploadhandler)
4 changes: 4 additions & 0 deletions examples/file-and-s3-upload/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { RemixBrowser } from "@remix-run/react";
import { hydrate } from "react-dom";

hydrate(<RemixBrowser />, document);
21 changes: 21 additions & 0 deletions examples/file-and-s3-upload/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);

responseHeaders.set("Content-Type", "text/html");

return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}
32 changes: 32 additions & 0 deletions examples/file-and-s3-upload/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";

export const meta: MetaFunction = () => ({
charset: "utf-8",
title: "New Remix App",
viewport: "width=device-width,initial-scale=1",
});

export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
60 changes: 60 additions & 0 deletions examples/file-and-s3-upload/app/routes/s3-upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { ActionFunction, UploadHandler } from "@remix-run/node";
import {
json,
unstable_composeUploadHandlers as composeUploadHandlers,
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
unstable_parseMultipartFormData as parseMultipartFormData,
} from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import { s3UploadHandler } from "~/utils/s3.server";

type ActionData = {
errorMsg?: string;
imgSrc?: string;
imgDesc?: string;
};

export const action: ActionFunction = async ({ request }) => {
const uploadHandler: UploadHandler = composeUploadHandlers(
s3UploadHandler,
createMemoryUploadHandler()
);
const formData = await parseMultipartFormData(request, uploadHandler);
const imgSrc = formData.get("img");
const imgDesc = formData.get("desc");
console.log(imgDesc)
if (!imgSrc) {
return json({
errorMsg: "Something went wrong while uploading",
});
}
return json({
imgSrc,
imgDesc,
});
};

export default function Index() {
const fetcher = useFetcher<ActionData>();
return (
<>
<fetcher.Form method="post" encType="multipart/form-data">
<label htmlFor="img-field">Image to upload</label>
<input id="img-field" type="file" name="img" accept="image/*" />
<label htmlFor="img-desc">Image description</label>
<input id="img-desc" type="text" name="desc" />
<button type="submit">Upload to S3</button>
</fetcher.Form>
{fetcher.type === "done" ? (
fetcher.data.errorMsg ? (
<h2>{fetcher.data.errorMsg}</h2>
) : (
<>
<div>File has been uploaded to S3 and is available under the following URL (if the bucket has public access enabled):</div>
<div>{fetcher.data.imgSrc}</div>
<img src={fetcher.data.imgSrc} alt={fetcher.data.imgDesc || "Uploaded image from S3"} />
</>
)) : null}
</>
);
}
46 changes: 46 additions & 0 deletions examples/file-and-s3-upload/app/utils/s3.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import AWS from "aws-sdk"
import type { UploadHandler } from "@remix-run/node"
import { writeAsyncIterableToWritable } from "@remix-run/node"
import { PassThrough } from "stream"

const { STORAGE_ACCESS_KEY, STORAGE_SECRET, STORAGE_REGION, STORAGE_BUCKET } = process.env

if (!(STORAGE_ACCESS_KEY && STORAGE_SECRET && STORAGE_REGION && STORAGE_BUCKET)) {
throw new Error(`Storage is missing required configuration.`)
}

const uploadStream = ({ Key }: Pick<AWS.S3.Types.PutObjectRequest, 'Key'>) => {
const s3 = new AWS.S3({
credentials: {
accessKeyId: STORAGE_ACCESS_KEY,
secretAccessKey: STORAGE_SECRET,
},
region: STORAGE_REGION,
})
const pass = new PassThrough()
return {
writeStream: pass,
promise: s3.upload({ Bucket: STORAGE_BUCKET, Key, Body: pass }).promise(),
}
}

export async function uploadStreamToS3(data: any, filename: string) {
const stream = uploadStream({
Key: filename,
})
await writeAsyncIterableToWritable(data, stream.writeStream)
const file = await stream.promise
return file.Location
}

export const s3UploadHandler: UploadHandler = async ({
name,
filename,
data,
}) => {
if (name !== "img") {
return undefined;
}
const uploadedFileLocation = await uploadStreamToS3(data, filename!)
return uploadedFileLocation
}
28 changes: 28 additions & 0 deletions examples/file-and-s3-upload/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"private": true,
"sideEffects": false,
"scripts": {
"build": "remix build",
"dev": "remix dev",
"start": "remix-serve build"
},
"dependencies": {
"@remix-run/node": "1.5.1",
"@remix-run/react": "1.5.1",
"@remix-run/serve": "1.5.1",
"aws-sdk": "^2.1152.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@remix-run/dev": "1.5.1",
"@remix-run/eslint-config": "1.5.1",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"eslint": "^8.10.0",
"typescript": "^4.6.2"
},
"engines": {
"node": ">=14"
}
}
Binary file added examples/file-and-s3-upload/public/favicon.ico
Binary file not shown.
10 changes: 10 additions & 0 deletions examples/file-and-s3-upload/remix.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @type {import('@remix-run/dev').AppConfig}
*/
module.exports = {
ignoredRouteFiles: ["**/.*"],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
};
2 changes: 2 additions & 0 deletions examples/file-and-s3-upload/remix.env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node/globals" />
6 changes: 6 additions & 0 deletions examples/file-and-s3-upload/sandbox.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"hardReloadOnChange": true,
"container": {
"port": 3000
}
}
22 changes: 22 additions & 0 deletions examples/file-and-s3-upload/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"resolveJsonModule": true,
"target": "ES2019",
"strict": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},

// Remix takes care of building everything in `remix build`.
"noEmit": true
}
}