feat:图片服务

This commit is contained in:
2026-02-13 17:46:13 +08:00
commit e0c08e8055
6 changed files with 1462 additions and 0 deletions

109
app.js Normal file
View File

@@ -0,0 +1,109 @@
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const sharp = require('sharp');
const PORT = process.env.PORT || 3000;
const IMAGE_BASE_DIR = process.env.IMAGE_BASE_DIR || '/app/images';
/**
* 安全检查:防止路径遍历攻击(如 ../../etc/passwd
* @param {string} targetPath 目标文件路径
* @param {string} baseDir 基础目录
* @returns {boolean} 是否合法
*/
function isSafePath(targetPath, baseDir) {
const resolvedTarget = path.resolve(targetPath).toLowerCase();
const resolvedBase = path.resolve(baseDir).toLowerCase();
return resolvedTarget.startsWith(resolvedBase);
}
/**
* 发送错误响应
* @param {http.ServerResponse} res 响应对象
* @param {number} statusCode 状态码
* @param {string} message 错误信息
*/
function sendError(res, statusCode, message) {
res.writeHead(statusCode, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(message);
}
const server = http.createServer(async (req, res) => {
if (req.method !== 'GET') {
return sendError(res, 405, '仅支持GET请求');
}
try {
const parsedUrl = url.parse(req.url, true);
const requestPath = parsedUrl.pathname;
const query = parsedUrl.query;
// 过滤根路径请求(避免直接访问服务根目录)
if (requestPath === '/') {
return sendError(res, 200, `
图片处理服务已启动!
使用示例:
1. 访问原图片http://localhost:${PORT}/a/b/c.png
2. 调整质量http://localhost:${PORT}/a/b/c.png?q=80
图片基础目录:${IMAGE_BASE_DIR}
`);
}
const requestedFilePath = requestPath.slice(1);
const fullImagePath = path.join(IMAGE_BASE_DIR, requestedFilePath);
// 安全检查:防止路径遍历
if (!isSafePath(fullImagePath, IMAGE_BASE_DIR)) {
return sendError(res, 403, '访问拒绝:非法路径(路径遍历攻击)');
}
if (!fs.existsSync(fullImagePath)) {
return sendError(res, 404, `图片不存在:${requestedFilePath}`);
}
if (!query.q) {
const fileStream = fs.createReadStream(fullImagePath);
const ext = path.extname(fullImagePath).slice(1); // 如 png/jpg
res.writeHead(200, { 'Content-Type': `image/${ext}` });
// 流式传输文件(适合大文件)
fileStream.pipe(res);
fileStream.on('error', (err) => {
sendError(res, 500, `读取文件失败:${err.message}`);
});
return;
}
const quality = parseInt(query.q);
if (isNaN(quality)) {
return sendError(res, 400, '质量参数q必须是数字1-100');
}
const finalQuality = Math.max(1, Math.min(100, quality));
const imageBuffer = await sharp(fullImagePath).rotate().jpeg({ quality: finalQuality }).toBuffer();
res.writeHead(200, {
'Content-Type': 'image/jpeg',
'Content-Length': imageBuffer.length
});
res.end(imageBuffer);
} catch (error) {
console.error('服务器异常:', error);
sendError(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);
});