Files
sst_site/.history/routes/media_20251019160925.js
2025-10-19 18:27:00 +09:00

345 lines
9.0 KiB
JavaScript

const express = require('express');
const router = express.Router();
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs').promises;
// Authentication middleware
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
next();
};
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
const uploadPath = path.join(__dirname, '../public/uploads');
try {
await fs.mkdir(uploadPath, { recursive: true });
cb(null, uploadPath);
} catch (error) {
cb(error);
}
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB
files: 10
},
fileFilter: (req, file, cb) => {
// Allow images only
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
}
});
// Upload single image
router.post('/upload', requireAuth, upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
const originalPath = req.file.path;
const filename = req.file.filename;
const nameWithoutExt = path.parse(filename).name;
// Create optimized versions
const sizes = {
thumbnail: { width: 300, height: 200 },
medium: { width: 800, height: 600 },
large: { width: 1200, height: 900 }
};
const optimizedImages = {};
for (const [sizeName, dimensions] of Object.entries(sizes)) {
const outputPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-${sizeName}.webp`
);
await sharp(originalPath)
.resize(dimensions.width, dimensions.height, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 85 })
.toFile(outputPath);
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
}
// Keep original as well (converted to webp for better compression)
const originalWebpPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-original.webp`
);
await sharp(originalPath)
.webp({ quality: 90 })
.toFile(originalWebpPath);
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
// Remove the original uploaded file
await fs.unlink(originalPath);
res.json({
success: true,
message: 'Image uploaded and optimized successfully',
images: optimizedImages,
metadata: {
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size
}
});
} catch (error) {
console.error('Image upload error:', error);
// Clean up files on error
if (req.file && req.file.path) {
try {
await fs.unlink(req.file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
res.status(500).json({
success: false,
message: 'Error uploading image'
});
}
});
// Upload multiple images
router.post('/upload-multiple', requireAuth, 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'
});
}
const uploadedImages = [];
for (const file of req.files) {
try {
const originalPath = file.path;
const filename = file.filename;
const nameWithoutExt = path.parse(filename).name;
// Create optimized versions
const sizes = {
thumbnail: { width: 300, height: 200 },
medium: { width: 800, height: 600 },
large: { width: 1200, height: 900 }
};
const optimizedImages = {};
for (const [sizeName, dimensions] of Object.entries(sizes)) {
const outputPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-${sizeName}.webp`
);
await sharp(originalPath)
.resize(dimensions.width, dimensions.height, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 85 })
.toFile(outputPath);
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
}
// Original as webp
const originalWebpPath = path.join(
path.dirname(originalPath),
`${nameWithoutExt}-original.webp`
);
await sharp(originalPath)
.webp({ quality: 90 })
.toFile(originalWebpPath);
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
uploadedImages.push({
originalName: file.originalname,
images: optimizedImages,
metadata: {
mimeType: file.mimetype,
size: file.size
}
});
// Remove original file
await fs.unlink(originalPath);
} catch (fileError) {
console.error(`Error processing file ${file.originalname}:`, fileError);
// Clean up this file and continue with others
try {
await fs.unlink(file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
}
res.json({
success: true,
message: `${uploadedImages.length} images uploaded and optimized successfully`,
images: uploadedImages
});
} catch (error) {
console.error('Multiple images upload error:', error);
// Clean up files on error
if (req.files) {
for (const file of req.files) {
try {
await fs.unlink(file.path);
} catch (unlinkError) {
console.error('Error removing file:', unlinkError);
}
}
}
res.status(500).json({
success: false,
message: 'Error uploading images'
});
}
});
// Delete image
router.delete('/:filename', requireAuth, async (req, res) => {
try {
const filename = req.params.filename;
const uploadPath = path.join(__dirname, '../public/uploads');
// Security check - ensure filename doesn't contain path traversal
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
return res.status(400).json({
success: false,
message: 'Invalid filename'
});
}
const filePath = path.join(uploadPath, filename);
try {
await fs.access(filePath);
await fs.unlink(filePath);
res.json({
success: true,
message: 'Image deleted successfully'
});
} catch (error) {
if (error.code === 'ENOENT') {
return res.status(404).json({
success: false,
message: 'Image not found'
});
}
throw error;
}
} catch (error) {
console.error('Image deletion error:', error);
res.status(500).json({
success: false,
message: 'Error deleting image'
});
}
});
// List uploaded images
router.get('/list', requireAuth, async (req, res) => {
try {
const uploadPath = path.join(__dirname, '../public/uploads');
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const files = await fs.readdir(uploadPath);
const imageFiles = files.filter(file =>
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
);
const total = imageFiles.length;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedFiles = imageFiles.slice(start, end);
const imagesWithStats = await Promise.all(
paginatedFiles.map(async (file) => {
try {
const filePath = path.join(uploadPath, file);
const stats = await fs.stat(filePath);
return {
filename: file,
url: `/uploads/${file}`,
size: stats.size,
modified: stats.mtime,
isImage: true
};
} catch (error) {
console.error(`Error getting stats for ${file}:`, error);
return null;
}
})
);
const validImages = imagesWithStats.filter(img => img !== null);
res.json({
success: true,
images: validImages,
pagination: {
current: page,
total: totalPages,
limit,
totalItems: total,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
} catch (error) {
console.error('List images error:', error);
res.status(500).json({
success: false,
message: 'Error listing images'
});
}
});
module.exports = router;