mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 03:31:25 +02:00
CodeQL flags except blocks where `pass` has no comment explaining why the exception is silently ignored (py/empty-except rule). Added context-specific comments to all 265 instances across 31 files: - database.py (~112): ALTER TABLE migrations — "Already applied" - archive/library/3MF parsing (~64): "Skip unparseable metadata" - virtual_printer network cleanup (~32): "Best-effort socket cleanup" - discovery/SSDP (~13): "SO_REUSEPORT not available" / socket cleanup - bambu_ftp/mqtt (~13): FTP cleanup, JSON decode, signal parsing - remaining routes/services (~31): context-specific comments
268 lines
7.8 KiB
Python
268 lines
7.8 KiB
Python
"""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."""
|
|
result = await db.execute(select(PendingUpload).where(PendingUpload.status == "pending"))
|
|
pending_uploads = result.scalars().all()
|
|
|
|
archived = 0
|
|
failed = 0
|
|
|
|
service = ArchiveService(db)
|
|
|
|
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,
|
|
},
|
|
)
|
|
|
|
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
|
|
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,
|
|
},
|
|
)
|
|
|
|
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}
|