Add JFIF to JPG conversion for student photo uploads

This commit is contained in:
dev
2026-09-10 00:41:16 +03:00
parent 63494f322b
commit 0d6d059f5b
+22 -10
View File
@@ -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 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 MAX_TOTAL_UPLOAD_BYTES = 30 * 1024 * 1024;
const upload = multer({ const upload = multer({
@@ -209,8 +209,9 @@ const upload = multer({
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
file.originalname = fixFilename(file.originalname); file.originalname = fixFilename(file.originalname);
const ext = path.extname(file.originalname).toLowerCase(); const ext = path.extname(file.originalname).toLowerCase();
const isImageExt = ALLOWED_IMAGE_EXT.has(ext);
if (file.fieldname === 'photo') { if (file.fieldname === 'photo') {
if (!file.mimetype || !file.mimetype.startsWith('image/') || !ALLOWED_IMAGE_EXT.has(ext)) { if (!isImageExt) {
return cb(new Error('Only images')); 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({ const adminUpload = multer({
storage: multer.diskStorage({ storage: multer.diskStorage({
destination: (_, __, cb) => { destination: (_, __, cb) => {
@@ -264,11 +265,22 @@ function removeUpload(file) {
safeUnlink(file && file.path); safeUnlink(file && file.path);
} }
async function convertHeicPhoto(file) { async function convertPhoto(file) {
if (!file || !file.path) return; if (!file || !file.path) return;
const ext = (path.extname(file.originalname || '') || '').toLowerCase(); const ext = (path.extname(file.originalname || '') || '').toLowerCase();
if (ext !== '.heic' && ext !== '.heif') return; if (ext !== '.heic' && ext !== '.heif' && ext !== '.jfif') return;
try { 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 outName = `${path.basename(file.path, path.extname(file.path))}.jpg`;
const outPath = path.join(path.dirname(file.path), outName); const outPath = path.join(path.dirname(file.path), outName);
const jpeg = await heicConvert({ buffer: fs.readFileSync(file.path), format: 'JPEG', quality: 0.85 }); 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.filename = outName;
file.originalname = outName; file.originalname = outName;
} catch (e) { } 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) => { groupPhotoUpload(req, res, async (err) => {
if (err) { if (err) {
if (err.code === 'LIMIT_FILE_SIZE') return res.status(400).json({ error: 'Файл слишком большой (макс. 10 МБ)' }); 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 и т.п. запрещены)' }); if (err.message === 'Not allowed extension') return res.status(400).json({ error: 'Недопустимый тип файла (*.html, *.js, *.svg и т.п. запрещены)' });
return res.status(400).json({ error: 'Недопустимый файл' }); 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; const { caption, taken_at } = req.body;
if (!req.file) return res.status(400).json({ error: 'Файл обязателен' }); if (!req.file) return res.status(400).json({ error: 'Файл обязателен' });
try { try {
await convertHeicPhoto(req.file); await convertPhoto(req.file);
const { rows } = await pool.query( const { rows } = await pool.query(
`INSERT INTO group_photos (group_id, photo_path, caption, taken_at) `INSERT INTO group_photos (group_id, photo_path, caption, taken_at)
VALUES ($1, $2, $3, $4) RETURNING *`, VALUES ($1, $2, $3, $4) RETURNING *`,
@@ -2124,7 +2136,7 @@ app.post('/api/entries', entryLimiter, (req, res, next) => {
entryFields(req, res, (err) => { entryFields(req, res, (err) => {
if (!err) return next(); if (!err) return next();
if (err.code === 'LIMIT_FILE_SIZE') return res.status(400).json({ error: 'Файл слишком большой (макс. 10 МБ)' }); 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 и т.п. запрещены)' }); if (err.message === 'Not allowed extension') return res.status(400).json({ error: 'Недопустимый тип файла (*.html, *.js, *.svg и т.п. запрещены)' });
return res.status(400).json({ error: 'Недопустимый файл' }); 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} минут` }); return res.status(429).json({ error: `Уже ответили: подождите ${intervalMin} минут` });
} }
} }
await convertHeicPhoto(photo); await convertPhoto(photo);
const photo_path = photo ? `/uploads/${photo.filename}` : null; const photo_path = photo ? `/uploads/${photo.filename}` : null;
const client = await pool.connect(); const client = await pool.connect();
try { try {