package handlers import ( "net/http" "github.com/flatrender/render-svc/internal/db" "github.com/flatrender/render-svc/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/minio/minio-go/v7" ) // SnapshotJobHandler queues per-scene single-frame AE snapshot jobs and exposes // the node-facing claim/result/fail lifecycle. On result it writes the image URL // onto content.scenes.snapshot_url (in the store, cross-schema). type SnapshotJobHandler struct { store *db.Store minio *minio.Client templatesBucket string } func NewSnapshotJobHandler(store *db.Store, mc *minio.Client, templatesBucket string) *SnapshotJobHandler { return &SnapshotJobHandler{store: store, minio: mc, templatesBucket: templatesBucket} } // POST /v1/scene-snapshots/:project_id (admin) → queue one job per active scene. func (h *SnapshotJobHandler) Enqueue(c *gin.Context) { pid, err := uuid.Parse(c.Param("project_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid project_id"}) return } n, err := h.store.EnqueueSceneSnapshots(c.Request.Context(), pid) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{"enqueued": n}) } // GET /v1/scene-snapshots/:project_id (admin) → job statuses for polling. func (h *SnapshotJobHandler) List(c *gin.Context) { pid, err := uuid.Parse(c.Param("project_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid project_id"}) return } jobs, err := h.store.ListSnapshotJobs(c.Request.Context(), pid) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } if jobs == nil { jobs = []db.SnapshotJob{} } c.JSON(http.StatusOK, gin.H{"jobs": jobs}) } // POST /v1/internal/snapshot/claim (node, HMAC) func (h *SnapshotJobHandler) Claim(c *gin.Context) { var req models.ClaimJobRequest _ = c.ShouldBindJSON(&req) claim, err := h.store.ClaimSnapshotJob(c.Request.Context(), req.NodeID) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } if claim == nil { c.Status(http.StatusNoContent) return } url, isBundle, md5 := resolveTemplateObject(h.minio, h.templatesBucket, claim.ProjectID) if url == "" { _ = h.store.SetSnapshotError(c.Request.Context(), claim.ID, "no template stored for this project — upload the .aep from «فایل‌ها» first") c.Status(http.StatusNoContent) return } c.JSON(http.StatusOK, gin.H{ "snapshot_job_id": claim.ID, "project_id": claim.ProjectID, "scene_id": claim.SceneID, "scene_key": claim.SceneKey, "comp_name": claim.CompName, "frame": claim.Frame, "aep_download_url": url, "is_bundle": isBundle, "bundle_md5": md5, }) } // POST /v1/internal/snapshot/:id/result (node, HMAC) body {image_url} func (h *SnapshotJobHandler) Result(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } var req struct { ImageURL string `json:"image_url"` } if err := c.ShouldBindJSON(&req); err != nil || req.ImageURL == "" { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "image_url required"}) return } if err := h.store.SetSnapshotResult(c.Request.Context(), id, req.ImageURL); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusNoContent) } // POST /v1/internal/snapshot/:id/fail (node, HMAC) body {reason} func (h *SnapshotJobHandler) Fail(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } var req struct { Reason string `json:"reason"` } _ = c.ShouldBindJSON(&req) if req.Reason == "" { req.Reason = "snapshot failed" } if err := h.store.SetSnapshotError(c.Request.Context(), id, req.Reason); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusNoContent) }