pending_uploads.py 7.5 KB

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