Files
rag/api/image_routes.py
lacerate551 90b915232a fix(security): 代码审查安全加固 — 三批次修复(6H/13M/12L)
第一批(快速修复):
- H6: main.py --debug 默认值 True→False,防止 Werkzeug RCE
- M2+M3: /search 增加 validate_query + top_k 范围限制(1-50)
- M4: context_count 范围限制(0-10) + 异常捕获
- L3: assert → raise RuntimeError(生产环境 API Key 检查)
- H1: SSE 错误事件移除 traceback 字段

第二批(安全加固):
- H2+H3: 文档接口路径遍历 realpath 校验 + 文件类型/大小限制
- H4+H5: 批量上传文件大小检查
- M6: LIKE 查询通配符转义
- M1: 37 处 str(e) 异常信息统一脱敏(6 文件)
- M5: CORS 生产环境限制来源
- M7: SESSION_MANAGER None 保护(503)
- M11: subprocess 参数注入防护(白名单 + -- 分隔符)

第三批(架构改进):
- M8+M9: 提取 JSON 解析共享工具(extract_json_object/list)
- M10: Prompt 注入检测防御(prompt_guard.py)
- M12: 解析器文件大小限制(Excel 50MB/TXT 20MB/PDF 100MB)
- M13: 全局单例竞态条件双重检查锁定(engine/bm25/intent_analyzer)
2026-06-05 15:26:32 +08:00

233 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
图片服务 API
路由:
- GET /images/<image_id> - 获取图片
- GET /images/<image_id>/info - 获取图片信息
- GET /images/list - 列出所有图片
"""
import os
import logging
from flask import Blueprint, send_file, jsonify, current_app
logger = logging.getLogger(__name__)
image_bp = Blueprint('images', __name__)
def get_images_base_path():
"""获取图片存储路径(扁平化)"""
# 使用扁平化路径 .data/images
data_images_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".data", "images")
if os.path.exists(data_images_path):
return data_images_path
# 回退到 documents/images
try:
from config import DOCUMENTS_PATH
return os.path.join(DOCUMENTS_PATH, "images")
except ImportError:
return os.path.join(os.path.dirname(os.path.dirname(__file__)), "documents", "images")
@image_bp.route('/images/<image_id>', methods=['GET'])
def get_image(image_id: str):
"""
获取图片
Args:
image_id: 图片 ID支持多种格式xxx.jpg, images/xxx.jpg, /images/xxx.jpg
Returns:
图片文件
"""
# 统一提取文件名(处理各种路径格式)
image_id = os.path.basename(image_id)
# 去掉扩展名(后面会重新匹配)
image_id = os.path.splitext(image_id)[0]
# 安全检查:防止路径遍历攻击
if '..' in image_id or '/' in image_id or '\\' in image_id:
return jsonify({"error": "无效的图片 ID"}), 400
images_path = get_images_base_path()
# 支持多种格式
supported_formats = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']
for ext in supported_formats:
image_path = os.path.join(images_path, f"{image_id}{ext}")
if os.path.exists(image_path):
try:
mimetype = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
'.webp': 'image/webp'
}.get(ext, 'image/octet-stream')
return send_file(image_path, mimetype=mimetype)
except Exception as e:
logger.error(f"读取图片异常: {e}")
return jsonify({"error": "读取图片失败"}), 500
return jsonify({"error": "图片不存在", "image_id": image_id}), 404
@image_bp.route('/images/<image_id>/info', methods=['GET'])
def get_image_info(image_id: str):
"""
获取图片元信息
Args:
image_id: 图片 ID
Returns:
图片元信息(宽度、高度、格式等)
"""
# 统一提取文件名
image_id = os.path.basename(image_id)
image_id = os.path.splitext(image_id)[0]
# 安全检查
if '..' in image_id or '/' in image_id or '\\' in image_id:
return jsonify({"error": "无效的图片 ID"}), 400
images_path = get_images_base_path()
supported_formats = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']
for ext in supported_formats:
image_path = os.path.join(images_path, f"{image_id}{ext}")
if os.path.exists(image_path):
try:
# 使用 PIL 获取图片信息
from PIL import Image
with Image.open(image_path) as img:
return jsonify({
"image_id": image_id,
"width": img.width,
"height": img.height,
"format": img.format,
"mode": img.mode,
"size_bytes": os.path.getsize(image_path),
"url": f"/images/{image_id}"
})
except ImportError:
# PIL 未安装,返回基本信息
return jsonify({
"image_id": image_id,
"size_bytes": os.path.getsize(image_path),
"url": f"/images/{image_id}"
})
except Exception as e:
logger.error(f"读取图片信息异常: {e}")
return jsonify({"error": "读取图片信息失败"}), 500
return jsonify({"error": "图片不存在", "image_id": image_id}), 404
@image_bp.route('/images/list', methods=['GET'])
def list_images():
"""
列出所有图片
Query Parameters:
limit: 最大返回数量(默认 50
offset: 偏移量(默认 0
Returns:
图片列表
"""
from flask import request
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
images_path = get_images_base_path()
if not os.path.exists(images_path):
return jsonify({"images": [], "total": 0})
supported_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
images = []
try:
for filename in os.listdir(images_path):
ext = os.path.splitext(filename)[1].lower()
if ext in supported_extensions:
image_id = os.path.splitext(filename)[0]
filepath = os.path.join(images_path, filename)
images.append({
"image_id": image_id,
"url": f"/images/{image_id}",
"size_bytes": os.path.getsize(filepath)
})
# 排序
images.sort(key=lambda x: x['image_id'])
# 分页
total = len(images)
images = images[offset:offset + limit]
return jsonify({
"images": images,
"total": total,
"limit": limit,
"offset": offset
})
except Exception as e:
logger.error(f"列出图片异常: {e}")
return jsonify({"error": "列出图片失败"}), 500
@image_bp.route('/images/stats', methods=['GET'])
def image_stats():
"""
获取图片统计信息
Returns:
图片总数、总大小等统计信息
"""
images_path = get_images_base_path()
if not os.path.exists(images_path):
return jsonify({
"total_images": 0,
"total_size_bytes": 0,
"supported_formats": ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']
})
supported_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
total_count = 0
total_size = 0
format_counts = {}
try:
for filename in os.listdir(images_path):
ext = os.path.splitext(filename)[1].lower()
if ext in supported_extensions:
total_count += 1
filepath = os.path.join(images_path, filename)
total_size += os.path.getsize(filepath)
format_counts[ext] = format_counts.get(ext, 0) + 1
return jsonify({
"total_images": total_count,
"total_size_bytes": total_size,
"total_size_mb": round(total_size / (1024 * 1024), 2),
"format_counts": format_counts,
"supported_formats": list(supported_extensions)
})
except Exception as e:
logger.error(f"获取统计信息异常: {e}")
return jsonify({"error": "获取统计信息失败"}), 500