pending_uploads.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """API routes for pending uploads (virtual printer queue mode)."""
  2. from datetime import UTC, datetime
  3. from pathlib import Path
  4. from fastapi import APIRouter, Depends, HTTPException
  5. from pydantic import BaseModel
  6. from sqlalchemy import select
  7. from sqlalchemy.ext.asyncio import AsyncSession
  8. from backend.app.core.database import get_db
  9. from backend.app.models.pending_upload import PendingUpload
  10. from backend.app.services.archive import ArchiveService
  11. router = APIRouter(prefix="/pending-uploads", tags=["pending-uploads"])
  12. class ArchiveRequest(BaseModel):
  13. """Request to archive a pending upload."""
  14. tags: str | None = None
  15. notes: str | None = None
  16. project_id: int | None = None
  17. class PendingUploadResponse(BaseModel):
  18. """Response model for pending upload."""
  19. id: int
  20. filename: str
  21. file_size: int
  22. source_ip: str | None
  23. status: str
  24. tags: str | None
  25. notes: str | None
  26. project_id: int | None
  27. uploaded_at: datetime
  28. class Config:
  29. from_attributes = True
  30. @router.get("/", response_model=list[PendingUploadResponse])
  31. async def list_pending_uploads(db: AsyncSession = Depends(get_db)):
  32. """List all pending uploads."""
  33. result = await db.execute(
  34. select(PendingUpload).where(PendingUpload.status == "pending").order_by(PendingUpload.uploaded_at.desc())
  35. )
  36. return result.scalars().all()
  37. @router.get("/count")
  38. async def get_pending_count(db: AsyncSession = Depends(get_db)):
  39. """Get count of pending uploads."""
  40. result = await db.execute(select(PendingUpload).where(PendingUpload.status == "pending"))
  41. count = len(result.scalars().all())
  42. return {"count": count}
  43. # Note: Bulk operations must be defined BEFORE parameterized routes
  44. # to prevent FastAPI from matching /archive-all as /{upload_id}
  45. @router.post("/archive-all")
  46. async def archive_all_pending(db: AsyncSession = Depends(get_db)):
  47. """Archive all pending uploads."""
  48. result = await db.execute(select(PendingUpload).where(PendingUpload.status == "pending"))
  49. pending_uploads = result.scalars().all()
  50. archived = 0
  51. failed = 0
  52. service = ArchiveService(db)
  53. for pending in pending_uploads:
  54. file_path = Path(pending.file_path)
  55. if not file_path.exists():
  56. pending.status = "discarded"
  57. failed += 1
  58. continue
  59. try:
  60. archive = await service.archive_print(
  61. printer_id=None,
  62. source_file=file_path,
  63. print_data={
  64. "status": "archived",
  65. "source": "virtual_printer",
  66. "source_ip": pending.source_ip,
  67. },
  68. )
  69. if archive:
  70. pending.status = "archived"
  71. pending.archived_id = archive.id
  72. pending.archived_at = datetime.now(UTC)
  73. archived += 1
  74. # Clean up temp file
  75. try:
  76. file_path.unlink()
  77. except Exception:
  78. pass
  79. else:
  80. failed += 1
  81. except Exception:
  82. failed += 1
  83. await db.commit()
  84. return {
  85. "archived": archived,
  86. "failed": failed,
  87. }
  88. @router.delete("/discard-all")
  89. async def discard_all_pending(db: AsyncSession = Depends(get_db)):
  90. """Discard all pending uploads."""
  91. result = await db.execute(select(PendingUpload).where(PendingUpload.status == "pending"))
  92. pending_uploads = result.scalars().all()
  93. discarded = 0
  94. for pending in pending_uploads:
  95. # Delete file from disk
  96. try:
  97. file_path = Path(pending.file_path)
  98. file_path.unlink(missing_ok=True)
  99. except Exception:
  100. pass
  101. pending.status = "discarded"
  102. discarded += 1
  103. await db.commit()
  104. return {"discarded": discarded}
  105. @router.get("/{upload_id}", response_model=PendingUploadResponse)
  106. async def get_pending_upload(
  107. upload_id: int,
  108. db: AsyncSession = Depends(get_db),
  109. ):
  110. """Get a specific pending upload."""
  111. result = await db.execute(select(PendingUpload).where(PendingUpload.id == upload_id))
  112. pending = result.scalar_one_or_none()
  113. if not pending:
  114. raise HTTPException(status_code=404, detail="Upload not found")
  115. return pending
  116. @router.post("/{upload_id}/archive")
  117. async def archive_pending_upload(
  118. upload_id: int,
  119. request: ArchiveRequest = None,
  120. db: AsyncSession = Depends(get_db),
  121. ):
  122. """Archive a pending upload."""
  123. result = await db.execute(select(PendingUpload).where(PendingUpload.id == upload_id))
  124. pending = result.scalar_one_or_none()
  125. if not pending:
  126. raise HTTPException(status_code=404, detail="Upload not found")
  127. if pending.status != "pending":
  128. raise HTTPException(status_code=400, detail="Upload already processed")
  129. # Check file exists
  130. file_path = Path(pending.file_path)
  131. if not file_path.exists():
  132. raise HTTPException(status_code=404, detail="Upload file not found on disk")
  133. # Archive the file
  134. service = ArchiveService(db)
  135. archive = await service.archive_print(
  136. printer_id=None,
  137. source_file=file_path,
  138. print_data={
  139. "status": "archived",
  140. "source": "virtual_printer",
  141. "source_ip": pending.source_ip,
  142. },
  143. )
  144. if not archive:
  145. raise HTTPException(status_code=500, detail="Failed to archive file")
  146. # Apply tags/notes/project from request
  147. if request:
  148. if request.tags:
  149. archive.tags = request.tags
  150. if request.notes:
  151. archive.notes = request.notes
  152. if request.project_id:
  153. archive.project_id = request.project_id
  154. # Update pending record
  155. pending.status = "archived"
  156. pending.archived_id = archive.id
  157. pending.archived_at = datetime.now(UTC)
  158. if request:
  159. pending.tags = request.tags
  160. pending.notes = request.notes
  161. pending.project_id = request.project_id
  162. await db.commit()
  163. # Clean up temp file
  164. try:
  165. file_path.unlink()
  166. except Exception:
  167. pass
  168. return {
  169. "id": archive.id,
  170. "print_name": archive.print_name,
  171. "filename": archive.filename,
  172. }
  173. @router.delete("/{upload_id}")
  174. async def discard_pending_upload(
  175. upload_id: int,
  176. db: AsyncSession = Depends(get_db),
  177. ):
  178. """Discard a pending upload without archiving."""
  179. result = await db.execute(select(PendingUpload).where(PendingUpload.id == upload_id))
  180. pending = result.scalar_one_or_none()
  181. if not pending:
  182. raise HTTPException(status_code=404, detail="Upload not found")
  183. # Delete file from disk
  184. file_path = Path(pending.file_path)
  185. try:
  186. file_path.unlink(missing_ok=True)
  187. except Exception:
  188. pass
  189. # Update status
  190. pending.status = "discarded"
  191. await db.commit()
  192. return {"success": True}