Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/Input_validators/ValidateNotesSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ const validateSummarizeNotes = (req, res, next) => {
}
next();
} catch (error) {
if (req.file && req.file.path) {
const safePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path));
require('fs').promises.unlink(safePath).catch((err) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (err.code !== 'ENOENT') console.error('Cleanup error:', err);
});
}
return handleValidationError(res, error);
}
};
Expand Down
6 changes: 6 additions & 0 deletions backend/Input_validators/ValidateResume.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ const validateAnalyzeResume = (req, res, next) => {
}
next();
} catch (error) {
if (req.file && req.file.path) {
const safePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path));
require('fs').promises.unlink(safePath).catch((err) => {
if (err.code !== 'ENOENT') console.error('Cleanup error:', err);
});
}
return handleValidationError(res, error);
}
};
Expand Down
18 changes: 15 additions & 3 deletions backend/controllers/notesSummaryController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const axios = require("axios");
const crypto = require("crypto");
const fs = require("fs");
const { generateWithFallback } = require("../utils/geminiHelper");
const {
inspectPdfBuffer,
Expand Down Expand Up @@ -103,14 +104,16 @@ function parseAiJson(raw) {
* @example
*/
const summarizeNotes = async (req, res) => {
let uploadedFilePath = null;
try {
let buffer;
let fileName;
let sourceType;
let sourceUrl = null;

if (req.file) {
buffer = req.file.buffer;
uploadedFilePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path));
buffer = await fs.promises.readFile(uploadedFilePath);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fileName = req.file.originalname;
sourceType = "upload";
} else {
Expand Down Expand Up @@ -139,6 +142,9 @@ const summarizeNotes = async (req, res) => {

const readingTime = computeReadingTime(pdfStats);
const contentHash = crypto.createHash("sha256").update(buffer).digest("hex");
const fileSize = buffer.length;
const base64Data = buffer.toString("base64");
buffer = null; // Release Buffer memory

const prompt = `You are an expert academic tutor helping a student decide whether a set of study notes is useful before they read it.

Expand Down Expand Up @@ -168,7 +174,7 @@ DO NOT wrap the response in markdown code blocks. Return ONLY the raw JSON objec
prompt,
{
inlineData: {
data: buffer.toString("base64"),
data: base64Data,
mimeType: "application/pdf",
},
},
Expand Down Expand Up @@ -202,7 +208,7 @@ DO NOT wrap the response in markdown code blocks. Return ONLY the raw JSON objec
res.status(200).json({
success: true,
fileName,
fileSize: buffer.length,
fileSize,
sourceType,
sourceUrl,
pageCount: pdfStats.numPages,
Expand Down Expand Up @@ -238,6 +244,12 @@ DO NOT wrap the response in markdown code blocks. Return ONLY the raw JSON objec
}

res.status(500).json({ success: false, message: "Failed to summarize notes." });
} finally {
if (uploadedFilePath) {
await require('fs').promises.unlink(uploadedFilePath).catch(err => {
if (err.code !== 'ENOENT') console.error("Failed to delete temp notes PDF:", err);
});
}
Comment on lines +247 to +252

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | πŸ—οΈ Heavy lift

Make temporary-file cleanup cover the complete post-upload lifecycle.

The controller finally blocks do not run when earlier route middleware returns a validation error. Those files remain on disk and can create a disk-exhaustion DoS.

  • backend/controllers/notesSummaryController.js#L244-L247: clean the file when validateSummarizeNotes short-circuits before summarizeNotes.
  • backend/controllers/resumeController.js#L169-L172: clean the file when validateResumeMagicBytes or validateAnalyzeResume short-circuits before analyzeResume.
πŸ“ Affects 2 files
  • backend/controllers/notesSummaryController.js#L244-L247 (this comment)
  • backend/controllers/resumeController.js#L169-L172
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/notesSummaryController.js` around lines 244 - 247, Extend
temporary-file cleanup beyond the controller finally blocks so files are removed
when validation middleware short-circuits: update
backend/controllers/notesSummaryController.js lines 244-247 around
summarizeNotes/validateSummarizeNotes, and
backend/controllers/resumeController.js lines 169-172 around
analyzeResume/validateResumeMagicBytes/validateAnalyzeResume. Ensure each
uploaded temporary file is deleted exactly once across both validation-error and
normal post-upload paths, preserving the existing error handling for unlink
failures.

}
};

Expand Down
16 changes: 15 additions & 1 deletion backend/controllers/resumeController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const { generateWithFallback } = require('../utils/geminiHelper');

/**
Expand Down Expand Up @@ -100,13 +101,20 @@ const compileResume = async (req, res) => {
* }
*/
const analyzeResume = async (req, res) => {
let uploadedFilePath = null;
try {
if (!req.file) {
return res.status(400).json({ message: "No resume file uploaded" });
}

uploadedFilePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path));
let fileBuffer = await fs.promises.readFile(uploadedFilePath);

const targetRole = req.body.targetRole || "General Professional";

const base64Data = fileBuffer.toString("base64");
fileBuffer = null; // Release Buffer memory
Comment on lines +111 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸš€ Performance & Scalability | 🟠 Major | πŸ—οΈ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 6 'readFile|inlineData|uploadFile|fileSize|uploadResume|uploadNotes' backend

Repository: Canopus-Labs/PrepPilot

Length of output: 15047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- upload limits and resume route ---'
sed -n '1,210p' backend/middlewares/uploadMiddleware.js
sed -n '1,80p' backend/routes/resumeRoutes.js

printf '%s\n' '--- resume controller and fallback helper references ---'
sed -n '90,165p' backend/controllers/resumeController.js
rg -n -C 8 'function generateWithFallback|const generateWithFallback|generateWithFallback|aiLimiter' backend

Repository: Canopus-Labs/PrepPilot

Length of output: 38065


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- fallback implementation ---'
sed -n '1,120p' backend/utils/geminiHelper.js

printf '%s\n' '--- controller cleanup and response path ---'
sed -n '150,245p' backend/controllers/resumeController.js

printf '%s\n' '--- exact memory multipliers at the configured resume limit ---'
python3 - <<'PY'
size = 5 * 1024 * 1024
base64_size = 4 * ((size + 2) // 3)
print({
    "resume_limit_bytes": size,
    "base64_bytes": base64_size,
    "peak_buffer_plus_base64_bytes": size + base64_size,
    "peak_buffer_plus_base64_mib": (size + base64_size) / (1024 * 1024),
    "peak_for_15_concurrent_uploads_mib": 15 * (size + base64_size) / (1024 * 1024),
})
PY

Repository: Canopus-Labs/PrepPilot

Length of output: 7990


Bound concurrent resume analyses before readFile().

uploadResume limits resumes to 5 MiB, not 15 MiB. Each active request can retain about 11.67 MiB for the Buffer and Base64 string, so 15 concurrent requests can retain about 175 MiB before SDK overhead. aiLimiter limits requests per hour, not in-flight analyses. Use file-backed or streaming Gemini input when supported; otherwise add a concurrency limit before readFile().

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/resumeController.js` around lines 111 - 116, Update
uploadResume so concurrent resume analyses are bounded before
fs.promises.readFile() allocates the file Buffer and Base64 string; use
supported file-backed or streaming Gemini input when available, otherwise
acquire an in-flight concurrency slot before readFile() and release it in all
success and error paths. Do not rely on aiLimiter, which only limits hourly
request volume.


// 2. Prompt Engineering
const prompt = `You are an expert ATS (Applicant Tracking System) and Senior Technical Recruiter.
Analyze the attached PDF resume against the target role: "${targetRole}".
Expand All @@ -133,7 +141,7 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw
prompt,
{
inlineData: {
data: req.file.buffer.toString("base64"),
data: base64Data,
mimeType: "application/pdf"
}
}
Expand Down Expand Up @@ -161,6 +169,12 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw
} catch (error) {
console.error("Resume Analysis Error:", error);
res.status(500).json({ message: "Failed to analyze resume" });
} finally {
if (uploadedFilePath) {
await require('fs').promises.unlink(uploadedFilePath).catch(err => {
if (err.code !== 'ENOENT') console.error("Failed to delete temp resume PDF:", err);
});
}
}
}

Expand Down
42 changes: 33 additions & 9 deletions backend/middlewares/uploadMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const { fileTypeFromBuffer } = require("file-type");
const os = require("os");
const crypto = require("crypto");

const MAX_FILE_SIZE = 5 * 1024 * 1024;

Expand Down Expand Up @@ -50,6 +51,7 @@ const IMAGE_EXTENSION_BY_MIME = {
* @returns {Promise<string|null>} Stored filename, or null if content is not an image.
*/
const resolveImageFileName = async (file) => {
const { fileTypeFromBuffer } = await import("file-type");
const fileType = await fileTypeFromBuffer(file.buffer);
const ext = fileType && IMAGE_EXTENSION_BY_MIME[fileType.mime];
if (!ext) return null;
Expand Down Expand Up @@ -120,23 +122,39 @@ const validateResumeMagicBytes = async (req, res, next) => {
}

try {
const fileType = await fileTypeFromBuffer(req.file.buffer);
const { fileTypeFromBuffer, fileTypeFromFile } = await import("file-type");
let fileType;
if (req.file.buffer) {
fileType = await fileTypeFromBuffer(req.file.buffer);
} else if (req.file.path) {
fileType = await fileTypeFromFile(req.file.path);
}

const allowedMimeTypes = new Set([
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
'application/pdf'
]);

if (!fileType || !allowedMimeTypes.has(fileType.mime)) {
if (req.file.path) {
const safePath = path.join(os.tmpdir(), path.basename(req.file.path));
fs.promises.unlink(safePath).catch((err) => {
if (err.code !== 'ENOENT') console.error('Cleanup error:', err);
});
}
return res.status(400).json({
success: false,
message: 'Invalid file type. Only PDF and Word documents (.pdf, .doc, .docx) are allowed.'
message: 'Invalid file type. Only PDF documents are allowed.'
});
}

next();
} catch (error) {
if (req.file && req.file.path) {
const safePath = path.join(os.tmpdir(), path.basename(req.file.path));
fs.promises.unlink(safePath).catch((err) => {
if (err.code !== 'ENOENT') console.error('Cleanup error:', err);
});
}
console.error('Error validating file type:', error);
return res.status(500).json({
success: false,
Expand All @@ -153,9 +171,12 @@ const upload = multer({
limits: { fileSize: MAX_FILE_SIZE },
});

// Upload instance for resumes (memory storage)
// Upload instance for resumes (disk storage)
const uploadResume = multer({
storage: multer.memoryStorage(),
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, os.tmpdir()),
filename: (req, file, cb) => cb(null, crypto.randomBytes(16).toString("hex") + ".pdf")
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fileFilter: resumeFileFilter,
limits: { fileSize: MAX_FILE_SIZE },
});
Expand All @@ -164,7 +185,10 @@ const uploadResume = multer({
const NOTES_MAX_FILE_SIZE = 15 * 1024 * 1024;

const uploadNotes = multer({
storage: multer.memoryStorage(),
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, os.tmpdir()),
filename: (req, file, cb) => cb(null, crypto.randomBytes(16).toString("hex") + ".pdf")
}),
fileFilter: resumeFileFilter, // PDF-only, same filter as resumes
limits: { fileSize: NOTES_MAX_FILE_SIZE },
});
Expand Down
Loading