From 0d6d059f5bcd6427cc4b0d682c6a74dcffb6e2ec Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 10 Sep 2026 00:41:16 +0300 Subject: [PATCH] Add JFIF to JPG conversion for student photo uploads --- server.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/server.js b/server.js index 02aa03b..b41ae67 100644 --- a/server.js +++ b/server.js @@ -190,7 +190,7 @@ function fixFilename(str) { } const BLOCKED_EXT = /\.(?:html?|js|mjs|cjs|svg|xml|json|map|wasm|php\d?|phtml|asp|aspx|jsp|sh|bat|cmd|cgi|exe|dll|com|msi|scr|hta|vbs|py|r|rb|htaccess)$/i; -const ALLOWED_IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.avif', '.ico', '.heic', '.heif']); +const ALLOWED_IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.avif', '.ico', '.heic', '.heif', '.jfif']); const MAX_TOTAL_UPLOAD_BYTES = 30 * 1024 * 1024; const upload = multer({ @@ -209,8 +209,9 @@ const upload = multer({ fileFilter: (req, file, cb) => { file.originalname = fixFilename(file.originalname); const ext = path.extname(file.originalname).toLowerCase(); + const isImageExt = ALLOWED_IMAGE_EXT.has(ext); if (file.fieldname === 'photo') { - if (!file.mimetype || !file.mimetype.startsWith('image/') || !ALLOWED_IMAGE_EXT.has(ext)) { + if (!isImageExt) { return cb(new Error('Only images')); } } @@ -219,7 +220,7 @@ const upload = multer({ }, }); -const ADMIN_ALLOWED_EXT = new Set(['.pdf', '.doc', '.docx', '.txt', '.md', '.html', '.htm', '.zip', '.rar', '.7z', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.avif', '.heic', '.heif']); +const ADMIN_ALLOWED_EXT = new Set(['.pdf', '.doc', '.docx', '.txt', '.md', '.html', '.htm', '.zip', '.rar', '.7z', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.avif', '.heic', '.heif', '.jfif']); const adminUpload = multer({ storage: multer.diskStorage({ destination: (_, __, cb) => { @@ -264,11 +265,22 @@ function removeUpload(file) { safeUnlink(file && file.path); } -async function convertHeicPhoto(file) { +async function convertPhoto(file) { if (!file || !file.path) return; const ext = (path.extname(file.originalname || '') || '').toLowerCase(); - if (ext !== '.heic' && ext !== '.heif') return; + if (ext !== '.heic' && ext !== '.heif' && ext !== '.jfif') return; try { + if (ext === '.jfif') { + // JFIF is already JPEG, just rename to .jpg for consistency + const outName = `${path.basename(file.path, path.extname(file.path))}.jpg`; + const outPath = path.join(path.dirname(file.path), outName); + fs.renameSync(file.path, outPath); + file.path = outPath; + file.filename = outName; + file.originalname = outName; + return; + } + // HEIC/HEIF conversion const outName = `${path.basename(file.path, path.extname(file.path))}.jpg`; const outPath = path.join(path.dirname(file.path), outName); const jpeg = await heicConvert({ buffer: fs.readFileSync(file.path), format: 'JPEG', quality: 0.85 }); @@ -278,7 +290,7 @@ async function convertHeicPhoto(file) { file.filename = outName; file.originalname = outName; } catch (e) { - console.error('HEIC convert failed:', e); + console.error('Photo convert failed:', e); } } @@ -1431,7 +1443,7 @@ app.post('/api/groups/:id/photos', requireAuth, (req, res, next) => { groupPhotoUpload(req, res, async (err) => { if (err) { if (err.code === 'LIMIT_FILE_SIZE') return res.status(400).json({ error: 'Файл слишком большой (макс. 10 МБ)' }); - if (err.message === 'Only images') return res.status(400).json({ error: 'Фото: допустимы только изображения (jpg, png, gif, webp, bmp, avif, ico, heic, heif)' }); + if (err.message === 'Only images') return res.status(400).json({ error: 'Фото: допустимы только изображения (jpg, png, gif, webp, bmp, avif, ico, heic, heif, jfif)' }); if (err.message === 'Not allowed extension') return res.status(400).json({ error: 'Недопустимый тип файла (*.html, *.js, *.svg и т.п. запрещены)' }); return res.status(400).json({ error: 'Недопустимый файл' }); } @@ -1450,7 +1462,7 @@ app.post('/api/groups/:id/photos', requireAuth, (req, res, next) => { const { caption, taken_at } = req.body; if (!req.file) return res.status(400).json({ error: 'Файл обязателен' }); try { - await convertHeicPhoto(req.file); + await convertPhoto(req.file); const { rows } = await pool.query( `INSERT INTO group_photos (group_id, photo_path, caption, taken_at) VALUES ($1, $2, $3, $4) RETURNING *`, @@ -2124,7 +2136,7 @@ app.post('/api/entries', entryLimiter, (req, res, next) => { entryFields(req, res, (err) => { if (!err) return next(); if (err.code === 'LIMIT_FILE_SIZE') return res.status(400).json({ error: 'Файл слишком большой (макс. 10 МБ)' }); - if (err.message === 'Only images') return res.status(400).json({ error: 'Фото: допустимы только изображения (jpg, png, gif, webp, bmp, avif, ico, heic, heif)' }); + if (err.message === 'Only images') return res.status(400).json({ error: 'Фото: допустимы только изображения (jpg, png, gif, webp, bmp, avif, ico, heic, heif, jfif)' }); if (err.message === 'Not allowed extension') return res.status(400).json({ error: 'Недопустимый тип файла (*.html, *.js, *.svg и т.п. запрещены)' }); return res.status(400).json({ error: 'Недопустимый файл' }); }); @@ -2172,7 +2184,7 @@ app.post('/api/entries', entryLimiter, (req, res, next) => { return res.status(429).json({ error: `Уже ответили: подождите ${intervalMin} минут` }); } } - await convertHeicPhoto(photo); + await convertPhoto(photo); const photo_path = photo ? `/uploads/${photo.filename}` : null; const client = await pool.connect(); try {