feat:图片服务
This commit is contained in:
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 依赖
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# 测试报告
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# 编译后的输出
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# 环境变量文件
|
||||||
|
.env
|
||||||
|
.env.development
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# 日志文件
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# 操作系统生成的文件
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# 编辑器配置文件
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
/uploads
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com/
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --production
|
||||||
|
COPY app.js .
|
||||||
|
RUN mkdir -p /app/images
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV IMAGE_BASE_DIR=/app/images
|
||||||
|
ENV PORT=3000
|
||||||
|
CMD ["node", "app.js"]
|
||||||
109
app.js
Normal file
109
app.js
Normal 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);
|
||||||
|
});
|
||||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
image-service:
|
||||||
|
image: cxx/image-service:0.0.1
|
||||||
|
container_name: image-service
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- /path/to/images:/app/images
|
||||||
|
environment:
|
||||||
|
- IMAGE_BASE_DIR=/app/images
|
||||||
|
- PORT=3000
|
||||||
|
restart: unless-stopped
|
||||||
1280
package-lock.json
generated
Normal file
1280
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
package.json
Normal file
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "image-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"sharp": "^0.34.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user