From df2359ce38be7e961412259726d41b4361c72f16 Mon Sep 17 00:00:00 2001 From: GSt4r <11350542+GSt4r@users.noreply.github.com> Date: Thu, 9 Jun 2022 23:10:28 +0200 Subject: [PATCH 1/5] S3 file upload (#1) * Implemented S3 upload * Updated README * Cleaned up dependencies --- .../file-and-cloudinary-upload/package.json | 1 - examples/file-and-s3-upload/.env.sample | 4 ++ examples/file-and-s3-upload/.eslintrc.js | 3 + examples/file-and-s3-upload/.gitignore | 6 ++ examples/file-and-s3-upload/README.md | 35 +++++++++++ .../file-and-s3-upload/app/entry.client.tsx | 4 ++ .../file-and-s3-upload/app/entry.server.tsx | 21 +++++++ examples/file-and-s3-upload/app/root.tsx | 32 ++++++++++ .../app/routes/s3-upload.tsx | 59 ++++++++++++++++++ .../file-and-s3-upload/app/utils/s3.server.ts | 46 ++++++++++++++ examples/file-and-s3-upload/package.json | 29 +++++++++ .../file-and-s3-upload/public/favicon.ico | Bin 0 -> 16958 bytes examples/file-and-s3-upload/remix.config.js | 10 +++ examples/file-and-s3-upload/remix.env.d.ts | 2 + .../file-and-s3-upload/sandbox.config.json | 6 ++ examples/file-and-s3-upload/tsconfig.json | 22 +++++++ 16 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 examples/file-and-s3-upload/.env.sample create mode 100644 examples/file-and-s3-upload/.eslintrc.js create mode 100644 examples/file-and-s3-upload/.gitignore create mode 100644 examples/file-and-s3-upload/README.md create mode 100644 examples/file-and-s3-upload/app/entry.client.tsx create mode 100644 examples/file-and-s3-upload/app/entry.server.tsx create mode 100644 examples/file-and-s3-upload/app/root.tsx create mode 100644 examples/file-and-s3-upload/app/routes/s3-upload.tsx create mode 100644 examples/file-and-s3-upload/app/utils/s3.server.ts create mode 100644 examples/file-and-s3-upload/package.json create mode 100644 examples/file-and-s3-upload/public/favicon.ico create mode 100644 examples/file-and-s3-upload/remix.config.js create mode 100644 examples/file-and-s3-upload/remix.env.d.ts create mode 100644 examples/file-and-s3-upload/sandbox.config.json create mode 100644 examples/file-and-s3-upload/tsconfig.json diff --git a/examples/file-and-cloudinary-upload/package.json b/examples/file-and-cloudinary-upload/package.json index 2420b7c20c6..2a1c19fcdfc 100644 --- a/examples/file-and-cloudinary-upload/package.json +++ b/examples/file-and-cloudinary-upload/package.json @@ -10,7 +10,6 @@ "@remix-run/node": "1.5.1", "@remix-run/react": "1.5.1", "@remix-run/serve": "1.5.1", - "cloudinary": "^1.28.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/examples/file-and-s3-upload/.env.sample b/examples/file-and-s3-upload/.env.sample new file mode 100644 index 00000000000..17b368d399e --- /dev/null +++ b/examples/file-and-s3-upload/.env.sample @@ -0,0 +1,4 @@ +STORAGE_ACCESS_KEY= +STORAGE_SECRET= +STORAGE_REGION= +STORAGE_BUCKET= \ No newline at end of file diff --git a/examples/file-and-s3-upload/.eslintrc.js b/examples/file-and-s3-upload/.eslintrc.js new file mode 100644 index 00000000000..ced78085f86 --- /dev/null +++ b/examples/file-and-s3-upload/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], +}; diff --git a/examples/file-and-s3-upload/.gitignore b/examples/file-and-s3-upload/.gitignore new file mode 100644 index 00000000000..3f7bf98da3e --- /dev/null +++ b/examples/file-and-s3-upload/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/examples/file-and-s3-upload/README.md b/examples/file-and-s3-upload/README.md new file mode 100644 index 00000000000..36249dcd670 --- /dev/null +++ b/examples/file-and-s3-upload/README.md @@ -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) diff --git a/examples/file-and-s3-upload/app/entry.client.tsx b/examples/file-and-s3-upload/app/entry.client.tsx new file mode 100644 index 00000000000..3eec1fd0a02 --- /dev/null +++ b/examples/file-and-s3-upload/app/entry.client.tsx @@ -0,0 +1,4 @@ +import { RemixBrowser } from "@remix-run/react"; +import { hydrate } from "react-dom"; + +hydrate(, document); diff --git a/examples/file-and-s3-upload/app/entry.server.tsx b/examples/file-and-s3-upload/app/entry.server.tsx new file mode 100644 index 00000000000..5afa18235cc --- /dev/null +++ b/examples/file-and-s3-upload/app/entry.server.tsx @@ -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( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + status: responseStatusCode, + headers: responseHeaders, + }); +} diff --git a/examples/file-and-s3-upload/app/root.tsx b/examples/file-and-s3-upload/app/root.tsx new file mode 100644 index 00000000000..927a0f745df --- /dev/null +++ b/examples/file-and-s3-upload/app/root.tsx @@ -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 ( + + + + + + + + + + + + + ); +} diff --git a/examples/file-and-s3-upload/app/routes/s3-upload.tsx b/examples/file-and-s3-upload/app/routes/s3-upload.tsx new file mode 100644 index 00000000000..c9bedadfc3f --- /dev/null +++ b/examples/file-and-s3-upload/app/routes/s3-upload.tsx @@ -0,0 +1,59 @@ +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 { Form, useActionData } 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({ + error: "Something went wrong while uploading", + }); + } + return json({ + imgSrc, + imgDesc, + }); +}; + +export default function Index() { + const data = useActionData(); + return ( + <> +
+ + + + + +
+ {data?.errorMsg &&

{data.errorMsg}

} + {data?.imgSrc && ( + <> +
File has been uploaded to S3 and is available under the following URL (if the bucket has public access enabled):
+
{data.imgSrc}
+ {data.imgDesc + + + )} + + ); +} diff --git a/examples/file-and-s3-upload/app/utils/s3.server.ts b/examples/file-and-s3-upload/app/utils/s3.server.ts new file mode 100644 index 00000000000..5f755c83fff --- /dev/null +++ b/examples/file-and-s3-upload/app/utils/s3.server.ts @@ -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) => { + 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 +} diff --git a/examples/file-and-s3-upload/package.json b/examples/file-and-s3-upload/package.json new file mode 100644 index 00000000000..e54e9a1b766 --- /dev/null +++ b/examples/file-and-s3-upload/package.json @@ -0,0 +1,29 @@ +{ + "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", + "cloudinary": "^1.28.1", + "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" + } +} diff --git a/examples/file-and-s3-upload/public/favicon.ico b/examples/file-and-s3-upload/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8830cf6821b354114848e6354889b8ecf6d2bc61 GIT binary patch literal 16958 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 EKbET!0{{R3 literal 0 HcmV?d00001 diff --git a/examples/file-and-s3-upload/remix.config.js b/examples/file-and-s3-upload/remix.config.js new file mode 100644 index 00000000000..260b82c7cb1 --- /dev/null +++ b/examples/file-and-s3-upload/remix.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ["**/.*"], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", +}; diff --git a/examples/file-and-s3-upload/remix.env.d.ts b/examples/file-and-s3-upload/remix.env.d.ts new file mode 100644 index 00000000000..72e2affe311 --- /dev/null +++ b/examples/file-and-s3-upload/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/file-and-s3-upload/sandbox.config.json b/examples/file-and-s3-upload/sandbox.config.json new file mode 100644 index 00000000000..4363d87a30d --- /dev/null +++ b/examples/file-and-s3-upload/sandbox.config.json @@ -0,0 +1,6 @@ +{ + "hardReloadOnChange": true, + "container": { + "port": 3000 + } +} diff --git a/examples/file-and-s3-upload/tsconfig.json b/examples/file-and-s3-upload/tsconfig.json new file mode 100644 index 00000000000..20f8a386a6c --- /dev/null +++ b/examples/file-and-s3-upload/tsconfig.json @@ -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 + } +} From 37b99767e8416b8f666e76d216cdc517191ae472 Mon Sep 17 00:00:00 2001 From: GSt4r <11350542+GSt4r@users.noreply.github.com> Date: Thu, 9 Jun 2022 23:11:37 +0200 Subject: [PATCH 2/5] Revert "S3 file upload (#1)" (#2) This reverts commit df2359ce38be7e961412259726d41b4361c72f16. --- .../file-and-cloudinary-upload/package.json | 1 + examples/file-and-s3-upload/.env.sample | 4 -- examples/file-and-s3-upload/.eslintrc.js | 3 - examples/file-and-s3-upload/.gitignore | 6 -- examples/file-and-s3-upload/README.md | 35 ----------- .../file-and-s3-upload/app/entry.client.tsx | 4 -- .../file-and-s3-upload/app/entry.server.tsx | 21 ------- examples/file-and-s3-upload/app/root.tsx | 32 ---------- .../app/routes/s3-upload.tsx | 59 ------------------ .../file-and-s3-upload/app/utils/s3.server.ts | 46 -------------- examples/file-and-s3-upload/package.json | 29 --------- .../file-and-s3-upload/public/favicon.ico | Bin 16958 -> 0 bytes examples/file-and-s3-upload/remix.config.js | 10 --- examples/file-and-s3-upload/remix.env.d.ts | 2 - .../file-and-s3-upload/sandbox.config.json | 6 -- examples/file-and-s3-upload/tsconfig.json | 22 ------- 16 files changed, 1 insertion(+), 279 deletions(-) delete mode 100644 examples/file-and-s3-upload/.env.sample delete mode 100644 examples/file-and-s3-upload/.eslintrc.js delete mode 100644 examples/file-and-s3-upload/.gitignore delete mode 100644 examples/file-and-s3-upload/README.md delete mode 100644 examples/file-and-s3-upload/app/entry.client.tsx delete mode 100644 examples/file-and-s3-upload/app/entry.server.tsx delete mode 100644 examples/file-and-s3-upload/app/root.tsx delete mode 100644 examples/file-and-s3-upload/app/routes/s3-upload.tsx delete mode 100644 examples/file-and-s3-upload/app/utils/s3.server.ts delete mode 100644 examples/file-and-s3-upload/package.json delete mode 100644 examples/file-and-s3-upload/public/favicon.ico delete mode 100644 examples/file-and-s3-upload/remix.config.js delete mode 100644 examples/file-and-s3-upload/remix.env.d.ts delete mode 100644 examples/file-and-s3-upload/sandbox.config.json delete mode 100644 examples/file-and-s3-upload/tsconfig.json diff --git a/examples/file-and-cloudinary-upload/package.json b/examples/file-and-cloudinary-upload/package.json index 2a1c19fcdfc..2420b7c20c6 100644 --- a/examples/file-and-cloudinary-upload/package.json +++ b/examples/file-and-cloudinary-upload/package.json @@ -10,6 +10,7 @@ "@remix-run/node": "1.5.1", "@remix-run/react": "1.5.1", "@remix-run/serve": "1.5.1", + "cloudinary": "^1.28.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/examples/file-and-s3-upload/.env.sample b/examples/file-and-s3-upload/.env.sample deleted file mode 100644 index 17b368d399e..00000000000 --- a/examples/file-and-s3-upload/.env.sample +++ /dev/null @@ -1,4 +0,0 @@ -STORAGE_ACCESS_KEY= -STORAGE_SECRET= -STORAGE_REGION= -STORAGE_BUCKET= \ No newline at end of file diff --git a/examples/file-and-s3-upload/.eslintrc.js b/examples/file-and-s3-upload/.eslintrc.js deleted file mode 100644 index ced78085f86..00000000000 --- a/examples/file-and-s3-upload/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/examples/file-and-s3-upload/.gitignore b/examples/file-and-s3-upload/.gitignore deleted file mode 100644 index 3f7bf98da3e..00000000000 --- a/examples/file-and-s3-upload/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules - -/.cache -/build -/public/build -.env diff --git a/examples/file-and-s3-upload/README.md b/examples/file-and-s3-upload/README.md deleted file mode 100644 index 36249dcd670..00000000000 --- a/examples/file-and-s3-upload/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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) diff --git a/examples/file-and-s3-upload/app/entry.client.tsx b/examples/file-and-s3-upload/app/entry.client.tsx deleted file mode 100644 index 3eec1fd0a02..00000000000 --- a/examples/file-and-s3-upload/app/entry.client.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { RemixBrowser } from "@remix-run/react"; -import { hydrate } from "react-dom"; - -hydrate(, document); diff --git a/examples/file-and-s3-upload/app/entry.server.tsx b/examples/file-and-s3-upload/app/entry.server.tsx deleted file mode 100644 index 5afa18235cc..00000000000 --- a/examples/file-and-s3-upload/app/entry.server.tsx +++ /dev/null @@ -1,21 +0,0 @@ -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( - - ); - - responseHeaders.set("Content-Type", "text/html"); - - return new Response("" + markup, { - status: responseStatusCode, - headers: responseHeaders, - }); -} diff --git a/examples/file-and-s3-upload/app/root.tsx b/examples/file-and-s3-upload/app/root.tsx deleted file mode 100644 index 927a0f745df..00000000000 --- a/examples/file-and-s3-upload/app/root.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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 ( - - - - - - - - - - - - - ); -} diff --git a/examples/file-and-s3-upload/app/routes/s3-upload.tsx b/examples/file-and-s3-upload/app/routes/s3-upload.tsx deleted file mode 100644 index c9bedadfc3f..00000000000 --- a/examples/file-and-s3-upload/app/routes/s3-upload.tsx +++ /dev/null @@ -1,59 +0,0 @@ -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 { Form, useActionData } 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({ - error: "Something went wrong while uploading", - }); - } - return json({ - imgSrc, - imgDesc, - }); -}; - -export default function Index() { - const data = useActionData(); - return ( - <> -
- - - - - -
- {data?.errorMsg &&

{data.errorMsg}

} - {data?.imgSrc && ( - <> -
File has been uploaded to S3 and is available under the following URL (if the bucket has public access enabled):
-
{data.imgSrc}
- {data.imgDesc - - - )} - - ); -} diff --git a/examples/file-and-s3-upload/app/utils/s3.server.ts b/examples/file-and-s3-upload/app/utils/s3.server.ts deleted file mode 100644 index 5f755c83fff..00000000000 --- a/examples/file-and-s3-upload/app/utils/s3.server.ts +++ /dev/null @@ -1,46 +0,0 @@ -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) => { - 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 -} diff --git a/examples/file-and-s3-upload/package.json b/examples/file-and-s3-upload/package.json deleted file mode 100644 index e54e9a1b766..00000000000 --- a/examples/file-and-s3-upload/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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", - "cloudinary": "^1.28.1", - "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" - } -} diff --git a/examples/file-and-s3-upload/public/favicon.ico b/examples/file-and-s3-upload/public/favicon.ico deleted file mode 100644 index 8830cf6821b354114848e6354889b8ecf6d2bc61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16958 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 EKbET!0{{R3 diff --git a/examples/file-and-s3-upload/remix.config.js b/examples/file-and-s3-upload/remix.config.js deleted file mode 100644 index 260b82c7cb1..00000000000 --- a/examples/file-and-s3-upload/remix.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @type {import('@remix-run/dev').AppConfig} - */ -module.exports = { - ignoredRouteFiles: ["**/.*"], - // appDirectory: "app", - // assetsBuildDirectory: "public/build", - // serverBuildPath: "build/index.js", - // publicPath: "/build/", -}; diff --git a/examples/file-and-s3-upload/remix.env.d.ts b/examples/file-and-s3-upload/remix.env.d.ts deleted file mode 100644 index 72e2affe311..00000000000 --- a/examples/file-and-s3-upload/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/examples/file-and-s3-upload/sandbox.config.json b/examples/file-and-s3-upload/sandbox.config.json deleted file mode 100644 index 4363d87a30d..00000000000 --- a/examples/file-and-s3-upload/sandbox.config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "hardReloadOnChange": true, - "container": { - "port": 3000 - } -} diff --git a/examples/file-and-s3-upload/tsconfig.json b/examples/file-and-s3-upload/tsconfig.json deleted file mode 100644 index 20f8a386a6c..00000000000 --- a/examples/file-and-s3-upload/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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 - } -} From 37d8ec8dd6d19ed51254eae2ec73b95fc10b5cb6 Mon Sep 17 00:00:00 2001 From: GSt4r <11350542+GSt4r@users.noreply.github.com> Date: Thu, 9 Jun 2022 23:13:23 +0200 Subject: [PATCH 3/5] S3 file upload (#3) * Implemented S3 upload * Updated README * Cleaned up dependencies --- examples/file-and-s3-upload/.env.sample | 4 ++ examples/file-and-s3-upload/.eslintrc.js | 3 + examples/file-and-s3-upload/.gitignore | 6 ++ examples/file-and-s3-upload/README.md | 35 +++++++++++ .../file-and-s3-upload/app/entry.client.tsx | 4 ++ .../file-and-s3-upload/app/entry.server.tsx | 21 +++++++ examples/file-and-s3-upload/app/root.tsx | 32 ++++++++++ .../app/routes/s3-upload.tsx | 59 ++++++++++++++++++ .../file-and-s3-upload/app/utils/s3.server.ts | 46 ++++++++++++++ examples/file-and-s3-upload/package.json | 28 +++++++++ .../file-and-s3-upload/public/favicon.ico | Bin 0 -> 16958 bytes examples/file-and-s3-upload/remix.config.js | 10 +++ examples/file-and-s3-upload/remix.env.d.ts | 2 + .../file-and-s3-upload/sandbox.config.json | 6 ++ examples/file-and-s3-upload/tsconfig.json | 22 +++++++ 15 files changed, 278 insertions(+) create mode 100644 examples/file-and-s3-upload/.env.sample create mode 100644 examples/file-and-s3-upload/.eslintrc.js create mode 100644 examples/file-and-s3-upload/.gitignore create mode 100644 examples/file-and-s3-upload/README.md create mode 100644 examples/file-and-s3-upload/app/entry.client.tsx create mode 100644 examples/file-and-s3-upload/app/entry.server.tsx create mode 100644 examples/file-and-s3-upload/app/root.tsx create mode 100644 examples/file-and-s3-upload/app/routes/s3-upload.tsx create mode 100644 examples/file-and-s3-upload/app/utils/s3.server.ts create mode 100644 examples/file-and-s3-upload/package.json create mode 100644 examples/file-and-s3-upload/public/favicon.ico create mode 100644 examples/file-and-s3-upload/remix.config.js create mode 100644 examples/file-and-s3-upload/remix.env.d.ts create mode 100644 examples/file-and-s3-upload/sandbox.config.json create mode 100644 examples/file-and-s3-upload/tsconfig.json diff --git a/examples/file-and-s3-upload/.env.sample b/examples/file-and-s3-upload/.env.sample new file mode 100644 index 00000000000..17b368d399e --- /dev/null +++ b/examples/file-and-s3-upload/.env.sample @@ -0,0 +1,4 @@ +STORAGE_ACCESS_KEY= +STORAGE_SECRET= +STORAGE_REGION= +STORAGE_BUCKET= \ No newline at end of file diff --git a/examples/file-and-s3-upload/.eslintrc.js b/examples/file-and-s3-upload/.eslintrc.js new file mode 100644 index 00000000000..ced78085f86 --- /dev/null +++ b/examples/file-and-s3-upload/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], +}; diff --git a/examples/file-and-s3-upload/.gitignore b/examples/file-and-s3-upload/.gitignore new file mode 100644 index 00000000000..3f7bf98da3e --- /dev/null +++ b/examples/file-and-s3-upload/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/examples/file-and-s3-upload/README.md b/examples/file-and-s3-upload/README.md new file mode 100644 index 00000000000..36249dcd670 --- /dev/null +++ b/examples/file-and-s3-upload/README.md @@ -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) diff --git a/examples/file-and-s3-upload/app/entry.client.tsx b/examples/file-and-s3-upload/app/entry.client.tsx new file mode 100644 index 00000000000..3eec1fd0a02 --- /dev/null +++ b/examples/file-and-s3-upload/app/entry.client.tsx @@ -0,0 +1,4 @@ +import { RemixBrowser } from "@remix-run/react"; +import { hydrate } from "react-dom"; + +hydrate(, document); diff --git a/examples/file-and-s3-upload/app/entry.server.tsx b/examples/file-and-s3-upload/app/entry.server.tsx new file mode 100644 index 00000000000..5afa18235cc --- /dev/null +++ b/examples/file-and-s3-upload/app/entry.server.tsx @@ -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( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + status: responseStatusCode, + headers: responseHeaders, + }); +} diff --git a/examples/file-and-s3-upload/app/root.tsx b/examples/file-and-s3-upload/app/root.tsx new file mode 100644 index 00000000000..927a0f745df --- /dev/null +++ b/examples/file-and-s3-upload/app/root.tsx @@ -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 ( + + + + + + + + + + + + + ); +} diff --git a/examples/file-and-s3-upload/app/routes/s3-upload.tsx b/examples/file-and-s3-upload/app/routes/s3-upload.tsx new file mode 100644 index 00000000000..c9bedadfc3f --- /dev/null +++ b/examples/file-and-s3-upload/app/routes/s3-upload.tsx @@ -0,0 +1,59 @@ +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 { Form, useActionData } 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({ + error: "Something went wrong while uploading", + }); + } + return json({ + imgSrc, + imgDesc, + }); +}; + +export default function Index() { + const data = useActionData(); + return ( + <> +
+ + + + + +
+ {data?.errorMsg &&

{data.errorMsg}

} + {data?.imgSrc && ( + <> +
File has been uploaded to S3 and is available under the following URL (if the bucket has public access enabled):
+
{data.imgSrc}
+ {data.imgDesc + + + )} + + ); +} diff --git a/examples/file-and-s3-upload/app/utils/s3.server.ts b/examples/file-and-s3-upload/app/utils/s3.server.ts new file mode 100644 index 00000000000..5f755c83fff --- /dev/null +++ b/examples/file-and-s3-upload/app/utils/s3.server.ts @@ -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) => { + 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 +} diff --git a/examples/file-and-s3-upload/package.json b/examples/file-and-s3-upload/package.json new file mode 100644 index 00000000000..edd814eb782 --- /dev/null +++ b/examples/file-and-s3-upload/package.json @@ -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" + } +} diff --git a/examples/file-and-s3-upload/public/favicon.ico b/examples/file-and-s3-upload/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8830cf6821b354114848e6354889b8ecf6d2bc61 GIT binary patch literal 16958 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 EKbET!0{{R3 literal 0 HcmV?d00001 diff --git a/examples/file-and-s3-upload/remix.config.js b/examples/file-and-s3-upload/remix.config.js new file mode 100644 index 00000000000..260b82c7cb1 --- /dev/null +++ b/examples/file-and-s3-upload/remix.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ["**/.*"], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", +}; diff --git a/examples/file-and-s3-upload/remix.env.d.ts b/examples/file-and-s3-upload/remix.env.d.ts new file mode 100644 index 00000000000..72e2affe311 --- /dev/null +++ b/examples/file-and-s3-upload/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/file-and-s3-upload/sandbox.config.json b/examples/file-and-s3-upload/sandbox.config.json new file mode 100644 index 00000000000..4363d87a30d --- /dev/null +++ b/examples/file-and-s3-upload/sandbox.config.json @@ -0,0 +1,6 @@ +{ + "hardReloadOnChange": true, + "container": { + "port": 3000 + } +} diff --git a/examples/file-and-s3-upload/tsconfig.json b/examples/file-and-s3-upload/tsconfig.json new file mode 100644 index 00000000000..20f8a386a6c --- /dev/null +++ b/examples/file-and-s3-upload/tsconfig.json @@ -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 + } +} From f1b40fec0d19caf448a38bf37cf61be6a1bb490d Mon Sep 17 00:00:00 2001 From: GSt4r <11350542+GSt4r@users.noreply.github.com> Date: Thu, 9 Jun 2022 23:17:53 +0200 Subject: [PATCH 4/5] Update contributors.yml --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index e3e34e35e24..851ec208dc1 100644 --- a/contributors.yml +++ b/contributors.yml @@ -123,6 +123,7 @@ - gonzoscript - graham42 - GregBrimble +- GSt4r - guerra08 - gunners6518 - hadizz From 19c1495493139d83ea4fc9918da77eafc10ad213 Mon Sep 17 00:00:00 2001 From: GSt4r <11350542+GSt4r@users.noreply.github.com> Date: Thu, 21 Jul 2022 21:26:50 +0200 Subject: [PATCH 5/5] S3 file upload (#4) * Implemented S3 upload * Updated README * Updated README * Cleaned up dependencies * Fixed dependencies * Refactored from useActionData to useFetcher --- .../app/routes/s3-upload.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/file-and-s3-upload/app/routes/s3-upload.tsx b/examples/file-and-s3-upload/app/routes/s3-upload.tsx index c9bedadfc3f..e4dc02d8b1b 100644 --- a/examples/file-and-s3-upload/app/routes/s3-upload.tsx +++ b/examples/file-and-s3-upload/app/routes/s3-upload.tsx @@ -5,7 +5,7 @@ import { unstable_createMemoryUploadHandler as createMemoryUploadHandler, unstable_parseMultipartFormData as parseMultipartFormData, } from "@remix-run/node"; -import { Form, useActionData } from "@remix-run/react"; +import { useFetcher } from "@remix-run/react"; import { s3UploadHandler } from "~/utils/s3.server"; type ActionData = { @@ -25,7 +25,7 @@ export const action: ActionFunction = async ({ request }) => { console.log(imgDesc) if (!imgSrc) { return json({ - error: "Something went wrong while uploading", + errorMsg: "Something went wrong while uploading", }); } return json({ @@ -35,25 +35,26 @@ export const action: ActionFunction = async ({ request }) => { }; export default function Index() { - const data = useActionData(); + const fetcher = useFetcher(); return ( <> -
+ - - {data?.errorMsg &&

{data.errorMsg}

} - {data?.imgSrc && ( - <> +
+ {fetcher.type === "done" ? ( + fetcher.data.errorMsg ? ( +

{fetcher.data.errorMsg}

+ ) : ( + <>
File has been uploaded to S3 and is available under the following URL (if the bucket has public access enabled):
-
{data.imgSrc}
- {data.imgDesc - +
{fetcher.data.imgSrc}
+ {fetcher.data.imgDesc - )} + )) : null} ); }