| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- """API routes for pending uploads (virtual printer queue mode)."""
- from datetime import datetime, timezone
- from pathlib import Path
- from fastapi import APIRouter, Depends, HTTPException
- from pydantic import BaseModel
- from sqlalchemy import select
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.core.auth import RequirePermissionIfAuthEnabled
- from backend.app.core.database import get_db
- from backend.app.core.permissions import Permission
- from backend.app.models.pending_upload import PendingUpload
- from backend.app.models.user import User
- from backend.app.services.archive import ArchiveService
- router = APIRouter(prefix="/pending-uploads", tags=["pending-uploads"])
- class ArchiveRequest(BaseModel):
- """Request to archive a pending upload."""
- tags: str | None = None
- notes: str | None = None
- project_id: int | None = None
- class PendingUploadResponse(BaseModel):
- """Response model for pending upload."""
- id: int
- filename: str
- file_size: int
- source_ip: str | None
- status: str
- tags: str | None
- notes: str | None
- project_id: int | None
- uploaded_at: datetime
- class Config:
- from_attributes = True
- @router.get("/", response_model=list[PendingUploadResponse])
- async def list_pending_uploads(
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_READ),
- ):
- """List all pending uploads."""
- result = await db.execute(
- select(PendingUpload).where(PendingUpload.status == "pending").order_by(PendingUpload.uploaded_at.desc())
- )
- return result.scalars().all()
- @router.get("/count")
- async def get_pending_count(
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_READ),
- ):
- """Get count of pending uploads."""
- result = await db.execute(select(PendingUpload).where(PendingUpload.status == "pending"))
- count = len(result.scalars().all())
- return {"count": count}
- # Note: Bulk operations must be defined BEFORE parameterized routes
- # to prevent FastAPI from matching /archive-all as /{upload_id}
- @router.post("/archive-all")
- async def archive_all_pending(
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_CREATE),
- ):
- """Archive all pending uploads."""
- from backend.app.api.routes.settings import get_setting
- result = await db.execute(select(PendingUpload).where(PendingUpload.status == "pending"))
- pending_uploads = result.scalars().all()
- archived = 0
- failed = 0
- service = ArchiveService(db)
- prefer_filename = (await get_setting(db, "virtual_printer_archive_name_source")) == "filename"
- for pending in pending_uploads:
- file_path = Path(pending.file_path)
- if not file_path.exists():
- pending.status = "discarded"
- failed += 1
- continue
- try:
- archive = await service.archive_print(
- printer_id=None,
- source_file=file_path,
- print_data={
- "status": "archived",
- "source": "virtual_printer",
- "source_ip": pending.source_ip,
- },
- prefer_filename_for_name=prefer_filename,
- )
- if archive:
- pending.status = "archived"
- pending.archived_id = archive.id
- pending.archived_at = datetime.now(timezone.utc)
- archived += 1
- # Clean up temp file
- try:
- file_path.unlink()
- except OSError:
- pass # Best-effort temp file cleanup after archiving
- else:
- failed += 1
- except Exception: # Mixed async DB + archive operations
- failed += 1
- await db.commit()
- return {
- "archived": archived,
- "failed": failed,
- }
- @router.delete("/discard-all")
- async def discard_all_pending(
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_DELETE_ALL),
- ):
- """Discard all pending uploads."""
- result = await db.execute(select(PendingUpload).where(PendingUpload.status == "pending"))
- pending_uploads = result.scalars().all()
- discarded = 0
- for pending in pending_uploads:
- # Delete file from disk
- try:
- file_path = Path(pending.file_path)
- file_path.unlink(missing_ok=True)
- except OSError:
- pass # Best-effort file deletion; record is still marked discarded
- pending.status = "discarded"
- discarded += 1
- await db.commit()
- return {"discarded": discarded}
- @router.get("/{upload_id}", response_model=PendingUploadResponse)
- async def get_pending_upload(
- upload_id: int,
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_READ),
- ):
- """Get a specific pending upload."""
- result = await db.execute(select(PendingUpload).where(PendingUpload.id == upload_id))
- pending = result.scalar_one_or_none()
- if not pending:
- raise HTTPException(status_code=404, detail="Upload not found")
- return pending
- @router.post("/{upload_id}/archive")
- async def archive_pending_upload(
- upload_id: int,
- request: ArchiveRequest = None,
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_CREATE),
- ):
- """Archive a pending upload."""
- result = await db.execute(select(PendingUpload).where(PendingUpload.id == upload_id))
- pending = result.scalar_one_or_none()
- if not pending:
- raise HTTPException(status_code=404, detail="Upload not found")
- if pending.status != "pending":
- raise HTTPException(status_code=400, detail="Upload already processed")
- # Check file exists
- file_path = Path(pending.file_path)
- if not file_path.exists():
- raise HTTPException(status_code=404, detail="Upload file not found on disk")
- # Archive the file
- from backend.app.api.routes.settings import get_setting
- prefer_filename = (await get_setting(db, "virtual_printer_archive_name_source")) == "filename"
- service = ArchiveService(db)
- archive = await service.archive_print(
- printer_id=None,
- source_file=file_path,
- print_data={
- "status": "archived",
- "source": "virtual_printer",
- "source_ip": pending.source_ip,
- },
- prefer_filename_for_name=prefer_filename,
- )
- if not archive:
- raise HTTPException(status_code=500, detail="Failed to archive file")
- # Apply tags/notes/project from request
- if request:
- if request.tags:
- archive.tags = request.tags
- if request.notes:
- archive.notes = request.notes
- if request.project_id:
- archive.project_id = request.project_id
- # Update pending record
- pending.status = "archived"
- pending.archived_id = archive.id
- pending.archived_at = datetime.now(timezone.utc)
- if request:
- pending.tags = request.tags
- pending.notes = request.notes
- pending.project_id = request.project_id
- await db.commit()
- # Clean up temp file
- try:
- file_path.unlink()
- except OSError:
- pass # Best-effort temp file cleanup after successful archive
- return {
- "id": archive.id,
- "print_name": archive.print_name,
- "filename": archive.filename,
- }
- @router.delete("/{upload_id}")
- async def discard_pending_upload(
- upload_id: int,
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_DELETE_ALL),
- ):
- """Discard a pending upload without archiving."""
- result = await db.execute(select(PendingUpload).where(PendingUpload.id == upload_id))
- pending = result.scalar_one_or_none()
- if not pending:
- raise HTTPException(status_code=404, detail="Upload not found")
- # Delete file from disk
- file_path = Path(pending.file_path)
- try:
- file_path.unlink(missing_ok=True)
- except OSError:
- pass # Best-effort file deletion on discard
- # Update status
- pending.status = "discarded"
- await db.commit()
- return {"success": True}
|