// src/server.js - 容器化图像处理服务
const express = require('express');
const sharp = require('sharp');
const multer = require('multer');
const fs = require('fs').promises;
const path = require('path');
class ContainerizedImageProcessor {
constructor() {
this.app = express();
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
}
setupMiddleware() {
// 配置multer进行文件上传
const upload = multer({
dest: '/app/uploads',
limits: {
fileSize: 50 * 1024 * 1024, // 50MB限制
files: 10
},
fileFilter: (req, file, cb) => {
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'image/tiff'];
cb(null, allowedMimes.includes(file.mimetype));
}
});
this.app.use(express.json());
this.app.use('/upload', upload.array('images', 10));
}
setupRoutes() {
// 单图像处理
this.app.post('/process', async (req, res) => {
try {
const result = await this.processImages(req.files, req.body.options);
res.json({ success: true, results: result });
} catch (error) {
console.error('处理失败:', error);
res.status(500).json({ error: '处理失败', details: error.message });
}
});
// 健康检查端点
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
memory: process.memoryUsage(),
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
}
async processImages(files, options = {}) {
const {
formats = ['webp', 'avif'],
sizes = [400, 800, 1200],
quality = 80
} = options;
const results = [];
for (const file of files) {
try {
const processedVariants = await this.processImageFile(file, {
formats,
sizes,
quality
});
results.push({
original: file.originalname,
variants: processedVariants,
success: true
});
// 清理上传的文件
await fs.unlink(file.path);
} catch (error) {
console.error(`处理 ${file.originalname} 失败:`, error);
results.push({
original: file.originalname,
error: error.message,
success: false
});
}
}
return results;
}
async processImageFile(file, options) {
const { formats, sizes, quality } = options;
const variants = [];
const inputPath = file.path;
const baseName = path.parse(file.originalname).name;
// 获取图像元数据
const image = sharp(inputPath);
const metadata = await image.metadata();
for (const format of formats) {
for (const size of sizes) {
// 如果原始图像更小则跳过
if (metadata.width < size) continue;
const outputFilename = `${baseName}-${size}.${format}`;
const outputPath = path.join('/app/output', outputFilename);
try {
let pipeline = image.clone()
.resize(size, null, {
withoutEnlargement: true,
kernel: sharp.kernel.lanczos3
});
// 应用格式特定的优化
switch (format) {
case 'webp':
pipeline = pipeline.webp({ quality, effort: 4 });
break;
case 'avif':
pipeline = pipeline.avif({
quality: Math.max(quality - 15, 50),
effort: 4
});
break;
case 'jpeg':
case 'jpg':
pipeline = pipeline.jpeg({
quality,
progressive: true,
mozjpeg: true
});
break;
}
await pipeline.toFile(outputPath);
const stats = await fs.stat(outputPath);
variants.push({
format,
size,
filename: outputFilename,
fileSize: stats.size,
url: `/output/${outputFilename}`
});
} catch (error) {
console.warn(`生成 ${format} 格式 ${size}px 变体失败:`, error);
}
}
}
return variants;
}
setupErrorHandling() {
this.app.use((error, req, res, next) => {
console.error('未处理的错误:', error);
res.status(500).json({
error: '内部服务器错误'
});
});
// 优雅关闭处理
process.on('SIGTERM', async () => {
console.log('收到SIGTERM,正在优雅关闭');
process.exit(0);
});
}
start(port = 3000) {
this.app.listen(port, '0.0.0.0', () => {
console.log(`图像处理服务运行在端口 ${port}`);
});
}
}
// 启动服务
const processor = new ContainerizedImageProcessor();
processor.start();