more edits

This commit is contained in:
yotakii
2026-02-15 16:28:31 +03:00
parent 6417fd6b01
commit 2909b675a1
2 changed files with 138 additions and 280 deletions

View File

@@ -6,82 +6,72 @@ const fs = require('fs');
const sharp = require('sharp');
const adminAuth = require('../middleware/adminAuth');
// =======================
// Config (LOCAL + PROD)
// =======================
const UPLOAD_DIR = process.env.UPLOAD_DIR
? path.resolve(process.env.UPLOAD_DIR)
: path.resolve(__dirname, '..', 'uploads'); // local fallback
// --- Helpers ---
const getUploadsDir = () => {
const envDir = (process.env.UPLOADS_DIR || '').trim();
if (envDir) return path.isAbsolute(envDir) ? envDir : path.resolve(process.cwd(), envDir);
return path.resolve(__dirname, '..', 'uploads');
};
const PUBLIC_BASE = process.env.UPLOAD_PUBLIC_URL || '/uploads';
const uploadsDir = getUploadsDir();
const urlPrefix = (process.env.UPLOADS_URL_PREFIX || '/uploads').replace(/\/$/, '');
// Ensure uploads directory exists
if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Configure multer for file upload
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, UPLOAD_DIR);
},
destination: (req, file, cb) => cb(null, uploadsDir),
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname).toLowerCase() || '.jpg';
const ext = path.extname(file.originalname).toLowerCase();
const name = path
.basename(file.originalname, ext)
.replace(/[^a-z0-9]/gi, '-')
.toLowerCase()
.replace(/-+/g, '-')
.replace(/(^-|-$)/g, '');
.replace(/^-|-$/g, '')
.toLowerCase();
cb(null, `${name || 'image'}-${uniqueSuffix}${ext}`);
cb(null, `${name || 'file'}-${uniqueSuffix}${ext || ''}`);
},
});
const fileFilter = (req, file, cb) => {
// Accept images only
if (file.mimetype && file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
if (file.mimetype && file.mimetype.startsWith('image/')) return cb(null, true);
cb(new Error('Only image files are allowed'), false);
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
},
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
});
const optimizeImageInPlace = async (filePath, ext) => {
const lowerExt = (ext || '').toLowerCase();
async function optimizeImageIfPossible(filePath, extLower) {
if (extLower === '.gif') return;
// GIF: skip optimization (sharp may drop animation)
if (lowerExt === '.gif') return;
const tmpPath = `${filePath}.tmp`;
const tmpPath = filePath + '.tmp';
let pipeline = sharp(filePath).resize(1920, 1080, {
fit: 'inside',
withoutEnlargement: true,
});
const pipeline = sharp(filePath)
.rotate()
.resize(1920, 1080, {
fit: 'inside',
withoutEnlargement: true,
});
if (lowerExt === '.png') {
await pipeline.png({ compressionLevel: 9 }).toFile(tmpPath);
} else if (lowerExt === '.webp') {
await pipeline.webp({ quality: 85 }).toFile(tmpPath);
if (extLower === '.png') {
pipeline = pipeline.png({ quality: 85, compressionLevel: 9 });
} else if (extLower === '.webp') {
pipeline = pipeline.webp({ quality: 85 });
} else {
// jpg/jpeg + anything else
await pipeline.jpeg({ quality: 85 }).toFile(tmpPath);
pipeline = pipeline.jpeg({ quality: 85 });
}
await pipeline.toFile(tmpPath);
fs.unlinkSync(filePath);
fs.renameSync(tmpPath, filePath);
};
}
// @route POST /api/upload
// @desc Upload single or multiple images
@@ -89,40 +79,35 @@ const optimizeImageInPlace = async (filePath, ext) => {
router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded',
});
return res.status(400).json({ success: false, message: 'No files uploaded' });
}
const uploadedFiles = await Promise.all(
req.files.map(async (file) => {
try {
const ext = path.extname(file.filename);
await optimizeImageInPlace(file.path, ext);
const uploadedFiles = [];
return {
filename: file.filename,
originalName: file.originalname,
url: `${PUBLIC_BASE}/${file.filename}`,
size: fs.statSync(file.path).size,
mimeType: file.mimetype,
uploadedAt: new Date(),
};
} catch (error) {
console.error('Error processing image:', error);
return {
filename: file.filename,
originalName: file.originalname,
url: `${PUBLIC_BASE}/${file.filename}`,
size: file.size,
mimeType: file.mimetype,
uploadedAt: new Date(),
error: 'Optimization failed',
};
}
})
);
for (const file of req.files) {
try {
const extLower = path.extname(file.filename).toLowerCase();
await optimizeImageIfPossible(file.path, extLower);
uploadedFiles.push({
filename: file.filename,
originalName: file.originalname,
url: `${urlPrefix}/${file.filename}`,
mimeType: file.mimetype,
uploadedAt: new Date(),
});
} catch (e) {
console.error('Error processing image:', e);
uploadedFiles.push({
filename: file.filename,
originalName: file.originalname,
url: `${urlPrefix}/${file.filename}`,
mimeType: file.mimetype,
uploadedAt: new Date(),
error: 'Optimization failed',
});
}
}
res.json({
success: true,
@@ -131,10 +116,7 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({
success: false,
message: error.message || 'Error uploading files',
});
res.status(500).json({ success: false, message: error.message || 'Error uploading files' });
}
});
@@ -143,33 +125,26 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
// @access Private (Admin)
router.get('/list', adminAuth, async (req, res) => {
try {
const files = fs.readdirSync(UPLOAD_DIR);
const files = fs.readdirSync(uploadsDir);
const fileList = files
.filter((file) => !file.startsWith('.'))
.filter((f) => !f.startsWith('.'))
.map((filename) => {
const filePath = path.join(UPLOAD_DIR, filename);
const filePath = path.join(uploadsDir, filename);
const stats = fs.statSync(filePath);
return {
filename,
url: `${PUBLIC_BASE}/${filename}`,
url: `${urlPrefix}/${filename}`,
size: stats.size,
uploadedAt: stats.mtime,
};
})
.sort((a, b) => b.uploadedAt - a.uploadedAt);
res.json({
success: true,
data: { files: fileList, total: fileList.length },
});
res.json({ success: true, data: { files: fileList, total: fileList.length } });
} catch (error) {
console.error('List files error:', error);
res.status(500).json({
success: false,
message: 'Error listing files',
});
res.status(500).json({ success: false, message: 'Error listing files' });
}
});
@@ -180,35 +155,22 @@ router.delete('/:filename', adminAuth, async (req, res) => {
try {
const { filename } = req.params;
// Security check
// Security check: prevent path traversal
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
return res.status(400).json({
success: false,
message: 'Invalid filename',
});
return res.status(400).json({ success: false, message: 'Invalid filename' });
}
const filePath = path.join(UPLOAD_DIR, filename);
const filePath = path.join(uploadsDir, filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'File not found',
});
return res.status(404).json({ success: false, message: 'File not found' });
}
fs.unlinkSync(filePath);
res.json({
success: true,
message: 'File deleted successfully',
});
res.json({ success: true, message: 'File deleted successfully' });
} catch (error) {
console.error('Delete file error:', error);
res.status(500).json({
success: false,
message: 'Error deleting file',
});
res.status(500).json({ success: false, message: 'Error deleting file' });
}
});