endpoints.py 6.8 KB

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