Add File
This commit is contained in:
947
src/landppt/api/image_api.py
Normal file
947
src/landppt/api/image_api.py
Normal file
@@ -0,0 +1,947 @@
|
|||||||
|
"""
|
||||||
|
图片服务API路由
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends, Request, UploadFile, File, Form
|
||||||
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..services.image.image_service import get_image_service
|
||||||
|
from ..services.image.config.image_config import get_image_config
|
||||||
|
from ..auth.middleware import get_current_user_required
|
||||||
|
from ..database.models import User
|
||||||
|
from ..utils.thread_pool import run_blocking_io, to_thread
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class ImageGenerationRequest(BaseModel):
|
||||||
|
prompt: str
|
||||||
|
provider: Optional[str] = None
|
||||||
|
size: Optional[str] = None
|
||||||
|
quality: Optional[str] = None
|
||||||
|
style: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ImageSuggestionRequest(BaseModel):
|
||||||
|
slide_title: str
|
||||||
|
slide_content: str
|
||||||
|
scenario: str
|
||||||
|
topic: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/image/status")
|
||||||
|
async def get_image_service_status(
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""获取图片服务状态"""
|
||||||
|
try:
|
||||||
|
image_config = get_image_config()
|
||||||
|
config = image_config.get_config()
|
||||||
|
|
||||||
|
# 检查可用的提供者
|
||||||
|
available_providers = []
|
||||||
|
|
||||||
|
# 检查DALL-E
|
||||||
|
if config.get('dalle', {}).get('api_key'):
|
||||||
|
available_providers.append('dalle')
|
||||||
|
|
||||||
|
# 检查Stable Diffusion
|
||||||
|
if config.get('stable_diffusion', {}).get('api_key'):
|
||||||
|
available_providers.append('stable_diffusion')
|
||||||
|
|
||||||
|
# 检查SiliconFlow
|
||||||
|
if config.get('siliconflow', {}).get('api_key'):
|
||||||
|
available_providers.append('siliconflow')
|
||||||
|
|
||||||
|
# 检查Pollinations(免费服务,总是可用)
|
||||||
|
available_providers.append('pollinations')
|
||||||
|
|
||||||
|
# 检查搜索服务
|
||||||
|
search_providers = []
|
||||||
|
if config.get('unsplash', {}).get('access_key'):
|
||||||
|
search_providers.append('unsplash')
|
||||||
|
if config.get('pixabay', {}).get('api_key'):
|
||||||
|
search_providers.append('pixabay')
|
||||||
|
|
||||||
|
# 检查缓存目录
|
||||||
|
cache_dir = Path(config.get('cache', {}).get('base_dir', 'temp/images_cache'))
|
||||||
|
cache_info = {
|
||||||
|
'directory': str(cache_dir),
|
||||||
|
'exists': cache_dir.exists(),
|
||||||
|
'size': '0 MB',
|
||||||
|
'file_count': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if cache_dir.exists():
|
||||||
|
try:
|
||||||
|
files = list(cache_dir.rglob('*'))
|
||||||
|
cache_info['file_count'] = len([f for f in files if f.is_file()])
|
||||||
|
|
||||||
|
total_size = sum(f.stat().st_size for f in files if f.is_file())
|
||||||
|
cache_info['size'] = f"{total_size / (1024 * 1024):.1f} MB"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get cache info: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok" if available_providers else "no_providers",
|
||||||
|
"available_providers": available_providers,
|
||||||
|
"search_providers": search_providers,
|
||||||
|
"cache_info": cache_info,
|
||||||
|
"message": f"Found {len(available_providers)} image generation providers and {len(search_providers)} search providers"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get image service status: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get image service status: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/test")
|
||||||
|
async def test_image_service(
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""测试图片服务"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
image_config = get_image_config()
|
||||||
|
config = image_config.get_config()
|
||||||
|
|
||||||
|
test_results = {
|
||||||
|
"providers": {},
|
||||||
|
"cache_info": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试DALL-E
|
||||||
|
if config.get('dalle', {}).get('api_key'):
|
||||||
|
try:
|
||||||
|
# 这里可以添加实际的DALL-E测试逻辑
|
||||||
|
test_results["providers"]["dalle"] = {
|
||||||
|
"available": True,
|
||||||
|
"message": "DALL-E API密钥已配置"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
test_results["providers"]["dalle"] = {
|
||||||
|
"available": False,
|
||||||
|
"message": f"DALL-E测试失败: {str(e)}"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
test_results["providers"]["dalle"] = {
|
||||||
|
"available": False,
|
||||||
|
"message": "DALL-E API密钥未配置"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试Stable Diffusion
|
||||||
|
if config.get('stable_diffusion', {}).get('api_key'):
|
||||||
|
try:
|
||||||
|
test_results["providers"]["stable_diffusion"] = {
|
||||||
|
"available": True,
|
||||||
|
"message": "Stable Diffusion API密钥已配置"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
test_results["providers"]["stable_diffusion"] = {
|
||||||
|
"available": False,
|
||||||
|
"message": f"Stable Diffusion测试失败: {str(e)}"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
test_results["providers"]["stable_diffusion"] = {
|
||||||
|
"available": False,
|
||||||
|
"message": "Stable Diffusion API密钥未配置"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试SiliconFlow
|
||||||
|
if config.get('siliconflow', {}).get('api_key'):
|
||||||
|
try:
|
||||||
|
test_results["providers"]["siliconflow"] = {
|
||||||
|
"available": True,
|
||||||
|
"message": "SiliconFlow API密钥已配置"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
test_results["providers"]["siliconflow"] = {
|
||||||
|
"available": False,
|
||||||
|
"message": f"SiliconFlow测试失败: {str(e)}"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
test_results["providers"]["siliconflow"] = {
|
||||||
|
"available": False,
|
||||||
|
"message": "SiliconFlow API密钥未配置"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试Pollinations(免费服务)
|
||||||
|
try:
|
||||||
|
test_results["providers"]["pollinations"] = {
|
||||||
|
"available": True,
|
||||||
|
"message": "Pollinations服务可用(免费服务)"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
test_results["providers"]["pollinations"] = {
|
||||||
|
"available": False,
|
||||||
|
"message": f"Pollinations测试失败: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试缓存
|
||||||
|
cache_dir = Path(config.get('cache', {}).get('base_dir', 'temp/images_cache'))
|
||||||
|
try:
|
||||||
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
files = list(cache_dir.rglob('*'))
|
||||||
|
file_count = len([f for f in files if f.is_file()])
|
||||||
|
total_size = sum(f.stat().st_size for f in files if f.is_file())
|
||||||
|
|
||||||
|
test_results["cache_info"] = {
|
||||||
|
"directory": str(cache_dir),
|
||||||
|
"file_count": file_count,
|
||||||
|
"size": f"{total_size / (1024 * 1024):.1f} MB",
|
||||||
|
"writable": os.access(cache_dir, os.W_OK)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
test_results["cache_info"] = {
|
||||||
|
"directory": str(cache_dir),
|
||||||
|
"error": f"缓存目录测试失败: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return test_results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Image service test failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Image service test failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/cache/clear")
|
||||||
|
async def clear_image_cache(
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""清理图片缓存"""
|
||||||
|
try:
|
||||||
|
image_config = get_image_config()
|
||||||
|
config = image_config.get_config()
|
||||||
|
|
||||||
|
cache_dir = Path(config.get('cache', {}).get('base_dir', 'temp/images_cache'))
|
||||||
|
|
||||||
|
if not cache_dir.exists():
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"deleted_files": 0,
|
||||||
|
"freed_space": "0 MB",
|
||||||
|
"message": "缓存目录不存在"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 在线程池中执行文件删除操作
|
||||||
|
result = await run_blocking_io(_clear_cache_sync, cache_dir)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"deleted_files": result["deleted_count"],
|
||||||
|
"freed_space": f"{result['freed_space_mb']:.1f} MB",
|
||||||
|
"message": f"成功清理了 {result['deleted_count']} 个文件"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to clear image cache: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to clear image cache: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_cache_sync(cache_dir: Path) -> Dict[str, Any]:
|
||||||
|
"""同步清理缓存(在线程池中运行)"""
|
||||||
|
# 统计删除前的信息
|
||||||
|
files = list(cache_dir.rglob('*'))
|
||||||
|
files_to_delete = [f for f in files if f.is_file()]
|
||||||
|
total_size_before = sum(f.stat().st_size for f in files_to_delete)
|
||||||
|
|
||||||
|
# 删除文件
|
||||||
|
deleted_count = 0
|
||||||
|
for file_path in files_to_delete:
|
||||||
|
try:
|
||||||
|
file_path.unlink()
|
||||||
|
deleted_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to delete {file_path}: {e}")
|
||||||
|
|
||||||
|
# 删除空目录
|
||||||
|
for dir_path in sorted([f for f in files if f.is_dir()], reverse=True):
|
||||||
|
try:
|
||||||
|
if not any(dir_path.iterdir()): # 如果目录为空
|
||||||
|
dir_path.rmdir()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to remove directory {dir_path}: {e}")
|
||||||
|
|
||||||
|
freed_space_mb = total_size_before / (1024 * 1024)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"deleted_count": deleted_count,
|
||||||
|
"freed_space_mb": freed_space_mb
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/generate")
|
||||||
|
async def generate_image(
|
||||||
|
request: ImageGenerationRequest,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""生成图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 创建图片生成请求对象
|
||||||
|
from ..services.image.models import ImageGenerationRequest as ServiceImageGenerationRequest, ImageProvider
|
||||||
|
|
||||||
|
# 解析尺寸
|
||||||
|
width, height = 1024, 1024
|
||||||
|
if request.size:
|
||||||
|
try:
|
||||||
|
width, height = map(int, request.size.split('x'))
|
||||||
|
except ValueError:
|
||||||
|
width, height = 1024, 1024
|
||||||
|
|
||||||
|
# 解析提供者
|
||||||
|
provider = ImageProvider.DALLE
|
||||||
|
if request.provider:
|
||||||
|
try:
|
||||||
|
provider = ImageProvider(request.provider)
|
||||||
|
except ValueError:
|
||||||
|
provider = ImageProvider.DALLE
|
||||||
|
|
||||||
|
service_request = ServiceImageGenerationRequest(
|
||||||
|
prompt=request.prompt,
|
||||||
|
provider=provider,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
quality=request.quality or "standard",
|
||||||
|
style=request.style
|
||||||
|
)
|
||||||
|
|
||||||
|
# 生成图片
|
||||||
|
result = await image_service.generate_image(service_request)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
# 返回通过本地图床服务可访问的图片URL(绝对地址)
|
||||||
|
if result.image_info:
|
||||||
|
from ..services.url_service import build_image_url
|
||||||
|
image_url = build_image_url(result.image_info.image_id)
|
||||||
|
else:
|
||||||
|
image_url = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"image_path": image_url,
|
||||||
|
"image_id": result.image_info.image_id if result.image_info else None,
|
||||||
|
"message": result.message
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.message,
|
||||||
|
"error_code": result.error_code
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Image generation failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Image generation failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/suggest")
|
||||||
|
async def suggest_images(
|
||||||
|
request: ImageSuggestionRequest,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""为幻灯片建议图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 获取图片建议
|
||||||
|
suggestions = await image_service.suggest_images_for_ppt_slide(
|
||||||
|
slide_title=request.slide_title,
|
||||||
|
slide_content=request.slide_content,
|
||||||
|
scenario=request.scenario,
|
||||||
|
topic=request.topic
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"suggestions": suggestions,
|
||||||
|
"message": "图片建议生成成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Image suggestion failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Image suggestion failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 图库管理API
|
||||||
|
@router.get("/api/image/gallery/stats")
|
||||||
|
async def get_gallery_stats(
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""获取图库统计信息"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
stats = await image_service.get_cache_stats()
|
||||||
|
|
||||||
|
# 按来源分类统计
|
||||||
|
cache_stats = {
|
||||||
|
'ai_generated': 0,
|
||||||
|
'web_search': 0,
|
||||||
|
'local_storage': 0,
|
||||||
|
'cache_size': '0 MB'
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'categories' in stats:
|
||||||
|
for category, count in stats['categories'].items():
|
||||||
|
if category in cache_stats:
|
||||||
|
cache_stats[category] = count
|
||||||
|
|
||||||
|
if 'total_size_mb' in stats:
|
||||||
|
cache_stats['cache_size'] = f"{stats['total_size_mb']:.1f} MB"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"stats": cache_stats
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get gallery stats: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get gallery stats: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/image/gallery/list")
|
||||||
|
async def list_gallery_images(
|
||||||
|
page: int = 1,
|
||||||
|
per_page: int = 20,
|
||||||
|
category: str = "",
|
||||||
|
search: str = "",
|
||||||
|
sort: str = "created_desc",
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""获取图库图片列表"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 构建搜索参数
|
||||||
|
search_params = {
|
||||||
|
'page': page,
|
||||||
|
'per_page': per_page,
|
||||||
|
'category': category if category else None,
|
||||||
|
'search': search if search else None,
|
||||||
|
'sort': sort
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取图片列表(这里需要实现相应的服务方法)
|
||||||
|
result = await image_service.list_cached_images(**search_params)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"images": result['images'],
|
||||||
|
"pagination": {
|
||||||
|
"current_page": page,
|
||||||
|
"per_page": per_page,
|
||||||
|
"total_count": result['total_count'],
|
||||||
|
"total_pages": (result['total_count'] + per_page - 1) // per_page,
|
||||||
|
"has_prev": page > 1,
|
||||||
|
"has_next": page * per_page < result['total_count']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to list gallery images: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to list gallery images: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/image/detail/{image_id}")
|
||||||
|
async def get_image_detail(
|
||||||
|
image_id: str,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""获取图片详细信息"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
image_info = await image_service.get_image(image_id)
|
||||||
|
|
||||||
|
if not image_info:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"image": {
|
||||||
|
"image_id": image_info.image_id,
|
||||||
|
"title": image_info.title,
|
||||||
|
"description": image_info.description,
|
||||||
|
"tags": ','.join([tag.name for tag in image_info.tags]) if image_info.tags else '',
|
||||||
|
"category": image_info.source_type.value,
|
||||||
|
"filename": image_info.filename,
|
||||||
|
"file_size": image_info.metadata.file_size,
|
||||||
|
"width": image_info.metadata.width,
|
||||||
|
"height": image_info.metadata.height,
|
||||||
|
"format": image_info.metadata.format.value,
|
||||||
|
"source_type": image_info.source_type.value,
|
||||||
|
"provider": image_info.provider.value,
|
||||||
|
"created_at": image_info.created_at,
|
||||||
|
"access_count": getattr(image_info, 'access_count', 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get image detail: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get image detail: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/image/{image_id}/info")
|
||||||
|
async def get_image_info(
|
||||||
|
image_id: str,
|
||||||
|
request: Request,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""获取图片信息,包括绝对URL"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
image_info = await image_service.get_image(image_id)
|
||||||
|
|
||||||
|
if not image_info:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
|
||||||
|
# 构建绝对URL
|
||||||
|
from ..services.url_service import build_image_url
|
||||||
|
absolute_url = build_image_url(image_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"image_info": {
|
||||||
|
"image_id": image_info.image_id,
|
||||||
|
"title": image_info.title,
|
||||||
|
"filename": image_info.filename,
|
||||||
|
"absolute_url": absolute_url,
|
||||||
|
"file_size": image_info.metadata.file_size,
|
||||||
|
"width": image_info.metadata.width,
|
||||||
|
"height": image_info.metadata.height,
|
||||||
|
"format": image_info.metadata.format.value,
|
||||||
|
"source_type": image_info.source_type.value,
|
||||||
|
"provider": image_info.provider.value,
|
||||||
|
"created_at": image_info.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get image info: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get image info: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/image/view/{image_id}")
|
||||||
|
async def view_image(
|
||||||
|
image_id: str
|
||||||
|
):
|
||||||
|
"""查看图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
image_info = await image_service.get_image(image_id)
|
||||||
|
|
||||||
|
if not image_info or not image_info.local_path:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
|
||||||
|
image_path = Path(image_info.local_path)
|
||||||
|
if not image_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Image file not found")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
path=str(image_path),
|
||||||
|
media_type=f"image/{image_info.metadata.format.value}",
|
||||||
|
filename=image_info.filename
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to view image: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to view image: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/image/thumbnail/{image_id}")
|
||||||
|
async def get_image_thumbnail(
|
||||||
|
image_id: str
|
||||||
|
):
|
||||||
|
"""获取图片缩略图"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 尝试获取缩略图
|
||||||
|
thumbnail_path = await image_service.get_thumbnail(image_id)
|
||||||
|
|
||||||
|
if thumbnail_path and Path(thumbnail_path).exists():
|
||||||
|
return FileResponse(
|
||||||
|
path=str(thumbnail_path),
|
||||||
|
media_type="image/jpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果没有缩略图,返回原图
|
||||||
|
image_info = await image_service.get_image(image_id)
|
||||||
|
if image_info and image_info.local_path and Path(image_info.local_path).exists():
|
||||||
|
return FileResponse(
|
||||||
|
path=str(image_info.local_path),
|
||||||
|
media_type=f"image/{image_info.metadata.format.value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="Thumbnail not found")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get thumbnail: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get thumbnail: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/image/download/{image_id}")
|
||||||
|
async def download_image(
|
||||||
|
image_id: str,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""下载单张图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
image_info = await image_service.get_image(image_id)
|
||||||
|
|
||||||
|
if not image_info or not image_info.local_path:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
|
||||||
|
image_path = Path(image_info.local_path)
|
||||||
|
if not image_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Image file not found")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
path=str(image_path),
|
||||||
|
media_type=f"image/{image_info.metadata.format.value}",
|
||||||
|
filename=image_info.filename,
|
||||||
|
headers={"Content-Disposition": f"attachment; filename=\"{image_info.filename}\""}
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to download image: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to download image: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class BatchDeleteRequest(BaseModel):
|
||||||
|
image_ids: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class BatchDownloadRequest(BaseModel):
|
||||||
|
image_ids: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/image/delete/{image_id}")
|
||||||
|
async def delete_single_image(
|
||||||
|
image_id: str,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""删除单张图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 删除图片
|
||||||
|
result = await image_service.delete_image(image_id)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "图片删除成功"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to delete image: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to delete image: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/gallery/batch-delete")
|
||||||
|
async def batch_delete_images(
|
||||||
|
request: BatchDeleteRequest,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""批量删除图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
failed_ids = []
|
||||||
|
|
||||||
|
for image_id in request.image_ids:
|
||||||
|
try:
|
||||||
|
result = await image_service.delete_image(image_id)
|
||||||
|
if result:
|
||||||
|
deleted_count += 1
|
||||||
|
else:
|
||||||
|
failed_ids.append(image_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to delete image {image_id}: {e}")
|
||||||
|
failed_ids.append(image_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"deleted_count": deleted_count,
|
||||||
|
"failed_count": len(failed_ids),
|
||||||
|
"failed_ids": failed_ids,
|
||||||
|
"message": f"成功删除 {deleted_count} 张图片"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Batch delete failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Batch delete failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/gallery/batch-download")
|
||||||
|
async def batch_download_images(
|
||||||
|
request: BatchDownloadRequest,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""批量下载图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 获取所有图片信息
|
||||||
|
image_infos = []
|
||||||
|
for image_id in request.image_ids:
|
||||||
|
try:
|
||||||
|
image_info = await image_service.get_image(image_id)
|
||||||
|
if image_info and image_info.local_path:
|
||||||
|
image_path = Path(image_info.local_path)
|
||||||
|
if image_path.exists():
|
||||||
|
image_infos.append({
|
||||||
|
'path': str(image_path),
|
||||||
|
'filename': image_info.filename
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get image {image_id}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 在线程池中创建ZIP文件
|
||||||
|
zip_data = await run_blocking_io(_create_zip_sync, image_infos)
|
||||||
|
|
||||||
|
# 生成文件名
|
||||||
|
timestamp = int(time.time())
|
||||||
|
filename = f"images_{timestamp}.zip"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
io.BytesIO(zip_data),
|
||||||
|
media_type="application/zip",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Batch download failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Batch download failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _create_zip_sync(image_infos: List[Dict[str, str]]) -> bytes:
|
||||||
|
"""同步创建ZIP文件(在线程池中运行)"""
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||||
|
for image_info in image_infos:
|
||||||
|
try:
|
||||||
|
zip_file.write(image_info['path'], image_info['filename'])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to add {image_info['filename']} to zip: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
zip_buffer.seek(0)
|
||||||
|
return zip_buffer.read()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/upload")
|
||||||
|
async def upload_image(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
title: str = Form(""),
|
||||||
|
description: str = Form(""),
|
||||||
|
category: str = Form("local_storage"),
|
||||||
|
tags: str = Form(""),
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""上传图片"""
|
||||||
|
try:
|
||||||
|
# 验证文件类型
|
||||||
|
if not file.content_type or not file.content_type.startswith('image/'):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid file type")
|
||||||
|
|
||||||
|
# 在线程池中读取文件数据
|
||||||
|
file_data = await file.read()
|
||||||
|
|
||||||
|
# 创建上传请求
|
||||||
|
from ..services.image.models import ImageUploadRequest
|
||||||
|
upload_request = ImageUploadRequest(
|
||||||
|
filename=file.filename,
|
||||||
|
file_size=len(file_data),
|
||||||
|
title=title if title else file.filename.split('.')[0],
|
||||||
|
description=description,
|
||||||
|
category=category,
|
||||||
|
tags=[tag.strip() for tag in tags.split(',') if tag.strip()] if tags else [],
|
||||||
|
content_type=file.content_type
|
||||||
|
)
|
||||||
|
|
||||||
|
# 上传图片
|
||||||
|
image_service = get_image_service()
|
||||||
|
result = await image_service.upload_image(upload_request, file_data)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"image_id": result.image_info.image_id,
|
||||||
|
"message": "图片上传成功"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail=result.message)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Image upload failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Image upload failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageUpdateRequest(BaseModel):
|
||||||
|
"""图片信息更新请求"""
|
||||||
|
title: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
tags: Optional[str] = None
|
||||||
|
category: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/image/{image_id}/update")
|
||||||
|
async def update_image_info(
|
||||||
|
image_id: str,
|
||||||
|
request: ImageUpdateRequest,
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""更新图片信息"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
image_info = await image_service.get_image(image_id)
|
||||||
|
|
||||||
|
if not image_info:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
|
||||||
|
# 更新图片信息
|
||||||
|
if request.title is not None:
|
||||||
|
image_info.title = request.title
|
||||||
|
|
||||||
|
if request.description is not None:
|
||||||
|
image_info.description = request.description
|
||||||
|
|
||||||
|
if request.tags is not None:
|
||||||
|
# 清除现有标签
|
||||||
|
image_info.tags = []
|
||||||
|
# 添加新标签
|
||||||
|
if request.tags.strip():
|
||||||
|
tag_names = [tag.strip() for tag in request.tags.split(',') if tag.strip()]
|
||||||
|
for tag_name in tag_names:
|
||||||
|
image_info.add_tag(tag_name)
|
||||||
|
|
||||||
|
if request.category is not None:
|
||||||
|
# 更新分类(通过source_type)
|
||||||
|
from ..services.image.models import ImageSourceType
|
||||||
|
try:
|
||||||
|
image_info.source_type = ImageSourceType(request.category)
|
||||||
|
except ValueError:
|
||||||
|
# 如果分类无效,保持原有分类
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 更新时间戳
|
||||||
|
import time
|
||||||
|
image_info.updated_at = time.time()
|
||||||
|
|
||||||
|
# 保存更新后的图片信息
|
||||||
|
# 通过重新保存元数据来更新信息
|
||||||
|
cache_key = image_service.cache_manager._generate_cache_key(image_info)
|
||||||
|
await image_service.cache_manager._save_image_metadata(cache_key, image_info)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "图片信息已更新",
|
||||||
|
"image": {
|
||||||
|
"image_id": image_info.image_id,
|
||||||
|
"title": image_info.title,
|
||||||
|
"description": image_info.description,
|
||||||
|
"tags": ','.join([tag.name for tag in image_info.tags]) if image_info.tags else '',
|
||||||
|
"category": image_info.source_type.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update image info: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to update image info: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/gallery/deduplicate")
|
||||||
|
async def deduplicate_gallery(
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""去重图库中的重复图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 执行去重操作
|
||||||
|
result = await image_service.deduplicate_cache()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"已去重 {result['duplicates_removed']} 张重复图片",
|
||||||
|
"duplicates_removed": result['duplicates_removed']
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to deduplicate gallery: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to deduplicate gallery: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/image/gallery/clear-all")
|
||||||
|
async def clear_all_images(
|
||||||
|
user: User = Depends(get_current_user_required)
|
||||||
|
):
|
||||||
|
"""清空图床 - 删除所有图片"""
|
||||||
|
try:
|
||||||
|
image_service = get_image_service()
|
||||||
|
|
||||||
|
# 获取所有图片的统计信息
|
||||||
|
stats = await image_service.get_cache_stats()
|
||||||
|
total_images = stats.get('total_entries', 0)
|
||||||
|
|
||||||
|
if total_images == 0:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"deleted_count": 0,
|
||||||
|
"message": "图床已经是空的"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清空所有缓存
|
||||||
|
deleted_count = await image_service.clear_all_cache()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"deleted_count": deleted_count,
|
||||||
|
"message": f"成功清空图床,删除了 {deleted_count} 张图片"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to clear all images: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to clear all images: {str(e)}")
|
||||||
Reference in New Issue
Block a user