upload.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """文件上传接口"""
  2. import os
  3. from typing import Optional
  4. from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form
  5. from sqlalchemy.orm import Session
  6. from app.core.database import get_db
  7. from app.core.auth import get_current_user
  8. from app.core.response import success_response, error_response
  9. from app.core.storage import storage
  10. router = APIRouter(prefix="/api/upload", tags=["文件上传"])
  11. # 允许的文件类型
  12. ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/heic", "image/heif"}
  13. ALLOWED_DOCUMENT_TYPES = {
  14. "application/pdf",
  15. "application/msword",
  16. "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
  17. }
  18. # 文件大小限制 (字节)
  19. MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10MB
  20. MAX_DOCUMENT_SIZE = 20 * 1024 * 1024 # 20MB
  21. async def validate_file(file: UploadFile, max_size: int, allowed_types: set) -> bytes:
  22. """验证文件"""
  23. if file.content_type not in allowed_types:
  24. raise HTTPException(
  25. status_code=400,
  26. detail=f"不支持的文件类型: {file.content_type}"
  27. )
  28. content = b""
  29. chunk_size = 1024 * 1024
  30. while True:
  31. chunk = await file.read(chunk_size)
  32. if not chunk:
  33. break
  34. content += chunk
  35. if len(content) > max_size:
  36. raise HTTPException(status_code=400, detail="文件大小超过限制")
  37. await file.seek(0)
  38. return content
  39. @router.post("/image", response_model=dict)
  40. async def upload_image(
  41. file: UploadFile = File(...),
  42. folder: str = Form("images"),
  43. current_user: dict = Depends(get_current_user)
  44. ):
  45. """上传图片"""
  46. try:
  47. content = await validate_file(file, MAX_IMAGE_SIZE, ALLOWED_IMAGE_TYPES)
  48. original_filename = file.filename or "image.jpg"
  49. url = storage.upload_file(content, original_filename, folder)
  50. return success_response({
  51. "url": url,
  52. "filename": original_filename,
  53. "contentType": file.content_type,
  54. "size": len(content)
  55. })
  56. except HTTPException:
  57. raise
  58. except Exception as e:
  59. return error_response(500, f"上传失败: {str(e)}")
  60. @router.post("/images", response_model=dict)
  61. async def upload_images(
  62. files: list[UploadFile] = File(...),
  63. folder: str = Form("images"),
  64. current_user: dict = Depends(get_current_user)
  65. ):
  66. """批量上传图片"""
  67. if len(files) > 9:
  68. return error_response(400, "最多支持同时上传9张图片")
  69. results = []
  70. errors = []
  71. for i, file in enumerate(files):
  72. try:
  73. content = await validate_file(file, MAX_IMAGE_SIZE, ALLOWED_IMAGE_TYPES)
  74. original_filename = file.filename or f"image_{i}.jpg"
  75. url = storage.upload_file(content, original_filename, folder)
  76. results.append({
  77. "url": url,
  78. "filename": original_filename,
  79. "contentType": file.content_type,
  80. "size": len(content),
  81. "index": i
  82. })
  83. except Exception as e:
  84. errors.append({"index": i, "filename": file.filename, "error": str(e)})
  85. return success_response({
  86. "success": results,
  87. "errors": errors,
  88. "totalSuccess": len(results),
  89. "totalError": len(errors)
  90. })
  91. @router.post("/document", response_model=dict)
  92. async def upload_document(
  93. file: UploadFile = File(...),
  94. folder: str = Form("documents"),
  95. current_user: dict = Depends(get_current_user)
  96. ):
  97. """上传文档"""
  98. try:
  99. content = await validate_file(file, MAX_DOCUMENT_SIZE, ALLOWED_DOCUMENT_TYPES)
  100. original_filename = file.filename or "document.pdf"
  101. url = storage.upload_file(content, original_filename, folder)
  102. return success_response({
  103. "url": url,
  104. "filename": original_filename,
  105. "contentType": file.content_type,
  106. "size": len(content)
  107. })
  108. except HTTPException:
  109. raise
  110. except Exception as e:
  111. return error_response(500, f"上传失败: {str(e)}")
  112. @router.post("/base64", response_model=dict)
  113. async def upload_base64(
  114. data: dict,
  115. current_user: dict = Depends(get_current_user)
  116. ):
  117. """上传 Base64 编码的图片"""
  118. try:
  119. base64_data = data.get("base64")
  120. filename = data.get("filename", "image.png")
  121. folder = data.get("folder", "images")
  122. if not base64_data:
  123. return error_response(400, "base64数据不能为空")
  124. url = storage.upload_base64(base64_data, filename, folder)
  125. return success_response({"url": url, "filename": filename})
  126. except Exception as e:
  127. return error_response(500, f"上传失败: {str(e)}")
  128. @router.delete("/file", response_model=dict)
  129. async def delete_file(
  130. url: str,
  131. current_user: dict = Depends(get_current_user)
  132. ):
  133. """删除文件"""
  134. try:
  135. success = storage.delete_file(url)
  136. if success:
  137. return success_response({"message": "删除成功"})
  138. return error_response(400, "文件不存在或删除失败")
  139. except Exception as e:
  140. return error_response(500, f"删除失败: {str(e)}")
  141. @router.get("/info", response_model=dict)
  142. async def get_storage_info(
  143. current_user: dict = Depends(get_current_user)
  144. ):
  145. """获取存储配置信息"""
  146. from app.core.config import settings
  147. info = {
  148. "storageType": settings.STORAGE_TYPE,
  149. "maxImageSize": MAX_IMAGE_SIZE,
  150. "maxDocumentSize": MAX_DOCUMENT_SIZE,
  151. "allowedImageTypes": list(ALLOWED_IMAGE_TYPES),
  152. "allowedDocumentTypes": list(ALLOWED_DOCUMENT_TYPES)
  153. }
  154. if settings.STORAGE_TYPE == "local":
  155. info["baseUrl"] = settings.LOCAL_STORAGE_URL
  156. elif settings.STORAGE_TYPE == "qiniu":
  157. info["domain"] = settings.QINIU_DOMAIN
  158. elif settings.STORAGE_TYPE == "oss":
  159. info["domain"] = settings.OSS_DOMAIN
  160. elif settings.STORAGE_TYPE == "minio":
  161. scheme = "https" if settings.MINIO_SECURE else "http"
  162. info["domain"] = settings.MINIO_BASE_URL or f"{scheme}://{settings.MINIO_ENDPOINT}/{settings.MINIO_BUCKET}"
  163. return success_response(info)