├── .env.copy ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ ├── pre-signed │ │ └── route.ts │ ├── temp-creds │ │ └── route.ts │ └── workers-api │ │ ├── download │ │ └── route.ts │ │ └── upload │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx ├── not-found.tsx ├── page.tsx └── styles.css ├── components.json ├── components ├── Button.tsx └── Upload.tsx ├── cors.json ├── env.d.ts ├── lib ├── r2.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── wrangler.toml /.env.copy: -------------------------------------------------------------------------------- 1 | ACCOUNT_ID='YOUR_ACCOUNT_ID' 2 | ACCESS_KEY_ID='YOUR_ACCESS_KEY_ID' 3 | SECRET_ACCESS_KEY='YOUR_SECRET_ACCESS_KEY' 4 | AUTH_TOKEN='YOUR_AUTH_TOKEN' -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:eslint-plugin-next-on-pages/recommended" 5 | ], 6 | "plugins": [ 7 | "eslint-plugin-next-on-pages" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # wrangler files 39 | .wrangler 40 | .dev.vars 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upload data to R2 2 | 3 | A [Next.js](https://nextjs.org/) example application demonstrating image uploading to [Cloudflare R2](https://developers.cloudflare.com/r2/). 4 | 5 | This is a Next.js project bootstrapped with [`c3`](https://developers.cloudflare.com/pages/get-started/c3). 6 | 7 | ## Upload Methods 8 | 9 | This app uses three different methods to upload and download images to an R2 bucket. 10 | 11 | 1. [Workers API](https://developers.cloudflare.com/r2/api/workers/): To use this method, make sure to add the R2 binding in [`wrangler.toml`](./wrangler.toml). 12 | 2. [Presigned URL](https://developers.cloudflare.com/r2/api/s3/presigned-urls/): To use this method, you need Account ID, Access Key ID, and Secret Access Key. Follow the [steps mentioned in the documentation](https://developers.cloudflare.com/r2/api/s3/tokens/) to generate the required credentials. 13 | 3. [Presigned URL using Temporary Credentials](https://developers.cloudflare.com/r2/api/s3/tokens/#temporary-access-credentials): To use this method, you need Account ID, Access Key ID, and Secret Access Key. You also need a personal API Token. Check the [Generating API Token](#configure-credentials-1) section to learn more. 14 | 15 | ## Getting Started 16 | 17 | ### Clone the repository 18 | 19 | Clone the repo on your local machine running the following command: 20 | 21 | ```bash 22 | git clone https://github.com/harshil1712/nextjs-r2-demo.git 23 | ``` 24 | 25 | ### Install dependencies 26 | 27 | Run the following command to install the required dependencies: 28 | 29 | ```bash 30 | npm run install 31 | ``` 32 | 33 | ### Access R2 34 | 35 | If you don't have access to R2, purchase it from your Cloudflare Dashboard. 36 | 37 | Create a new bucket. Follow the documentation to learn how to [create a new bucket](https://developers.cloudflare.com/r2/get-started/#2-create-a-bucket). 38 | 39 | ### For Workers API 40 | 41 | ### Add bindings 42 | 43 | Add the R2 bindings to the `wrangler.toml` file. It should look something like this: 44 | 45 | ```toml 46 | [[r2_buckets]] 47 | binding = "IMAGES" 48 | bucket_name = "my-bucket" 49 | ``` 50 | 51 | > If you update the value of `binding`, make sure you update it in the `env.d.ts`, `app/api/workers-api/upload` and `app/api/workers-api/download` files. 52 | 53 | ### For Presigned URL 54 | 55 | To use Presigned URL, you need the Account ID, Access Key ID, and Secret Access Key. Follow the [steps mentioned in the documentation](https://developers.cloudflare.com/r2/api/s3/tokens/) to generate the required credentials. 56 | 57 | #### Configure credentials 58 | 59 | Rename `.env.copy` to `.env.local`. Paste the credentials you got in the previous step. 60 | 61 | > For this method, you don't need `AUTH_TOKEN`. You leave it as is or remove it from the `.env.local` file. 62 | 63 | #### Configure CORS 64 | 65 | You will need to configure the CORS policies to be able to access the objects. Use the CORS policy available in the `cors.json` file. 66 | 67 | > **Note:** You might have to update `AllowedOrigins`. 68 | 69 | You add this CORS policy to your bucket via the Dashboard. You can find the steps to do that in [the documentation](https://developers.cloudflare.com/r2/buckets/cors/#add-cors-policies-from-the-dashboard). 70 | 71 | ### For Presigned URL with Temporary Credentials 72 | 73 | #### Configure credentials 74 | 75 | Apart from the Account ID, Access Key ID, and Secret Access Key, you also need an API Token. To create the API Token, follow the instructions mentioned in the [Create API Token documentation](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/). 76 | 77 | Make sure you have the following permissions: 78 | 79 | ```txt 80 | Account - Workers R2 Storage:Edit 81 | All users - API Tokens:Edit 82 | ``` 83 | 84 | Rename `.env.copy` to `.env.local`. Paste the credentials you got in the previous step. For `AUTH_TOKEN` paste the new generated `API Token`. 85 | 86 | #### Configure CORS 87 | 88 | You will need to configure the CORS policies to be able to access the objects. Use the CORS policy available in the `cors.json` file. 89 | 90 | > **Note:** You might have to update `AllowedOrigins`. 91 | 92 | You add this CORS policy to your bucket via the Dashboard. You can find the steps to do that in [the documentation](https://developers.cloudflare.com/r2/buckets/cors/#add-cors-policies-from-the-dashboard). 93 | 94 | ### Run development server 95 | 96 | Execute the following command to run the development server: 97 | 98 | ```bash 99 | npm run dev 100 | ``` 101 | 102 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 103 | 104 | ## Cloudflare integration 105 | 106 | Besides the `dev` script mentioned above `c3` has added a few extra scripts that allow you to integrate the application with the [Cloudflare Pages](https://pages.cloudflare.com/) environment, these are: 107 | 108 | - `pages:build` to build the application for Pages using the [`@cloudflare/next-on-pages`](https://github.com/cloudflare/next-on-pages) CLI 109 | - `preview` to locally preview your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI 110 | - `deploy` to deploy your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI 111 | 112 | > **Note:** while the `dev` script is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the [`@cloudflare/next-on-pages` recommended workflow](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md#recommended-workflow)) 113 | 114 | ### Bindings 115 | 116 | Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform. 117 | 118 | You can use bindings during development, when previewing locally your application and of course in the deployed application: 119 | 120 | - To use bindings in dev mode you need to define them in the `next.config.js` file under `setupDevBindings`, this mode uses the `next-dev` `@cloudflare/next-on-pages` submodule. For more details see its [documentation](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md). 121 | 122 | - To use bindings in the preview mode you need to add them to the `pages:preview` script accordingly to the `wrangler pages dev` command. For more details see its [documentation](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1) or the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). 123 | 124 | - To use bindings in the deployed application you will need to configure them in the Cloudflare [dashboard](https://dash.cloudflare.com/). For more details see the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). 125 | -------------------------------------------------------------------------------- /app/api/pre-signed/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | // import S3 from "@/lib/r2"; 3 | import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; 4 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 5 | import { S3Client } from "@aws-sdk/client-s3"; 6 | 7 | export const runtime = "edge"; 8 | 9 | const ACCOUNT_ID = process.env.ACCOUNT_ID as string; 10 | const ACCESS_KEY_ID = process.env.ACCESS_KEY_ID as string; 11 | const SECRET_ACCESS_KEY = process.env.SECRET_ACCESS_KEY as string; 12 | 13 | const S3 = new S3Client({ 14 | region: "auto", 15 | endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, 16 | credentials: { 17 | accessKeyId: ACCESS_KEY_ID, 18 | secretAccessKey: SECRET_ACCESS_KEY, 19 | }, 20 | }); 21 | 22 | // Get Pre-Signed URL for Upload 23 | export async function POST(request: NextRequest) { 24 | const { filename }: { filename: string } = await request.json(); 25 | 26 | try { 27 | const url = await getSignedUrl( 28 | S3, 29 | new PutObjectCommand({ 30 | Bucket: "BUCKET_NAME", 31 | Key: filename, 32 | }), 33 | { 34 | expiresIn: 600, 35 | } 36 | ); 37 | return Response.json({ url }); 38 | } catch (error: any) { 39 | return Response.json({ error: error.message }); 40 | } 41 | } 42 | 43 | // Get Pre-Signed URL for Download 44 | export async function GET(request: NextRequest) { 45 | const filename = request.nextUrl.searchParams.get("filename") as string; 46 | try { 47 | const url = await getSignedUrl( 48 | S3, 49 | new GetObjectCommand({ 50 | Bucket: "BUCKET_NAME", 51 | Key: filename, 52 | }), 53 | { 54 | expiresIn: 600, 55 | } 56 | ); 57 | return Response.json({ url }); 58 | } catch (error: any) { 59 | return Response.json({ error: error.message }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/api/temp-creds/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; 3 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 4 | import { S3Client } from "@aws-sdk/client-s3"; 5 | 6 | export const runtime = "edge"; 7 | 8 | const ACCOUNT_ID = process.env.ACCOUNT_ID as string; 9 | const ACCESS_KEY_ID = process.env.ACCESS_KEY_ID as string; 10 | const AUTH_TOKEN = process.env.AUTH_TOKEN as string; 11 | 12 | const BUCKET_NAME = ""; 13 | 14 | const generateS3Client = async () => { 15 | const getToken = await fetch( 16 | `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/r2/temp-access-credentials`, 17 | { 18 | method: "POST", 19 | headers: { 20 | Authorization: `Bearer ${AUTH_TOKEN}`, 21 | }, 22 | body: JSON.stringify({ 23 | bucket: BUCKET_NAME, 24 | parentAccessKeyId: ACCESS_KEY_ID, 25 | permission: "object-read-write", 26 | ttlSeconds: 6400, 27 | }), 28 | } 29 | ); 30 | const res = (await getToken.json()) as TempCredRes; 31 | if (res.success) { 32 | const { secretAccessKey, sessionToken } = res.result; 33 | return new S3Client({ 34 | region: "auto", 35 | endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, 36 | credentials: { 37 | accessKeyId: ACCESS_KEY_ID, 38 | secretAccessKey: secretAccessKey, 39 | sessionToken: sessionToken, 40 | }, 41 | }); 42 | } 43 | throw new Error("Fetching Session Token Failed"); 44 | }; 45 | 46 | // Get Pre-Signed URL for Upload 47 | export async function POST(request: NextRequest) { 48 | const { filename }: { filename: string } = await request.json(); 49 | const S3 = await generateS3Client(); 50 | 51 | try { 52 | const url = await getSignedUrl( 53 | S3, 54 | new PutObjectCommand({ 55 | Bucket: BUCKET_NAME, 56 | Key: filename, 57 | }), 58 | { 59 | expiresIn: 600, 60 | } 61 | ); 62 | return Response.json({ url }); 63 | } catch (error: any) { 64 | return Response.json({ error: error.message }); 65 | } 66 | } 67 | 68 | // Get Pre-Signed URL for Download 69 | export async function GET(request: NextRequest) { 70 | const filename = request.nextUrl.searchParams.get("filename") as string; 71 | const S3 = await generateS3Client(); 72 | try { 73 | const url = await getSignedUrl( 74 | S3, 75 | new GetObjectCommand({ 76 | Bucket: BUCKET_NAME, 77 | Key: filename, 78 | }), 79 | { 80 | expiresIn: 600, 81 | } 82 | ); 83 | return Response.json({ url }); 84 | } catch (error: any) { 85 | return Response.json({ error: error.message }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/api/workers-api/download/route.ts: -------------------------------------------------------------------------------- 1 | import { getRequestContext } from "@cloudflare/next-on-pages"; 2 | import type { NextRequest } from "next/server"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export async function GET(request: NextRequest) { 7 | const fileName = request.nextUrl.searchParams.get("filename") as string; 8 | try { 9 | const obj = await getRequestContext().env.IMAGES.get(fileName); 10 | 11 | if (obj === null) { 12 | return new Response("Object Not Found", { status: 404 }); 13 | } 14 | return new Response(obj.body); 15 | } catch (err) { 16 | console.log(err); 17 | return Response.json({ status: "error" }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/api/workers-api/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { getRequestContext } from "@cloudflare/next-on-pages"; 2 | import type { NextRequest } from "next/server"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export async function PUT(request: NextRequest) { 7 | const fileName = request.nextUrl.searchParams.get("filename") as string; 8 | 9 | const formData = await request.formData(); 10 | const file = formData.get("file"); 11 | try { 12 | const res = await getRequestContext().env.IMAGES.put(fileName, file); 13 | console.log(res); 14 | return Response.json({ status: "success" }); 15 | } catch (err) { 16 | console.log(err); 17 | return Response.json({ status: "error" }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshil1712/nextjs-r2-demo/232d38ed3ccf98b6c49e89d24bd6b54cd203c232/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | // This is the root layout component for your Next.js app. 2 | // Learn more: https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required 3 | 4 | import { Manrope } from "next/font/google"; 5 | import { DM_Sans } from "next/font/google"; 6 | import "./styles.css"; 7 | import { ReactNode } from "react"; 8 | import Link from "next/link"; 9 | 10 | const manrope = Manrope({ 11 | subsets: ["latin"], 12 | display: "swap", 13 | variable: "--font-manrope", 14 | }); 15 | const dm_sans = DM_Sans({ 16 | subsets: ["latin"], 17 | display: "swap", 18 | variable: "--font-dm_sans", 19 | }); 20 | 21 | export default function Layout({ children }: { children: ReactNode }) { 22 | return ( 23 | 24 | 25 | Next.js R2 Demo 26 | 27 | 35 | {children} 36 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export const runtime = "edge"; 2 | 3 | export default function NotFound() { 4 | return ( 5 | <> 6 | 404: This page could not be found. 7 |
8 |
9 |