mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-05 13:41:36 +02:00
Camera streams, snapshots, thumbnails, timelapse videos, photos, QR codes, and cover images served via <img>/<video> tags were previously unauthenticated because browser media elements cannot send Authorization headers. When auth is enabled, these endpoints are now protected by a reusable stream token (?token=xxx) obtained from POST /printers/camera/stream-token (requires CAMERA_VIEW permission).
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
import logging
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy import delete, func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.core.auth import RequireCameraStreamTokenIfAuthEnabled, RequirePermissionIfAuthEnabled
|
|
from backend.app.core.config import settings
|
|
from backend.app.core.database import get_db
|
|
from backend.app.core.permissions import Permission
|
|
from backend.app.models.print_log import PrintLogEntry
|
|
from backend.app.models.user import User
|
|
from backend.app.schemas.print_log import PrintLogEntrySchema, PrintLogResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/print-log", tags=["print-log"])
|
|
|
|
|
|
@router.get("/", response_model=PrintLogResponse)
|
|
async def get_print_log(
|
|
search: str | None = None,
|
|
printer_id: int | None = None,
|
|
created_by_username: str | None = None,
|
|
status: str | None = None,
|
|
date_from: datetime | None = None,
|
|
date_to: datetime | None = None,
|
|
limit: int = Query(default=50, ge=1, le=500),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ),
|
|
):
|
|
"""Get the print log."""
|
|
query = select(PrintLogEntry)
|
|
count_query = select(func.count(PrintLogEntry.id))
|
|
|
|
if printer_id is not None:
|
|
query = query.where(PrintLogEntry.printer_id == printer_id)
|
|
count_query = count_query.where(PrintLogEntry.printer_id == printer_id)
|
|
if created_by_username:
|
|
query = query.where(PrintLogEntry.created_by_username == created_by_username)
|
|
count_query = count_query.where(PrintLogEntry.created_by_username == created_by_username)
|
|
if status:
|
|
query = query.where(PrintLogEntry.status == status)
|
|
count_query = count_query.where(PrintLogEntry.status == status)
|
|
if search:
|
|
query = query.where(PrintLogEntry.print_name.ilike(f"%{search}%"))
|
|
count_query = count_query.where(PrintLogEntry.print_name.ilike(f"%{search}%"))
|
|
if date_from:
|
|
query = query.where(PrintLogEntry.created_at >= date_from)
|
|
count_query = count_query.where(PrintLogEntry.created_at >= date_from)
|
|
if date_to:
|
|
query = query.where(PrintLogEntry.created_at <= date_to)
|
|
count_query = count_query.where(PrintLogEntry.created_at <= date_to)
|
|
|
|
# Get total count
|
|
total_result = await db.execute(count_query)
|
|
total = total_result.scalar() or 0
|
|
|
|
# Get paginated results
|
|
query = query.order_by(PrintLogEntry.created_at.desc()).offset(offset).limit(limit)
|
|
result = await db.execute(query)
|
|
entries = result.scalars().all()
|
|
|
|
return PrintLogResponse(
|
|
items=[
|
|
PrintLogEntrySchema(
|
|
id=e.id,
|
|
print_name=e.print_name,
|
|
printer_name=e.printer_name,
|
|
printer_id=e.printer_id,
|
|
status=e.status,
|
|
started_at=e.started_at,
|
|
completed_at=e.completed_at,
|
|
duration_seconds=e.duration_seconds,
|
|
filament_type=e.filament_type,
|
|
filament_color=e.filament_color,
|
|
filament_used_grams=e.filament_used_grams,
|
|
thumbnail_path=e.thumbnail_path,
|
|
created_by_username=e.created_by_username,
|
|
created_at=e.created_at,
|
|
)
|
|
for e in entries
|
|
],
|
|
total=total,
|
|
)
|
|
|
|
|
|
@router.get("/{entry_id}/thumbnail")
|
|
async def get_print_log_thumbnail(
|
|
entry_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: None = RequireCameraStreamTokenIfAuthEnabled,
|
|
):
|
|
"""Get the thumbnail for a print log entry.
|
|
|
|
Requires a stream token query param (?token=xxx) when auth is enabled.
|
|
"""
|
|
entry = await db.get(PrintLogEntry, entry_id)
|
|
if not entry or not entry.thumbnail_path:
|
|
raise HTTPException(404, "Thumbnail not found")
|
|
|
|
thumb_path = settings.base_dir / entry.thumbnail_path
|
|
if not thumb_path.exists():
|
|
raise HTTPException(404, "Thumbnail file not found")
|
|
|
|
return FileResponse(
|
|
path=thumb_path,
|
|
media_type="image/png",
|
|
headers={"Cache-Control": "public, max-age=86400"},
|
|
)
|
|
|
|
|
|
@router.delete("/")
|
|
async def clear_print_log(
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_DELETE_ALL),
|
|
):
|
|
"""Clear the print log.
|
|
|
|
Only deletes log entries. Archives and queue items are never touched.
|
|
"""
|
|
result = await db.execute(delete(PrintLogEntry))
|
|
deleted = result.rowcount
|
|
await db.commit()
|
|
|
|
logger.info("Print log cleared: %d entries deleted", deleted)
|
|
return {"deleted": deleted}
|