Skip to content

Commit

Permalink
Add ability to store images to s3 bucket in backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Jweng88 committed Sep 23, 2024
1 parent 6b3b6af commit 45b78e3
Show file tree
Hide file tree
Showing 7 changed files with 1,900 additions and 285 deletions.
1,967 changes: 1,715 additions & 252 deletions question-service/package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion question-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@
"author": "",
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.1691.0",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongoose-id-autoincrement": "^1.0.4"
"mongoose-id-autoincrement": "^1.0.4",
"multer": "^1.4.5-lts.1",
"multer-s3": "^3.0.1",
"uuid": "^10.0.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/multer": "^1.4.12",
"@types/multer-s3": "^3.0.3",
"@types/node": "^20.12.7",
"@types/uuid": "^10.0.0",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
Expand Down
32 changes: 32 additions & 0 deletions question-service/src/config/s3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import AWS from "aws-sdk";
import dotenv from "dotenv";

dotenv.config();

// Configure AWS SDK
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION, // Ensure you use the right region
});

export const uploadToS3 = async (file: Express.Multer.File) => {
const params = {
Bucket: process.env.AWS_BUCKET_NAME as string, // S3 bucket name
Key: `${Date.now()}_${file.originalname}`, // Unique file name
Body: file.buffer, // File buffer from multer's memoryStorage
ContentType: file.mimetype,
};

const data = await s3.upload(params).promise();
return data.Location; // Returns the file URL after upload
};

export const deleteFromS3 = async (key: string) => {
const params = {
Bucket: process.env.AWS_BUCKET_NAME as string,
Key: key,
};

await s3.deleteObject(params).promise();
};
107 changes: 80 additions & 27 deletions question-service/src/controller/question-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { Request, Response } from "express";
import Question from "../model/question-model";
import { uploadToS3, deleteFromS3 } from "../config/s3";

