117 lines
3.8 KiB
JavaScript
117 lines
3.8 KiB
JavaScript
require('dotenv').config();
|
||
|
||
const http = require('http');
|
||
const url = require('url');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const {sendResponse, isSafePath, processImage} = require("./utils");
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
const IMAGE_BASE_DIR = process.env.IMAGE_BASE_DIR || '/app/images';
|
||
|
||
// 定义支持的图片扩展名
|
||
const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'tiff', 'tif']);
|
||
|
||
// MIME 类型映射
|
||
const MIME_TYPES = {
|
||
'jpg': 'image/jpeg',
|
||
'jpeg': 'image/jpeg',
|
||
'png': 'image/png',
|
||
'gif': 'image/gif',
|
||
'webp': 'image/webp',
|
||
'bmp': 'image/bmp',
|
||
'svg': 'image/svg+xml',
|
||
'tiff': 'image/tiff',
|
||
'tif': 'image/tiff',
|
||
'mp4': 'video/mp4',
|
||
'webm': 'video/webm',
|
||
'ogg': 'video/ogg',
|
||
'avi': 'video/x-msvideo',
|
||
'mov': 'video/quicktime',
|
||
'mp3': 'audio/mpeg',
|
||
'wav': 'audio/wav',
|
||
'pdf': 'application/pdf',
|
||
'txt': 'text/plain',
|
||
'html': 'text/html',
|
||
'css': 'text/css',
|
||
'js': 'application/javascript',
|
||
'json': 'application/json'
|
||
};
|
||
|
||
const server = http.createServer(async (req, res) => {
|
||
if (req.method !== 'GET') {
|
||
return sendResponse(res, 405, '仅支持GET请求');
|
||
}
|
||
|
||
try {
|
||
const parsedUrl = url.parse(req.url, true);
|
||
const requestPath = parsedUrl.pathname;
|
||
const query = parsedUrl.query;
|
||
|
||
// 过滤根路径请求(避免直接访问服务根目录)
|
||
if (requestPath === '/') {
|
||
return sendResponse(res, 200, '图片处理服务已启动!');
|
||
}
|
||
|
||
const requestedFilePath = requestPath.slice(1);
|
||
const fullImagePath = path.join(IMAGE_BASE_DIR, requestedFilePath);
|
||
|
||
// 安全检查:防止路径遍历
|
||
if (!isSafePath(fullImagePath, IMAGE_BASE_DIR)) {
|
||
return sendResponse(res, 403, '访问拒绝:非法路径(路径遍历攻击)');
|
||
}
|
||
|
||
if (!fs.existsSync(fullImagePath)) {
|
||
return sendResponse(res, 404, `文件不存在:${requestedFilePath}`);
|
||
}
|
||
|
||
// 获取文件扩展名
|
||
const ext = path.extname(fullImagePath).toLowerCase().slice(1);
|
||
const isImage = IMAGE_EXTENSIONS.has(ext);
|
||
const hasImageParams = query.q || query.w || query.width || query.h || query.height;
|
||
|
||
// 如果不是图片文件,或者图片文件但没有处理参数,直接返回原文件
|
||
if (!isImage || !hasImageParams) {
|
||
const fileStream = fs.createReadStream(fullImagePath);
|
||
|
||
// 设置正确的 Content-Type
|
||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||
res.writeHead(200, {'Content-Type': contentType});
|
||
|
||
// 流式传输文件
|
||
fileStream.pipe(res);
|
||
fileStream.on('error', (err) => {
|
||
sendResponse(res, 500, `读取文件失败:${err.message}`);
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 处理图片压缩和调整大小
|
||
const imageBuffer = await processImage(fullImagePath, query);
|
||
|
||
res.writeHead(200, {
|
||
'Content-Type': 'image/jpeg',
|
||
'Content-Length': imageBuffer.length,
|
||
'Cache-Control': 'public, max-age=86400' // 缓存1天
|
||
});
|
||
res.end(imageBuffer);
|
||
|
||
} catch (error) {
|
||
console.error('服务器异常:', error);
|
||
sendResponse(res, 500, `服务器内部错误:${error.message}`);
|
||
}
|
||
});
|
||
|
||
// ===================== 启动服务器 =====================
|
||
server.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`服务地址:http://0.0.0.0:${PORT}`);
|
||
});
|
||
|
||
// 捕获全局未处理异常
|
||
process.on('uncaughtException', (err) => {
|
||
console.error('全局未捕获异常:', err);
|
||
});
|
||
process.on('unhandledRejection', (reason) => {
|
||
console.error('全局未处理Promise拒绝:', reason);
|
||
});
|