├── .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 |
14 |
15 | 404
16 |
17 |
18 |
This page could not be found.
19 |
20 |
21 |
22 | >
23 | );
24 | }
25 |
26 | const styles = {
27 | error: {
28 | fontFamily:
29 | 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
30 | height: "100vh",
31 | textAlign: "center",
32 | display: "flex",
33 | flexDirection: "column",
34 | alignItems: "center",
35 | justifyContent: "center",
36 | },
37 |
38 | desc: {
39 | display: "inline-block",
40 | },
41 |
42 | h1: {
43 | display: "inline-block",
44 | margin: "0 20px 0 0",
45 | padding: "0 23px 0 0",
46 | fontSize: 24,
47 | fontWeight: 500,
48 | verticalAlign: "top",
49 | lineHeight: "49px",
50 | },
51 |
52 | h2: {
53 | fontSize: 14,
54 | fontWeight: 400,
55 | lineHeight: "49px",
56 | margin: 0,
57 | },
58 | } as const;
59 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Upload } from "@/components/Upload";
2 |
3 | export default function Home() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: var(--font-dm_sans), sans-serif;
7 | }
8 |
9 | h1,
10 | h2,
11 | h3,
12 | h4,
13 | h5,
14 | h6 {
15 | font-family: var(--font-manrope), sans-serif;
16 | }
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": false
11 | },
12 | "aliases": {
13 | "utils": "@/lib/utils",
14 | "components": "@/components"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import type { FormEventHandler } from "react";
2 |
3 | interface ButtonProps {
4 | text: string;
5 | submitHandler: FormEventHandler;
6 | }
7 |
8 | export default function Button({ text, submitHandler }: ButtonProps) {
9 | return (
10 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/Upload.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This code was generated by v0 by Vercel.
3 | * @see https://v0.dev/t/PQtgGPYs6Nt
4 | * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
5 | */
6 |
7 | "use client";
8 | import { ChangeEvent, FormEvent, useState } from "react";
9 | import { IconProps } from "@radix-ui/react-icons/dist/types";
10 | import Button from "./Button";
11 |
12 | export function Upload() {
13 | const [file, setFile] = useState();
14 | const [uploading, setUploading] = useState(false);
15 | const [message, setMessage] = useState("");
16 | const [preview, setPreview] = useState(null);
17 |
18 | const handleFileChange = async (event: ChangeEvent) => {
19 | if (event.target.files && event.target.files.length > 0) {
20 | const uploadedFile = event.target.files?.[0];
21 | setFile(uploadedFile);
22 | let reader = new FileReader();
23 |
24 | reader.onloadend = () => {
25 | setPreview(reader.result as string);
26 | };
27 | reader.readAsDataURL(uploadedFile);
28 | }
29 | setMessage("");
30 | };
31 |
32 | const handleWorkerApiSubmit = async (event: FormEvent) => {
33 | event.preventDefault();
34 | if (file) {
35 | setUploading(true);
36 | setMessage("Uploading...");
37 | const formData = new FormData();
38 | formData.append("file", file);
39 |
40 | try {
41 | const res = await fetch(
42 | `/api/workers-api/upload?filename=${file.name}`,
43 | {
44 | method: "PUT",
45 | body: formData,
46 | }
47 | );
48 |
49 | const result = (await res.json()) as { status: string };
50 | result.status === "success"
51 | ? setMessage("File Upload Successful")
52 | : setMessage("File Upload Failed");
53 | } catch (error) {
54 | setMessage("An error occured");
55 | } finally {
56 | setUploading(false);
57 | }
58 | } else {
59 | setMessage("Please select a file");
60 | }
61 | };
62 |
63 | const handlePreSignedUrlSubmit = async (
64 | event: FormEvent
65 | ) => {
66 | event.preventDefault();
67 | if (file) {
68 | setUploading(true);
69 | setMessage("Uploading...");
70 | try {
71 | // Fetch the Pre-Signed URL
72 | const res = await fetch("/api/pre-signed", {
73 | method: "POST",
74 | headers: {
75 | "Content-Type": "application/json",
76 | },
77 | body: JSON.stringify({ filename: file.name }),
78 | });
79 | if (res.ok) {
80 | const { url }: { url: string } = await res.json();
81 | const uploadRes = await fetch(url, {
82 | method: "PUT",
83 | body: file,
84 | });
85 | console.log(uploadRes);
86 | if (uploadRes.ok) {
87 | setMessage("File Upload Successful!");
88 | } else {
89 | setMessage("File Upload Failed");
90 | }
91 | } else {
92 | setMessage("Pre-Sign URL error");
93 | }
94 | } catch (error) {
95 | console.error(error);
96 | setMessage("An error occured");
97 | } finally {
98 | setUploading(false);
99 | }
100 | } else {
101 | setMessage("Please select a file");
102 | }
103 | };
104 |
105 | const handleTempCredsSubmit = async (event: FormEvent) => {
106 | event.preventDefault();
107 | if (file) {
108 | setUploading(true);
109 | setMessage("Uploading...");
110 | try {
111 | // Fetch the Pre-Signed URL
112 | const res = await fetch("/api/temp-creds", {
113 | method: "POST",
114 | headers: {
115 | "Content-Type": "application/json",
116 | },
117 | body: JSON.stringify({ filename: file.name }),
118 | });
119 | if (res.ok) {
120 | const { url }: { url: string } = await res.json();
121 | const uploadRes = await fetch(url, {
122 | method: "PUT",
123 | body: file,
124 | });
125 | console.log(uploadRes);
126 | if (uploadRes.ok) {
127 | setMessage("File Upload Successful!");
128 | } else {
129 | setMessage("File Upload Failed");
130 | }
131 | } else {
132 | setMessage("Pre-Sign URL error");
133 | }
134 | } catch (error) {
135 | console.error(error);
136 | setMessage("An error occured");
137 | } finally {
138 | setUploading(false);
139 | }
140 | } else {
141 | setMessage("Please select a file");
142 | }
143 | };
144 |
145 | return (
146 |
147 |
148 |
149 |
150 |
151 | Upload Image
152 |
153 |
199 |
202 |
203 |
204 |
205 |
206 | );
207 | }
208 |
209 | function UploadIcon(props: IconProps) {
210 | return (
211 |
227 | );
228 | }
229 |
--------------------------------------------------------------------------------
/cors.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "AllowedOrigins": [
4 | "http://localhost:3000"
5 | ],
6 | "AllowedMethods": [
7 | "GET",
8 | "PUT",
9 | "POST"
10 | ],
11 | "AllowedHeaders": [
12 | "*"
13 | ],
14 | "ExposeHeaders": [
15 | "ETag"
16 | ],
17 | "MaxAgeSeconds": 3000
18 | }
19 | ]
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by Wrangler
2 | // by running `wrangler types --env-interface CloudflareEnv env.d.ts`
3 |
4 | interface CloudflareEnv {
5 | SECRET_ACCESS_KEY: string;
6 | ACCESS_KEY_ID: string;
7 | ACCOUNT_ID: string;
8 | IMAGES: R2Bucket;
9 | }
10 |
11 | interface TempCredRes {
12 | success: boolean;
13 | errors: [];
14 | messages: [];
15 | result: {
16 | accessKeyId: string;
17 | secretAccessKey: string;
18 | sessionToken: string;
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/lib/r2.ts:
--------------------------------------------------------------------------------
1 | import {
2 | S3Client,
3 | } from "@aws-sdk/client-s3";
4 |
5 | const ACCOUNT_ID = process.env.ACCOUNT_ID as string;
6 | const ACCESS_KEY_ID = process.env.ACCESS_KEY_ID as string;
7 | const SECRET_ACCESS_KEY = process.env.SECRET_ACCESS_KEY as string;
8 |
9 |
10 | const S3 = new S3Client({
11 | region: "auto",
12 | endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
13 | credentials: {
14 | accessKeyId: ACCESS_KEY_ID,
15 | secretAccessKey: SECRET_ACCESS_KEY,
16 | },
17 | });
18 |
19 | export default S3;
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev';
2 |
3 | // Here we use the @cloudflare/next-on-pages next-dev module to allow us to use bindings during local development
4 | // (when running the application with `next dev`), for more information see:
5 | // https://github.com/cloudflare/next-on-pages/blob/5712c57ea7/internal-packages/next-dev/README.md
6 | if (process.env.NODE_ENV === 'development') {
7 | await setupDevPlatform();
8 | }
9 |
10 | /** @type {import('next').NextConfig} */
11 | const nextConfig = {};
12 |
13 | export default nextConfig;
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-r2-demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "pages:build": "npx @cloudflare/next-on-pages",
11 | "preview": "npm run pages:build && wrangler pages dev",
12 | "deploy": "npm run pages:build && wrangler pages deploy",
13 | "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
14 | },
15 | "dependencies": {
16 | "@aws-sdk/client-s3": "^3.600.0",
17 | "@aws-sdk/s3-presigned-post": "^3.600.0",
18 | "@aws-sdk/s3-request-presigner": "^3.600.0",
19 | "@radix-ui/react-collapsible": "^1.0.3",
20 | "@radix-ui/react-icons": "^1.3.0",
21 | "class-variance-authority": "^0.7.0",
22 | "clsx": "^2.1.1",
23 | "lucide-react": "^0.394.0",
24 | "next": "14.1.0",
25 | "react": "^18",
26 | "react-dom": "^18",
27 | "tailwind-merge": "^2.3.0",
28 | "tailwindcss-animate": "^1.0.7"
29 | },
30 | "devDependencies": {
31 | "@cloudflare/next-on-pages": "^1.11.3",
32 | "@cloudflare/workers-types": "^4.20240605.0",
33 | "@types/node": "^20",
34 | "@types/react": "^18",
35 | "@types/react-dom": "^18",
36 | "autoprefixer": "^10.0.1",
37 | "eslint": "^8",
38 | "eslint-config-next": "14.1.0",
39 | "eslint-plugin-next-on-pages": "^1.11.3",
40 | "postcss": "^8",
41 | "tailwindcss": "^3.3.0",
42 | "typescript": "^5",
43 | "vercel": "^34.2.7",
44 | "wrangler": "^3.60.2"
45 | }
46 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [require("tailwindcss-animate")],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | },
23 | "types": [
24 | "@cloudflare/workers-types/2023-07-01"
25 | ]
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | #:schema node_modules/wrangler/config-schema.json
2 | name = "talk-demo"
3 | compatibility_date = "2024-06-10"
4 | compatibility_flags = ["nodejs_compat"]
5 | pages_build_output_dir = ".vercel/output/static"
6 |
7 | # Automatically place your workloads in an optimal location to minimize latency.
8 | # If you are running back-end logic in a Pages Function, running it closer to your back-end infrastructure
9 | # rather than the end user may result in better performance.
10 | # Docs: https://developers.cloudflare.com/pages/functions/smart-placement/#smart-placement
11 | # [placement]
12 | # mode = "smart"
13 |
14 | # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
15 | # Docs:
16 | # - https://developers.cloudflare.com/pages/functions/bindings/#environment-variables
17 | # Note: Use secrets to store sensitive data.
18 | # - https://developers.cloudflare.com/pages/functions/bindings/#secrets
19 | # [vars]
20 | # MY_VARIABLE = "production_value"
21 |
22 | # Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network
23 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai
24 | # [ai]
25 | # binding = "AI"
26 |
27 | # Bind a D1 database. D1 is Cloudflare’s native serverless SQL database.
28 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases
29 | # [[d1_databases]]
30 | # binding = "MY_DB"
31 | # database_name = "my-database"
32 | # database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
33 |
34 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
35 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
36 | # Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects
37 | # [[durable_objects.bindings]]
38 | # name = "MY_DURABLE_OBJECT"
39 | # class_name = "MyDurableObject"
40 | # script_name = 'my-durable-object'
41 |
42 | # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
43 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces
44 | # KV Example:
45 | # [[kv_namespaces]]
46 | # binding = "MY_KV_NAMESPACE"
47 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
48 |
49 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.
50 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers
51 | # [[queues.producers]]
52 | # binding = "MY_QUEUE"
53 | # queue = "my-queue"
54 |
55 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.
56 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets
57 | # [[r2_buckets]]
58 | # binding = "MY_BUCKET"
59 | # bucket_name = "my-bucket"
60 |
61 | # Bind another Worker service. Use this binding to call another Worker without network overhead.
62 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings
63 | # [[services]]
64 | # binding = "MY_SERVICE"
65 | # service = "my-service"
66 |
67 | # To use different bindings for preview and production environments, follow the examples below.
68 | # When using environment-specific overrides for bindings, ALL bindings must be specified on a per-environment basis.
69 | # Docs: https://developers.cloudflare.com/pages/functions/wrangler-configuration#environment-specific-overrides
70 |
71 | ######## PREVIEW environment config ########
72 |
73 | [env.preview.vars]
74 | ACCOUNT_ID='a67e14daa5f8dceeb91fe5449ba496eb'
75 | ACCESS_KEY_ID='0592a467415628ac03e8a10a0af6d564'
76 | SECRET_ACCESS_KEY='0b7251a05c11d98be9aab501551e5dc1c82942fe75729ec6e90c27efa4b35abe'
77 |
78 | # [[env.preview.kv_namespaces]]
79 | # binding = "MY_KV_NAMESPACE"
80 | # id = ""
81 |
82 | ######## PRODUCTION environment config ########
83 |
84 | # [env.production.vars]
85 | # API_KEY = "abc123"
86 |
87 | # [[env.production.kv_namespaces]]
88 | # binding = "MY_KV_NAMESPACE"
89 | # id = ""
90 |
--------------------------------------------------------------------------------