export.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. """
  2. Export API router.
  3. Provides endpoints for exporting project annotations in various formats.
  4. """
  5. import os
  6. from fastapi import APIRouter, HTTPException, status, Request
  7. from fastapi.responses import FileResponse
  8. from database import get_db_connection
  9. from schemas.export import (
  10. ExportRequest, ExportJobResponse, ExportProgressResponse,
  11. ExportFormat, ExportStatus
  12. )
  13. from services.export_service import ExportService
  14. router = APIRouter(
  15. prefix="/api",
  16. tags=["export"]
  17. )
  18. @router.post("/projects/{project_id}/export", response_model=ExportJobResponse)
  19. async def create_export(
  20. request: Request,
  21. project_id: str,
  22. export_request: ExportRequest
  23. ):
  24. """
  25. Create a new export job for a project.
  26. Args:
  27. request: FastAPI Request object (contains user info)
  28. project_id: Project unique identifier
  29. export_request: Export configuration
  30. Returns:
  31. Export job details
  32. Raises:
  33. HTTPException: 404 if project not found
  34. HTTPException: 403 if user doesn't have permission
  35. Requires authentication (admin only).
  36. """
  37. # Get current user
  38. user = request.state.user
  39. # Check admin permission
  40. if user["role"] != "admin":
  41. raise HTTPException(
  42. status_code=status.HTTP_403_FORBIDDEN,
  43. detail="只有管理员可以导出数据"
  44. )
  45. # Verify project exists
  46. project = ExportService.get_project_data(project_id)
  47. if not project:
  48. raise HTTPException(
  49. status_code=status.HTTP_404_NOT_FOUND,
  50. detail=f"项目 '{project_id}' 不存在"
  51. )
  52. # Create export job
  53. job_id = ExportService.create_export_job(
  54. project_id=project_id,
  55. format=export_request.format.value,
  56. status_filter=export_request.status_filter.value,
  57. include_metadata=export_request.include_metadata,
  58. created_by=user["id"]
  59. )
  60. # Execute export synchronously (for simplicity)
  61. # In production, this should be done asynchronously with a task queue
  62. result = ExportService.execute_export(
  63. job_id=job_id,
  64. project_id=project_id,
  65. format=export_request.format.value,
  66. status_filter=export_request.status_filter.value,
  67. include_metadata=export_request.include_metadata
  68. )
  69. # Get updated job info
  70. job = ExportService.get_export_job(job_id)
  71. if not job:
  72. raise HTTPException(
  73. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  74. detail="导出任务创建失败"
  75. )
  76. # Build download URL if completed
  77. download_url = None
  78. if job["status"] == ExportStatus.COMPLETED.value and job["file_path"]:
  79. download_url = f"/api/exports/{job_id}/download"
  80. return ExportJobResponse(
  81. id=job["id"],
  82. project_id=job["project_id"],
  83. format=job["format"],
  84. status=job["status"],
  85. status_filter=job["status_filter"],
  86. include_metadata=job["include_metadata"],
  87. file_path=job["file_path"],
  88. download_url=download_url,
  89. error_message=job["error_message"],
  90. created_at=job["created_at"],
  91. completed_at=job["completed_at"],
  92. total_tasks=job["total_tasks"],
  93. exported_tasks=job["exported_tasks"]
  94. )
  95. @router.get("/exports/{export_id}/status", response_model=ExportProgressResponse)
  96. async def get_export_status(request: Request, export_id: str):
  97. """
  98. Get export job status and progress.
  99. Args:
  100. request: FastAPI Request object (contains user info)
  101. export_id: Export job unique identifier
  102. Returns:
  103. Export job progress details
  104. Raises:
  105. HTTPException: 404 if export job not found
  106. Requires authentication.
  107. """
  108. job = ExportService.get_export_job(export_id)
  109. if not job:
  110. raise HTTPException(
  111. status_code=status.HTTP_404_NOT_FOUND,
  112. detail=f"导出任务 '{export_id}' 不存在"
  113. )
  114. # Calculate progress
  115. progress = 0.0
  116. if job["total_tasks"] > 0:
  117. progress = job["exported_tasks"] / job["total_tasks"]
  118. elif job["status"] == ExportStatus.COMPLETED.value:
  119. progress = 1.0
  120. return ExportProgressResponse(
  121. id=job["id"],
  122. status=job["status"],
  123. progress=progress,
  124. total_tasks=job["total_tasks"],
  125. exported_tasks=job["exported_tasks"],
  126. error_message=job["error_message"]
  127. )
  128. @router.get("/exports/{export_id}/download")
  129. async def download_export(request: Request, export_id: str):
  130. """
  131. Download exported file.
  132. Args:
  133. request: FastAPI Request object (contains user info)
  134. export_id: Export job unique identifier
  135. Returns:
  136. File download response
  137. Raises:
  138. HTTPException: 404 if export job or file not found
  139. HTTPException: 400 if export not completed
  140. Requires authentication.
  141. """
  142. job = ExportService.get_export_job(export_id)
  143. if not job:
  144. raise HTTPException(
  145. status_code=status.HTTP_404_NOT_FOUND,
  146. detail=f"导出任务 '{export_id}' 不存在"
  147. )
  148. if job["status"] != ExportStatus.COMPLETED.value:
  149. raise HTTPException(
  150. status_code=status.HTTP_400_BAD_REQUEST,
  151. detail=f"导出任务尚未完成,当前状态: {job['status']}"
  152. )
  153. if not job["file_path"] or not os.path.exists(job["file_path"]):
  154. raise HTTPException(
  155. status_code=status.HTTP_404_NOT_FOUND,
  156. detail="导出文件不存在"
  157. )
  158. # Determine media type based on format
  159. media_type = "application/json"
  160. if job["format"] == ExportFormat.CSV.value:
  161. media_type = "text/csv"
  162. # Get filename from path
  163. filename = os.path.basename(job["file_path"])
  164. return FileResponse(
  165. path=job["file_path"],
  166. media_type=media_type,
  167. filename=filename
  168. )
  169. @router.get("/exports/{export_id}", response_model=ExportJobResponse)
  170. async def get_export_job(request: Request, export_id: str):
  171. """
  172. Get export job details.
  173. Args:
  174. request: FastAPI Request object (contains user info)
  175. export_id: Export job unique identifier
  176. Returns:
  177. Export job details
  178. Raises:
  179. HTTPException: 404 if export job not found
  180. Requires authentication.
  181. """
  182. job = ExportService.get_export_job(export_id)
  183. if not job:
  184. raise HTTPException(
  185. status_code=status.HTTP_404_NOT_FOUND,
  186. detail=f"导出任务 '{export_id}' 不存在"
  187. )
  188. # Build download URL if completed
  189. download_url = None
  190. if job["status"] == ExportStatus.COMPLETED.value and job["file_path"]:
  191. download_url = f"/api/exports/{job['id']}/download"
  192. return ExportJobResponse(
  193. id=job["id"],
  194. project_id=job["project_id"],
  195. format=job["format"],
  196. status=job["status"],
  197. status_filter=job["status_filter"],
  198. include_metadata=job["include_metadata"],
  199. file_path=job["file_path"],
  200. download_url=download_url,
  201. error_message=job["error_message"],
  202. created_at=job["created_at"],
  203. completed_at=job["completed_at"],
  204. total_tasks=job["total_tasks"],
  205. exported_tasks=job["exported_tasks"]
  206. )