export const questionController = {
// Create a question
Expand All @@ -26,7 +27,7 @@ export const questionController = {
return res.status(400).json({ error: "All fields are required." });
}

if (!templateCode || !testCases || !Array.isArray(testCases)) {
if (!templateCode || !testCases /*|| !Array.isArray(testCases)*/) {
return res
.status(400)
.json({ error: "Invalid input for template code or test cases" });
Expand All @@ -41,13 +42,29 @@ export const questionController = {
.json({ error: "A question with this title already exists." });
}

// Parse the testCases if they are sent as a JSON string
// if (typeof req.body.testCases === "string") {
// req.body.testCases = JSON.parse(req.body.testCases);
// }

// // Handle image upload if present
// let updatedDescription = description;
// if (req.files) {
// const files = req.files as Express.Multer.File[];
// for (const file of files) {
// const imageUrl = await uploadToS3(file);
// updatedDescription += `\n![Image](${imageUrl})`; // Append image URL at the end of description
// }
// }

const question = new Question({
title,
description,
category,
complexity,
templateCode,
testCases,
title: title,
// description: updatedDescription,
description: description,
category: category,
complexity: complexity,
templateCode: templateCode,
testCases: req.body.testCases,
});

const savedQuestion = await question.save();
Expand Down Expand Up @@ -159,24 +176,42 @@ export const questionController = {
return res.status(400).json({ error: "All fields are required." });
}

if (!templateCode || !testCases || !Array.isArray(testCases)) {
if (!templateCode /*|| !testCases || !Array.isArray(testCases)*/) {
return res
.status(400)
.json({ error: "Invalid input for template code or test cases" });
}

try {
const updatedQuestion = await Question.findOneAndUpdate(
{ question_id: id },
{ title, description, category, complexity, templateCode, testCases },
{ new: true } // Return the updated document
);

if (updatedQuestion) {
res.status(200).json(updatedQuestion);
} else {
res.status(404).json({ message: "Question not found" });
}
// const updatedQuestion = await Question.findOneAndUpdate(
// { question_id: id },
// { title, description, category, complexity, templateCode, testCases },
// { new: true } // Return the updated document
// );

const question = await Question.findOne({ question_id: id });
if (!question)
return res.status(404).json({ message: "Question not found" });

// let updatedDescription = description;
// if (req.files) {
// const files = req.files as Express.Multer.File[];
// for (const file of files) {
// const imageUrl = await uploadToS3(file);
// updatedDescription += `\n![Image](${imageUrl})`; // Append new image URL to description
// }
// }

question.title = title || question.title;
// question.description = updatedDescription || question.description;
question.description = description || question.description;
question.category = category || question.category;
question.complexity = complexity || question.complexity;
question.templateCode = templateCode || question.templateCode;
question.testCases = testCases || question.testCases;

await question.save();
res.status(200).json(question);
} catch (err) {
res
.status(500)
Expand All @@ -188,14 +223,32 @@ export const questionController = {
deleteQuestion: async (req: Request, res: Response) => {
try {
const { id } = req.params;
const deletedQuestion = await Question.findOneAndDelete({
question_id: id,
});
if (deletedQuestion) {
res.status(200).json({ message: "Question deleted successfully" });
} else {
res.status(404).json({ message: "Question not found" });
}
const question = await Question.findOne({ question_id: id });
if (!question)
return res.status(404).json({ message: "Question not found" });
// const deletedQuestion = await Question.findOneAndDelete({
// question_id: id,
// });

// // Extract image URLs from the description and delete them from S3
// const imageUrls = question.description
// .match(/!\[Image]\((.*?)\)/g)
// ?.map((img) => img.slice(9, -1));
// if (imageUrls) {
// for (const imageUrl of imageUrls) {
// const key = imageUrl.split("/").pop() as string;
// await deleteFromS3(key);
// }
// }

await question.delete();
res.status(200).json({ message: "Question deleted successfully" });

// if (deletedQuestion) {
// res.status(200).json({ message: "Question deleted successfully" });
// } else {
// res.status(404).json({ message: "Question not found" });
// }
} catch (err) {
res
.status(500)
Expand Down
45 changes: 45 additions & 0 deletions question-service/src/middleware/image-upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import multer from "multer";
import path from "path";

// Define storage settings
const storage = multer.memoryStorage(); // Store files in memory to directly upload to S3

const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const fileTypes = /jpeg|jpg|png/;
const extname = fileTypes.test(
path.extname(file.originalname).toLowerCase()
);
const mimetype = fileTypes.test(file.mimetype);
if (extname && mimetype) {
return cb(null, true);
} else {
cb(new Error("Only images are allowed"));
}
},
});

export default upload;

// import multer from "multer";
// import multerS3 from "multer-s3";
// import s3 from "../config/s3";

// // Set up the multer middleware with S3 storage
// const upload = multer({
// storage: multerS3({
// s3: s3,
// bucket: process.env.S3_BUCKET_NAME,
// acl: "public-read", // make the files publicly readable (optional)
// metadata: (req, file, cb) => {
// cb(null, { fieldName: file.fieldname });
// },
// key: (req, file, cb) => {
// cb(null, `questions/${Date.now().toString()}-${file.originalname}`);
// },
// }),
// limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB limit
// });

// export default upload;
5 changes: 3 additions & 2 deletions question-service/src/model/question-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ interface QuestionDocument extends Document {
category: string[];
complexity: string;
templateCode: string; // New field for the template code
testCases: TestCase[]; // New field for test cases (array of test cases)
// testCases: TestCase[]; // New field for test cases (array of test cases)
testCases: string[]; // New field for test cases (array of test cases)
}

const TestCaseSchema = new Schema<TestCase>({
Expand All @@ -44,7 +45,7 @@ const questionSchema: Schema = new Schema({
},
complexity: { type: String, required: true },
templateCode: { type: String, required: true }, // Adding template code
testCases: [{ type: [TestCaseSchema], required: true }], // Adding test cases
testCases: [{ type: [String], required: false }], // Adding test cases
});

// Middleware to auto-increment the question_id before saving
Expand Down
20 changes: 17 additions & 3 deletions question-service/src/routes/question-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
import { Router } from "express";
import { questionController } from "../controller/question-controller";
import authMiddleware from "../middleware/question-middleware";
import upload from "../middleware/image-upload";

const router = Router();

// Route to create a question
router.post("/", authMiddleware, questionController.createQuestion);
router.post(
"/",
// upload.array("images", 5),
questionController.createQuestion
);

// Route to get all questions
router.get("/", questionController.getAllQuestions);
Expand All @@ -23,9 +28,18 @@ router.get("/", questionController.getAllQuestions);
router.get("/:id", questionController.getQuestionById);

// Route to update a question
router.put("/:id", authMiddleware, questionController.updateQuestion);
router.put(
"/:id",
// authMiddleware,
// upload.array("images", 5),
questionController.updateQuestion
);

// Route to delete a question
router.delete("/:id", authMiddleware, questionController.deleteQuestion);
router.delete(
"/:id",
// authMiddleware,
questionController.deleteQuestion
);

export default router;

0 comments on commit 45b78e3

Please sign in to comment.