Skip to content

Commit 1ecd37a

Browse files
committed
feat: migration of cloudinary to gcp for object storage
1 parent 12ffbbb commit 1ecd37a

11 files changed

Lines changed: 1778 additions & 1488 deletions

File tree

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@dnd-kit/core": "^6.3.1",
1414
"@dnd-kit/sortable": "^10.0.0",
1515
"@dnd-kit/utilities": "^3.2.2",
16+
"@google-cloud/storage": "^7.17.1",
1617
"@google/genai": "^0.7.0",
1718
"@radix-ui/react-accordion": "^1.2.4",
1819
"@radix-ui/react-dialog": "^1.1.7",
@@ -26,11 +27,12 @@
2627
"@remixicon/react": "^4.6.0",
2728
"@t3-oss/env-nextjs": "^0.10.1",
2829
"@types/mongoose": "^5.11.97",
30+
"@types/node": "^24.6.1",
2931
"@ungap/with-resolvers": "^0.1.0",
3032
"@upstash/ratelimit": "^2.0.5",
3133
"@vercel/kv": "^3.0.0",
3234
"axios": "^1.8.4",
33-
"canvas": "^3.1.0",
35+
"canvas": "^3.2.0",
3436
"class-variance-authority": "^0.7.1",
3537
"cloudinary": "^2.6.0",
3638
"clsx": "^2.1.1",
@@ -51,6 +53,7 @@
5153
"next-qrcode": "^2.5.1",
5254
"next-themes": "^0.3.0",
5355
"pdf-lib": "^1.17.1",
56+
"pdfjs-dist": "3.4.120",
5457
"prettier": "^3.5.3",
5558
"prettier-plugin-tailwindcss": "^0.6.11",
5659
"raw-loader": "^4.0.2",
@@ -67,6 +70,7 @@
6770
},
6871
"devDependencies": {
6972
"@types/eslint": "8.56.12",
73+
"@types/pdfjs-dist": "^2.10.378",
7074
"@types/react": "^18.3.20",
7175
"@types/react-beautiful-dnd": "^13.1.8",
7276
"@types/react-dom": "^18.3.6",

pnpm-lock.yaml

Lines changed: 1638 additions & 1251 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/api/signImage.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/app/api/upload/route.ts

Lines changed: 79 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,79 @@
11
import { NextResponse } from "next/server";
22
import { PDFDocument } from "pdf-lib";
33
import { connectToDatabase } from "@/lib/mongoose";
4-
import cloudinary from "cloudinary";
5-
import type { CloudinaryUploadResult } from "@/interface";
64
import { PaperAdmin } from "@/db/papers";
5+
import { Storage } from "@google-cloud/storage";
6+
7+
interface GCPCredentials {
8+
type: string;
9+
project_id: string;
10+
private_key_id: string;
11+
private_key: string;
12+
client_email: string;
13+
client_id: string;
14+
auth_uri: string;
15+
token_uri: string;
16+
auth_provider_x509_cert_url: string;
17+
client_x509_cert_url: string;
18+
universe_domain?: string;
19+
}
720

8-
cloudinary.v2.config({
9-
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
10-
api_key: process.env.CLOUDINARY_API_KEY,
11-
api_secret: process.env.CLOUDINARY_SECRET,
12-
});
13-
14-
const config1 = {
15-
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME_1,
16-
api_key: process.env.CLOUDINARY_API_KEY_1,
17-
api_secret: process.env.CLOUDINARY_SECRET_1,
18-
};
21+
// Initialize GCP Storage
22+
const credentials: GCPCredentials = JSON.parse(
23+
process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON ?? "{}",
24+
) as GCPCredentials;
1925

20-
const config2 = {
21-
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME_2,
22-
api_key: process.env.CLOUDINARY_API_KEY_2,
23-
api_secret: process.env.CLOUDINARY_SECRET_2,
24-
};
26+
const storage = new Storage({
27+
projectId: process.env.GOOGLE_CLOUD_PROJECT,
28+
credentials,
29+
});
2530

26-
const cloudinaryConfigs = [config1, config2];
31+
const bucketName = process.env.GOOGLE_CLOUD_BUCKET ?? "";
32+
const bucket = storage.bucket(bucketName);
2733

2834
export async function POST(req: Request) {
2935
try {
30-
if (!process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET) {
31-
return NextResponse.json(
32-
{ message: "ServerMisconfiguration" },
33-
{ status: 500 },
34-
);
35-
}
3636
await connectToDatabase();
37-
const count: number = await PaperAdmin.countDocuments();
38-
const configIndex = count % cloudinaryConfigs.length;
39-
console.log(configIndex);
40-
cloudinary.v2.config(cloudinaryConfigs[configIndex]);
4137

42-
const uploadPreset = process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET;
4338
const formData = await req.formData();
44-
const files: File[] = formData.getAll("files") as File[];
39+
const files = formData.getAll("files") as File[];
4540
const isPdf = formData.get("isPdf") === "true";
41+
const thumb = formData.get("thumbnail") as File | null;
4642

47-
let pdfData = "";
48-
49-
if (isPdf && files.length > 0 && files[0]) {
50-
const pdfFile = files[0];
51-
const pdfBytes = await pdfFile.arrayBuffer();
52-
const pdfBuffer = Buffer.from(pdfBytes);
53-
pdfData = pdfBuffer.toString("base64");
54-
} else if (files.length > 0) {
55-
const pdfBytes = await CreatePDF(files);
56-
const pdfBuffer = Buffer.from(pdfBytes);
57-
pdfData = pdfBuffer.toString("base64");
58-
}
43+
console.log("Received thumbnail:", thumb ? thumb.name : "NULL");
5944

60-
let final_url: string | undefined = "";
61-
let public_id_cloudinary: string | undefined = "";
6245

6346
if (!files || files.length === 0) {
64-
return NextResponse.json(
65-
{ error: "No files received." },
66-
{ status: 400 },
67-
);
47+
return NextResponse.json({ error: "No files received." }, { status: 400 });
6848
}
6949

70-
if (!isPdf) {
71-
try {
72-
if (!process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET) {
73-
return;
74-
}
75-
76-
const mergedPdfBytes = await CreatePDF(files);
77-
[public_id_cloudinary, final_url] = await uploadPDFFile(
78-
mergedPdfBytes,
79-
uploadPreset,
80-
);
81-
} catch (error) {
82-
console.error("Error creating PDF:", error);
83-
return NextResponse.json(
84-
{ error: "Failed to process PDF" },
85-
{ status: 500 },
86-
);
87-
}
50+
let pdfBytes: ArrayBuffer;
51+
if (isPdf && files[0]) {
52+
pdfBytes = await files[0].arrayBuffer();
8853
} else {
89-
[public_id_cloudinary, final_url] = await uploadPDFFile(
90-
files[0]!,
91-
uploadPreset,
92-
);
54+
pdfBytes = await createPDFfromImages(files);
9355
}
9456

95-
const paper = new PaperAdmin({
96-
cloudinary_index: configIndex,
57+
const { file_url, thumbnail_url } = await uploadToGCS(pdfBytes, thumb);
9758

98-
public_id_cloudinary,
99-
final_url,
100-
thumbnail_url: null,
59+
// Save in MongoDB
60+
const paper = new PaperAdmin({
61+
file_url,
62+
thumbnail_url,
10163
subject: null,
10264
slot: null,
10365
year: null,
10466
exam: null,
10567
semester: null,
106-
campus: null,
68+
campus: formData.get("campus"),
10769
ambiguous_tags: [],
10870
});
109-
11071
await paper.save();
111-
return NextResponse.json({ status: "success" }, { status: 201 });
72+
73+
return NextResponse.json(
74+
{ status: "success", file_url, thumbnail_url },
75+
{ status: 201 },
76+
);
11277
} catch (error) {
11378
console.error(error);
11479
return NextResponse.json(
@@ -118,63 +83,52 @@ export async function POST(req: Request) {
11883
}
11984
}
12085

121-
async function uploadPDFFile(file: File | ArrayBuffer, uploadPreset: string) {
122-
let bytes;
123-
if (file instanceof File) {
124-
bytes = await file.arrayBuffer();
125-
} else {
126-
bytes = file;
86+
async function uploadToGCS(bytes: ArrayBuffer, thumbFile?: File | null) {
87+
const buffer = Buffer.from(bytes);
88+
89+
const pdfFilename = `papers/${Date.now()}-${Math.random()
90+
.toString(36)
91+
.substring(2)}.pdf`;
92+
await bucket.file(pdfFilename).save(buffer, {
93+
resumable: false,
94+
contentType: "application/pdf",
95+
});
96+
const file_url = `https://storage.googleapis.com/${bucketName}/${pdfFilename}`;
97+
98+
let thumbnail_url: string | null = null;
99+
if (thumbFile) {
100+
const thumbBuffer = Buffer.from(await thumbFile.arrayBuffer());
101+
const thumbFilename = pdfFilename.replace(".pdf", ".png");
102+
await bucket.file(thumbFilename).save(thumbBuffer, {
103+
resumable: false,
104+
contentType: "image/png",
105+
});
106+
thumbnail_url = `https://storage.googleapis.com/${bucketName}/${thumbFilename}`;
127107
}
128-
return uploadFile(bytes, uploadPreset, "application/pdf");
129-
}
130-
131-
async function uploadFile(
132-
bytes: ArrayBuffer,
133-
uploadPreset: string,
134-
fileType: string,
135-
) {
136-
try {
137-
const buffer = Buffer.from(bytes);
138-
const dataUrl = `data:${fileType};base64,${buffer.toString("base64")}`;
139-
140-
const uploadResult = (await cloudinary.v2.uploader.unsigned_upload(
141-
dataUrl,
142-
uploadPreset,
143-
)) as CloudinaryUploadResult;
144108

145-
return [uploadResult.public_id, uploadResult.secure_url];
146-
} catch (e) {
147-
throw e;
148-
}
109+
return { file_url, thumbnail_url };
149110
}
150111

151-
async function CreatePDF(orderedFiles: File[]) {
112+
async function createPDFfromImages(files: File[]) {
152113
const pdfDoc = await PDFDocument.create();
153114

154-
for (const file of orderedFiles) {
155-
const fileBlob = new Blob([file]);
156-
const imgBytes = Buffer.from(await fileBlob.arrayBuffer());
115+
for (const file of files) {
116+
const imgBytes = Buffer.from(await file.arrayBuffer());
157117
let img;
158-
if (file instanceof File) {
159-
if (file.type === "image/png") {
160-
img = await pdfDoc.embedPng(imgBytes);
161-
} else if (file.type === "image/jpeg" || file.type === "image/jpg") {
162-
img = await pdfDoc.embedJpg(imgBytes);
163-
} else {
164-
continue;
165-
}
166-
const page = pdfDoc.addPage([img.width, img.height]);
167-
page.drawImage(img, {
168-
x: 0,
169-
y: 0,
170-
width: img.width,
171-
height: img.height,
172-
});
173-
}
118+
if (file.type === "image/png") {
119+
img = await pdfDoc.embedPng(imgBytes);
120+
} else if (file.type === "image/jpeg" || file.type === "image/jpg") {
121+
img = await pdfDoc.embedJpg(imgBytes);
122+
} else continue;
123+
124+
const page = pdfDoc.addPage([img.width, img.height]);
125+
page.drawImage(img, { x: 0, y: 0, width: img.width, height: img.height });
174126
}
175127

176128
const mergedPdfBytes = await pdfDoc.save();
177129
const ab = new ArrayBuffer(mergedPdfBytes.byteLength);
178130
new Uint8Array(ab).set(mergedPdfBytes);
179131
return ab;
180132
}
133+
134+
export const runtime = "nodejs";

src/app/paper/[id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ const PaperPage = async ({ params }: { params: { id: string } }) => {
165165
</h1>
166166
<center>
167167
<PdfViewer
168-
url={paper.final_url}
168+
url={paper.file_url}
169169
name={`${extractBracketContent(paper.subject)}-${paper.exam}-${paper.slot}-${paper.year}`}
170170
></PdfViewer>
171171
</center>

0 commit comments

Comments
 (0)