Skip to content

Commit c7378c0

Browse files
Feat/R2 migration (#174)
1 parent 72b5dd2 commit c7378c0

File tree

17 files changed

+1684
-77
lines changed

17 files changed

+1684
-77
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { db, eq } from "db";
2+
import { userHackerData } from "db/schema";
3+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
4+
import { staticUploads } from "config";
5+
6+
export const S3 = new S3Client({
7+
region: "auto",
8+
endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID!}.r2.cloudflarestorage.com`,
9+
credentials: {
10+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
11+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
12+
},
13+
});
14+
15+
export async function migrateBlob() {
16+
const resumeData = await db.query.userHackerData.findMany({
17+
columns: { resume: true, clerkID: true },
18+
});
19+
20+
for (let resumeEntry of resumeData) {
21+
const { resume: resumeUrlAsString, clerkID: userID } = resumeEntry;
22+
if (!resumeUrlAsString.length) continue;
23+
24+
const resumeUrl = new URL(resumeUrlAsString);
25+
const resumeFetchResponse = await fetch(resumeUrl);
26+
27+
if (!resumeFetchResponse.ok) {
28+
console.log("resume fetch failed");
29+
}
30+
31+
const key = "Migrated" + decodeURIComponent(resumeUrl.pathname);
32+
33+
const cmd = new PutObjectCommand({
34+
Key: key,
35+
Bucket: staticUploads.bucketName,
36+
ContentType: "application/pdf",
37+
});
38+
39+
S3.send(cmd);
40+
41+
// New url to correspond to an api route
42+
const newResumeUrl = `/api/upload/resume/view?key=${key}`;
43+
44+
await db
45+
.update(userHackerData)
46+
.set({ resume: newResumeUrl.toString() })
47+
.where(eq(userHackerData.clerkID, userID));
48+
}
49+
}

apps/infrastructure-migrator/driver.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { drizzle as pgDrizzle } from "drizzle-orm/vercel-postgres";
33
import { drizzle } from "drizzle-orm/libsql";
44
import * as pgSchema from "./schema";
55
import { createClient } from "@libsql/client";
6+
import { migrateBlob } from "./blob-mover";
67
export * from "drizzle-orm";
78
import dotenv from "dotenv";
89
import * as schema from "db/schema";
@@ -195,6 +196,12 @@ async function migratePostgresSqLite() {
195196

196197
console.log("Migrated Chats To Users ✅\n\n");
197198

199+
console.log("Migrating Vercel Blob Files To R2");
200+
201+
migrateBlob();
202+
203+
console.log("Migrated Vercel Blob Files To R2");
204+
198205
return process.exit(0);
199206
}
200207

apps/infrastructure-migrator/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"author": "",
1010
"license": "ISC",
1111
"dependencies": {
12+
"@aws-sdk/client-s3": "^3.758.0",
1213
"@libsql/client": "^0.14.0",
1314
"@vercel/postgres": "^0.9.0",
1415
"config": "workspace:*",

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"with-env": "dotenv -e ../../.env --"
1111
},
1212
"dependencies": {
13+
"@aws-sdk/client-s3": "^3.750.0",
1314
"@aws-sdk/client-ses": "^3.616.0",
15+
"@aws-sdk/s3-request-presigner": "^3.750.0",
1416
"@clerk/nextjs": "^4.29.4",
1517
"@gsap/react": "^2.1.1",
1618
"@hookform/resolvers": "^3.9.0",

apps/web/src/actions/registration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use server";
22
import { authenticatedAction } from "@/lib/safe-action";
33
import { db, sql } from "db";
4-
import { del } from "@vercel/blob";
4+
import { del } from "@/lib/utils/server/file-upload";
55
import z from "zod";
66
import { returnValidationErrors } from "next-safe-action";
77
import { hackerRegistrationFormValidator } from "@/validators/shared/registration";
@@ -87,6 +87,7 @@ export const registerHacker = authenticatedAction
8787
if (resume != null && resume != c.noResumeProvidedURL) {
8888
console.log(resume);
8989
console.log("deleting resume");
90+
9091
await del(resume);
9192
}
9293
if (

apps/web/src/actions/user-profile-mod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { z } from "zod";
55
import { db } from "db";
66
import { userCommonData, userHackerData } from "db/schema";
77
import { eq } from "db/drizzle";
8-
import { del } from "@vercel/blob";
8+
import { del } from "@/lib/utils/server/file-upload";
99
import { decodeBase64AsFile } from "@/lib/utils/shared/files";
1010
import { revalidatePath } from "next/cache";
1111
import { UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE } from "@/lib/constants";

apps/web/src/app/api/upload/pfp/route.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
1-
import { handleBlobUpload, type HandleBlobUploadBody } from "@vercel/blob";
1+
import { getPresignedUploadUrl } from "@/lib/utils/server/s3";
22
import { NextResponse } from "next/server";
33
import { auth } from "@clerk/nextjs";
4-
import c from "config";
4+
import { staticUploads } from "config";
55

6-
export async function POST(request: Request): Promise<NextResponse> {
7-
const body = (await request.json()) as HandleBlobUploadBody;
8-
const { userId } = await auth();
6+
interface RequestBody {
7+
location: string;
8+
fileName: string;
9+
}
910

11+
// TODO: Verify this route works with create team.
12+
export async function POST(request: Request): Promise<NextResponse> {
1013
try {
11-
const jsonResponse = await handleBlobUpload({
12-
body,
13-
request,
14-
onBeforeGenerateToken: async (pathname) => {
15-
// Step 1. Generate a client token for the browser to upload the file
16-
17-
// ⚠️ Authenticate users before allowing client tokens to be generated and sent to browsers. Otherwise, you're exposing your Blob store to be an anonymous upload platform.
18-
// See https://nextjs.org/docs/pages/building-your-application/routing/authenticating for more information
14+
const body: RequestBody = (await request.json()) as RequestBody;
1915

20-
if (!userId) {
21-
throw new Error("Not authenticated or bad pathname");
22-
}
16+
const { userId } = auth();
17+
if (!userId) {
18+
return new NextResponse(
19+
"You do not have permission to upload files",
20+
{
21+
status: 401,
22+
},
23+
);
24+
}
2325

24-
return {
25-
maximumSizeInBytes: c.maxProfilePhotoSizeInBytes, // optional, default and maximum is 500MB
26-
allowedContentTypes: ["image/jpeg", "image/png"], // optional, default is no restriction
27-
};
28-
},
29-
onUploadCompleted: async () => undefined,
30-
});
26+
const randomSeq = crypto.randomUUID();
27+
const [fileName, extension] = body.fileName.split(".");
28+
const key = `${body.location}/${fileName}-${randomSeq}.${extension}`;
29+
const url = await getPresignedUploadUrl(staticUploads.bucketName, key);
3130

32-
return NextResponse.json(jsonResponse);
31+
return NextResponse.json({ url, key });
3332
} catch (error) {
3433
return NextResponse.json(
3534
{ error: (error as Error).message },

apps/web/src/app/api/upload/resume/register/route.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,33 @@
1-
import { handleBlobUpload, type HandleBlobUploadBody } from "@vercel/blob";
1+
import { getPresignedUploadUrl } from "@/lib/utils/server/s3";
22
import { NextResponse } from "next/server";
33
import { auth } from "@clerk/nextjs";
4-
import c from "config";
4+
import { staticUploads } from "config";
55

6-
export async function POST(request: Request): Promise<NextResponse> {
7-
const body = (await request.json()) as HandleBlobUploadBody;
8-
const { userId } = await auth();
6+
interface RequestBody {
7+
location: string;
8+
fileName: string;
9+
}
910

11+
export async function POST(request: Request): Promise<NextResponse> {
1012
try {
11-
const jsonResponse = await handleBlobUpload({
12-
body,
13-
request,
14-
onBeforeGenerateToken: async (pathname) => {
15-
// Step 1. Generate a client token for the browser to upload the file
16-
17-
// ⚠️ Authenticate users before allowing client tokens to be generated and sent to browsers. Otherwise, you're exposing your Blob store to be an anonymous upload platform.
18-
// See https://nextjs.org/docs/pages/building-your-application/routing/authenticating for more information
13+
const body: RequestBody = (await request.json()) as RequestBody;
1914

20-
if (!userId) {
21-
throw new Error("Not authenticated or bad pathname");
22-
}
15+
const { userId } = auth();
16+
if (!userId) {
17+
return new NextResponse(
18+
"You do not have permission to upload files",
19+
{
20+
status: 401,
21+
},
22+
);
23+
}
2324

24-
return {
25-
maximumSizeInBytes: c.maxResumeSizeInBytes, // optional, default and maximum is 500MB
26-
allowedContentTypes: ["application/pdf"], // optional, default is no restriction
27-
};
28-
},
29-
onUploadCompleted: async () => undefined,
30-
});
25+
const randomSeq = crypto.randomUUID();
26+
const [fileName, extension] = body.fileName.split(".");
27+
const key = `${body.location}/${fileName}-${randomSeq}.${extension}`;
28+
const url = await getPresignedUploadUrl(staticUploads.bucketName, key);
3129

32-
return NextResponse.json(jsonResponse);
30+
return NextResponse.json({ url, key });
3331
} catch (error) {
3432
return NextResponse.json(
3533
{ error: (error as Error).message },
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { getPresignedViewingUrl } from "@/lib/utils/server/s3";
2+
import { redirect } from "next/navigation";
3+
import { staticUploads } from "config";
4+
import { auth } from "@clerk/nextjs";
5+
6+
export async function GET(request: Request) {
7+
const { userId } = auth();
8+
if (!userId) {
9+
return new Response("You must be logged in to access this resource", {
10+
status: 401,
11+
});
12+
}
13+
14+
const key = new URL(request.url).searchParams.get("key");
15+
if (!key) {
16+
return new Response(
17+
"Request must have a query parameter 'key' associated with it",
18+
{
19+
status: 400,
20+
},
21+
);
22+
}
23+
24+
const decodedKey = decodeURIComponent(key);
25+
26+
// Presign the url and return redirect to it.
27+
const presignedViewingUrl = await getPresignedViewingUrl(
28+
staticUploads.bucketName,
29+
decodedKey,
30+
);
31+
32+
return redirect(presignedViewingUrl);
33+
}
34+
35+
export const runtime = "edge";

apps/web/src/components/dash/main/team/NewTeam.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { Shell } from "lucide-react";
2121
import { useRouter } from "next/navigation";
2222
import { newTeamValidator } from "@/validators/shared/team";
2323
import c from "config";
24-
import { put } from "@vercel/blob";
24+
import { put } from "@/lib/utils/client/file-upload";
2525

2626
export default function NewTeamForm() {
2727
const formValidator = newTeamValidator.merge(
@@ -47,9 +47,9 @@ export default function NewTeamForm() {
4747
const photo = values.photo;
4848

4949
if (photo) {
50-
const { url } = await put(photo.name, photo, {
51-
access: "public",
52-
handleBlobUploadUrl: "/api/upload/pfp",
50+
// TODO: verify this works with the create team
51+
const url = await put(photo.name, photo, {
52+
presignHandlerUrl: "/api/upload/pfp",
5353
});
5454
teamPhotoURL = url;
5555
} else {

0 commit comments

Comments
 (0)