mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 04:31:28 +02:00
Adds GET /slicer-plugin/info and POST /slicer-plugin/uploads, which take a sliced file plus a JSON manifest in one multipart request: what the user asked for (store, or queue a plate x copies on a printer, optionally held for a manual start) and where the file came from (slicer and plugin versions, presets, a model fingerprint kept for later matching). The manifest does not carry the slice's numbers; plates, time, grams and sliced_for_model are read from the file by the library's own parser. The work is delegated rather than re-implemented: the body of POST /library/files becomes a shared ingest_library_upload, and queueing calls POST /queue/ itself, so every validation, compatibility, filament and budget check applies unchanged. Sends are idempotent on a plugin-chosen upload_id, claimed through a unique constraint, released when the attempt fails, taken over when abandoned, and replayed only to the caller who first used it; a retried queue send is otherwise a second print. A missing plate and a key that may not queue are refused before anything is stored. A refusal from the queue keeps the file, reports queue_error and rolls back what that route wrote first, since with billing on it creates the batch before asking for a cost centre. cost_center_id is accepted for the same reason. New table slicer_plugin_uploads, created on startup.
54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
"""Uploads sent by the slicer plugin, and where each one came from."""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
|
|
class SlicerPluginUpload(Base):
|
|
"""One upload from the slicer plugin, keyed by the id the plugin chose for it.
|
|
|
|
Two jobs. It makes the upload idempotent: the plugin retries a send whose
|
|
response it never saw, and a retry that queued the file a second time would
|
|
print it twice. And it keeps what the file cannot say about itself -- which
|
|
slicer and presets produced it, and a fingerprint of the model -- which is
|
|
what lets a later slice of the same model find this one.
|
|
"""
|
|
|
|
__tablename__ = "slicer_plugin_uploads"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
|
|
# Chosen by the plugin, one per send. Unique, so two requests carrying the
|
|
# same id cannot both get past the claim in slicer_plugin.py.
|
|
upload_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
|
|
|
# Who sent it: "user:<id>", "apikey:<id>", or "anonymous" with auth off. A
|
|
# replay from anyone else is refused rather than answered with this
|
|
# caller's result.
|
|
caller: Mapped[str] = mapped_column(String(40))
|
|
|
|
# "processing" while the request that claimed the id is still working,
|
|
# "done" once `response` holds what it returned.
|
|
status: Mapped[str] = mapped_column(String(20), default="processing")
|
|
|
|
library_file_id: Mapped[int | None] = mapped_column(
|
|
ForeignKey("library_files.id", ondelete="SET NULL"), nullable=True, index=True
|
|
)
|
|
|
|
# Provenance, as the plugin reported it.
|
|
slicer: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
slicer_version: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
plugin_version: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
project_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
model_fingerprint: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
|
|
presets: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON
|
|
|
|
# The JSON body the first request returned, sent back verbatim on a replay.
|
|
response: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|