endpoints.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. # app/api/endpoints.py
  2. from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends
  3. from fastapi.responses import StreamingResponse
  4. from app.api.deps import get_request_context
  5. from typing import List, Optional
  6. import urllib.parse
  7. from app.models.schemas import FileItem, CreateFolderRequest
  8. from app.services.minio_service import minio_service
  9. from app.services.img_minio_service import img_minio_service
  10. from app.utils.path_utils import validate_path, sanitize_filename
  11. from app.utils.logger_utils import logger
  12. from app.core.config import settings
  13. from app.core.exceptions import (
  14. NotFoundException,
  15. ValidationException,
  16. InternalServerException,
  17. ErrorMessage
  18. )
  19. from minio.error import S3Error
  20. router = APIRouter(dependencies=[Depends(get_request_context)])
  21. @router.get("/files", response_model=List[FileItem])
  22. async def list_files(
  23. path: str = "/",
  24. ):
  25. # 验证路径
  26. if path != "/":
  27. path = validate_path(path, allow_root=True)
  28. return minio_service.list_files(path)
  29. @router.post("/upload")
  30. async def upload_file(
  31. file: UploadFile = File(...),
  32. path: str = Form(...),
  33. ):
  34. """
  35. 上传文件到MinIO存储
  36. path: 完整的文件路径 (例如: folder/subfolder/image.png)
  37. """
  38. logger.info(f"接收到的文件名: {file.filename}")
  39. logger.info(f"接收到的路径参数: {path}")
  40. # 验证和清理路径
  41. path = validate_path(path)
  42. # 验证文件名
  43. if not file.filename:
  44. raise ValidationException(detail="文件名不能为空")
  45. filename = sanitize_filename(file.filename)
  46. # 获取文件大小
  47. file.file.seek(0, 2)
  48. size = file.file.tell()
  49. file.file.seek(0)
  50. # 检查文件大小限制
  51. if size > settings.MAX_UPLOAD_SIZE:
  52. max_size_mb = settings.MAX_UPLOAD_SIZE / (1024 * 1024)
  53. raise HTTPException(
  54. status_code=413,
  55. detail=f"文件大小超过限制 ({max_size_mb}MB)"
  56. )
  57. # 构建完整路径
  58. full_path = f"{path}/{filename}" if path else filename
  59. logger.info(f"上传文件: {filename}, 目标路径: {full_path}, 大小: {size} bytes")
  60. minio_service.upload_file(
  61. file.file,
  62. full_path,
  63. file.content_type or "application/octet-stream",
  64. size,
  65. )
  66. return {"message": "Upload successful", "filename": filename, "path": full_path}
  67. @router.post("/folder")
  68. async def create_folder(
  69. req: CreateFolderRequest,
  70. ):
  71. # 验证路径
  72. path = validate_path(req.path)
  73. minio_service.create_folder(path)
  74. logger.info(f"创建文件夹: {path}")
  75. return {"message": "Folder created"}
  76. @router.get("/download")
  77. async def download_file(
  78. path: str,
  79. ):
  80. # 验证路径
  81. path = validate_path(path)
  82. try:
  83. data_stream = minio_service.get_file_stream(path)
  84. except S3Error as e:
  85. if e.code == "NoSuchKey":
  86. logger.warning(f"文件不存在: {path}")
  87. raise NotFoundException(detail=ErrorMessage.FILE_NOT_FOUND)
  88. raise
  89. filename = path.split("/")[-1]
  90. quoted_filename = urllib.parse.quote(filename)
  91. logger.info(f"下载文件: {path}")
  92. return StreamingResponse(
  93. data_stream,
  94. media_type="application/octet-stream",
  95. headers={
  96. "Content-Disposition": f"attachment; filename*=UTF-8''{quoted_filename}"
  97. }
  98. )
  99. @router.delete("/delete")
  100. async def delete_item(
  101. path: str,
  102. ):
  103. # 验证路径
  104. path = validate_path(path)
  105. is_dir = path.endswith("/")
  106. minio_service.delete_file(path, is_dir)
  107. logger.info(f"删除{'文件夹' if is_dir else '文件'}: {path}")
  108. return {"message": "Deleted successfully"}
  109. @router.get("/preview")
  110. async def preview_file(
  111. path: str,
  112. ):
  113. # 验证路径
  114. path = validate_path(path)
  115. url = minio_service.get_presigned_url(path)
  116. return {"url": url}
  117. @router.get("/storage-info")
  118. async def get_storage_info():
  119. return minio_service.get_storage_info()
  120. @router.get("/img")
  121. async def preview_image(path: str):
  122. # 验证路径
  123. path = validate_path(path)
  124. url = img_minio_service.get_presigned_url(path)
  125. return {"url": url}
  126. @router.post("/img/upload")
  127. async def upload_image(
  128. file: UploadFile = File(..., description="上传的图片文件"),
  129. path: Optional[str] = Form(None, description="指定存储路径,不指定则使用文件名")
  130. ):
  131. """
  132. 上传图片到MinIO存储
  133. """
  134. # 验证文件名
  135. if not file.filename:
  136. raise ValidationException(detail="文件名不能为空")
  137. filename = sanitize_filename(file.filename)
  138. # 读取文件内容
  139. file_content = await file.read()
  140. # 检查文件大小限制
  141. if len(file_content) > settings.MAX_UPLOAD_SIZE:
  142. max_size_mb = settings.MAX_UPLOAD_SIZE / (1024 * 1024)
  143. raise HTTPException(
  144. status_code=413,
  145. detail=f"文件大小超过限制 ({max_size_mb}MB)"
  146. )
  147. # 生成对象名称:路径 + 文件名
  148. if path:
  149. # 验证和清理路径
  150. path = validate_path(path)
  151. if not path.endswith('/'):
  152. path = path + '/'
  153. object_name = f"{path}{filename}"
  154. else:
  155. # 直接使用文件名
  156. object_name = filename
  157. logger.info(f"上传图片: {filename}, 目标路径: {object_name}, 大小: {len(file_content)} bytes")
  158. # 上传到MinIO
  159. stored_name = img_minio_service.upload_image_data(
  160. image_data=file_content,
  161. object_name=object_name,
  162. content_type=file.content_type or "image/jpeg"
  163. )
  164. return {
  165. "message": "图片上传成功",
  166. "object_name": stored_name,
  167. "file_size": len(file_content),
  168. "content_type": file.content_type
  169. }
  170. @router.put("/img/rename")
  171. async def rename_image(
  172. old_path: str = Form(..., description="原文件路径"),
  173. new_path: str = Form(..., description="新文件路径"),
  174. overwrite: bool = Form(False, description="是否覆盖已存在的新路径文件")
  175. ):
  176. """
  177. 重命名或移动图片文件
  178. """
  179. # 验证路径
  180. old_path = validate_path(old_path)
  181. new_path = validate_path(new_path)
  182. # 检查原文件是否存在
  183. if not img_minio_service.image_exists(old_path):
  184. raise NotFoundException(detail="原文件不存在")
  185. # 检查新文件是否已存在
  186. if not overwrite and img_minio_service.image_exists(new_path):
  187. raise ValidationException(
  188. detail="目标文件已存在,如需覆盖请设置 overwrite=true"
  189. )
  190. # 执行重命名操作
  191. success = img_minio_service.rename_image(old_path, new_path)
  192. if success:
  193. logger.info(f"重命名图片: {old_path} -> {new_path}")
  194. return {
  195. "message": "文件重命名成功",
  196. "old_path": old_path,
  197. "new_path": new_path
  198. }
  199. else:
  200. raise InternalServerException(detail="文件重命名失败")