support HEIC/HEIF uploads: convert to JPEG via heic-convert; graceful multer errors for group photos
This commit is contained in:
@@ -4,6 +4,7 @@ const multer = require('multer');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const helmet = require('helmet');
|
||||
const bcrypt = require('bcrypt');
|
||||
const heicConvert = require('heic-convert');
|
||||
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
@@ -65,7 +66,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']);
|
||||
const ALLOWED_IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.avif', '.ico', '.heic', '.heif']);
|
||||
const MAX_TOTAL_UPLOAD_BYTES = 30 * 1024 * 1024;
|
||||
|
||||
const upload = multer({
|
||||
@@ -115,6 +116,24 @@ function removeUpload(file) {
|
||||
safeUnlink(file && file.path);
|
||||
}
|
||||
|
||||
async function convertHeicPhoto(file) {
|
||||
if (!file || !file.path) return;
|
||||
const ext = (path.extname(file.originalname || '') || '').toLowerCase();
|
||||
if (ext !== '.heic' && ext !== '.heif') return;
|
||||
try {
|
||||
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 });
|
||||
fs.writeFileSync(outPath, jpeg);
|
||||
safeUnlink(file.path);
|
||||
file.path = outPath;
|
||||
file.filename = outName;
|
||||
file.originalname = outName;
|
||||
} catch (e) {
|
||||
console.error('HEIC convert failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEntryFiles(entryId) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT photo_path AS p FROM entries WHERE id = $1
|
||||
@@ -785,10 +804,20 @@ app.get('/api/groups/:id/photos', requireAdmin, async (req, res) => {
|
||||
res.json({ photos: rows, total });
|
||||
});
|
||||
|
||||
app.post('/api/groups/:id/photos', requireAdmin, upload.single('photo'), async (req, res) => {
|
||||
const groupPhotoUpload = upload.single('photo');
|
||||
app.post('/api/groups/:id/photos', requireAdmin, (req, res, next) => {
|
||||
groupPhotoUpload(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 === 'Not allowed extension') return res.status(400).json({ error: 'Недопустимый тип файла (*.html, *.js, *.svg и т.п. запрещены)' });
|
||||
return res.status(400).json({ error: 'Недопустимый файл' });
|
||||
});
|
||||
}, async (req, res) => {
|
||||
const { caption, taken_at } = req.body;
|
||||
if (!req.file) return res.status(400).json({ error: 'Файл обязателен' });
|
||||
try {
|
||||
await convertHeicPhoto(req.file);
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO group_photos (group_id, photo_path, caption, taken_at)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||
@@ -1129,7 +1158,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)' });
|
||||
if (err.message === 'Only images') return res.status(400).json({ error: 'Фото: допустимы только изображения (jpg, png, gif, webp, bmp, avif, ico, heic, heif)' });
|
||||
if (err.message === 'Not allowed extension') return res.status(400).json({ error: 'Недопустимый тип файла (*.html, *.js, *.svg и т.п. запрещены)' });
|
||||
return res.status(400).json({ error: 'Недопустимый файл' });
|
||||
});
|
||||
@@ -1177,6 +1206,7 @@ app.post('/api/entries', entryLimiter, (req, res, next) => {
|
||||
return res.status(429).json({ error: `Уже ответили: подождите ${intervalMin} минут` });
|
||||
}
|
||||
}
|
||||
await convertHeicPhoto(photo);
|
||||
const photo_path = photo ? `/uploads/${photo.filename}` : null;
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user