diff --git a/backend/Input_validators/ValidateNotesSummary.js b/backend/Input_validators/ValidateNotesSummary.js index 970dff1b..fc754d25 100644 --- a/backend/Input_validators/ValidateNotesSummary.js +++ b/backend/Input_validators/ValidateNotesSummary.js @@ -24,7 +24,7 @@ const summarizeRequestSchema = z.object({ fileName: z.string().max(200).optional(), }); -const validateSummarizeNotes = (req, res, next) => { +const validateSummarizeNotes = async (req, res, next) => { try { req.body = summarizeRequestSchema.parse(req.body || {}); if (!req.file && !req.body.url) { @@ -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)); + await require('fs').promises.unlink(safePath).catch((err) => { + if (err.code !== 'ENOENT') console.error('Cleanup error:', err); + }); + } return handleValidationError(res, error); } }; diff --git a/backend/Input_validators/ValidateResume.js b/backend/Input_validators/ValidateResume.js index 7e5b0b20..0f45b8a5 100644 --- a/backend/Input_validators/ValidateResume.js +++ b/backend/Input_validators/ValidateResume.js @@ -45,7 +45,7 @@ const validateCompileResume = (req, res, next) => { }; // Middleware for analyzeResume -const validateAnalyzeResume = (req, res, next) => { +const validateAnalyzeResume = async (req, res, next) => { try { analyzeResumeSchema.parse(req.body); // also ensure file is uploaded @@ -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)); + await require('fs').promises.unlink(safePath).catch((err) => { + if (err.code !== 'ENOENT') console.error('Cleanup error:', err); + }); + } return handleValidationError(res, error); } }; diff --git a/backend/controllers/notesSummaryController.js b/backend/controllers/notesSummaryController.js index e8d6eac1..05335bb7 100644 --- a/backend/controllers/notesSummaryController.js +++ b/backend/controllers/notesSummaryController.js @@ -1,5 +1,6 @@ const axios = require("axios"); const crypto = require("crypto"); +const fs = require("fs"); const { generateWithFallback } = require("../utils/geminiHelper"); const { inspectPdfBuffer, @@ -103,6 +104,7 @@ function parseAiJson(raw) { * @example */ const summarizeNotes = async (req, res) => { + let uploadedFilePath = null; try { let buffer; let fileName; @@ -110,7 +112,8 @@ const summarizeNotes = async (req, res) => { 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); fileName = req.file.originalname; sourceType = "upload"; } else { @@ -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. @@ -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", }, }, @@ -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, @@ -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); + }); + } } }; diff --git a/backend/controllers/resumeController.js b/backend/controllers/resumeController.js index e4f8070c..d21b54d7 100644 --- a/backend/controllers/resumeController.js +++ b/backend/controllers/resumeController.js @@ -1,5 +1,6 @@ const axios = require('axios'); const FormData = require('form-data'); +const fs = require('fs'); const { generateWithFallback } = require('../utils/geminiHelper'); /** @@ -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 + // 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}". @@ -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" } } @@ -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); + }); + } } } diff --git a/backend/middlewares/uploadMiddleware.js b/backend/middlewares/uploadMiddleware.js index c64b7855..7a28ad71 100644 --- a/backend/middlewares/uploadMiddleware.js +++ b/backend/middlewares/uploadMiddleware.js @@ -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; @@ -50,6 +51,7 @@ const IMAGE_EXTENSION_BY_MIME = { * @returns {Promise} 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; @@ -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, @@ -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") + }), fileFilter: resumeFileFilter, limits: { fileSize: MAX_FILE_SIZE }, }); @@ -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 }, });