Files
rag/api/image_routes.py
lacerate551 183a57e7f1 refactor(api): 统一响应格式迁移 + 异步任务系统 + 状态码体系完善
- 将全部路由文件(12个)的 jsonify 响应迁移至 success_response/error_response 统一格式
- 修复 sync_routes.py error_response 参数错误(P0)
- 新增异步任务系统:task_registry + task_routes
- 新增状态码:TASK_NOT_FOUND(4014)、TASK_CONFLICT(4015)、REINDEX_ERROR(5040)
- 修正 task_routes/exam_pkg 中语义不匹配的状态码
- 更新 curl 测试手册、后端对接规范文档
- 添加缓存性能报告和 Redis 迁移计划
2026-06-05 22:56:00 +08:00

235 lines
7.6 KiB
Python
Raw 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
from core.status_codes import SUCCESS, BAD_REQUEST, NOT_FOUND, INTERNAL_ERROR
from api.response_utils import success_response, error_response
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 error_response("BAD_REQUEST", BAD_REQUEST, "无效的图片 ID", http_status=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 error_response("READ_ERROR", INTERNAL_ERROR, "读取图片失败", http_status=500)
return error_response("NOT_FOUND", NOT_FOUND, "图片不存在", http_status=404, image_id=image_id)
@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 error_response("BAD_REQUEST", BAD_REQUEST, "无效的图片 ID", http_status=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 success_response(data={
"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 success_response(data={
"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 error_response("READ_ERROR", INTERNAL_ERROR, "读取图片信息失败", http_status=500)
return error_response("NOT_FOUND", NOT_FOUND, "图片不存在", http_status=404, image_id=image_id)
@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 success_response(data={"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 success_response(data={
"images": images,
"total": total,
"limit": limit,
"offset": offset
})
except Exception as e:
logger.error(f"列出图片异常: {e}")
return error_response("LIST_ERROR", INTERNAL_ERROR, "列出图片失败", http_status=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 success_response(data={
"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 success_response(data={
"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 error_response("STATS_ERROR", INTERNAL_ERROR, "获取统计信息失败", http_status=500)