6cf8716d7e
Build backend images / build content-svc (push) Failing after 13s
Build backend images / build file-svc (push) Failing after 53s
Build backend images / build gateway (push) Failing after 1m22s
Build backend images / build identity-svc (push) Failing after 19s
Build backend images / build notification-svc (push) Failing after 21s
Build backend images / build render-svc (push) Failing after 20s
Build backend images / build studio-svc (push) Failing after 1m6s
C2 — real-AE scene snapshots on the node:
- node-agent: runner/snapshot.go RunSnapshot (aerender -comp <key> -s f -e f
→ findRenderedOutput → ffmpeg -frames:v 1 PNG); client ClaimSnapshot /
GetSnapshotUploadURL / ReportSnapshotResult / ReportSnapshotFail; snapshotLoop +
pollSnapshotOnce mirroring the scan loop (reuses the AE-exclusive lock).
- render-svc: GetSnapshotJobMeta + UploadURL handler presigns a PUT to the
public-read user-uploads bucket at snapshots/{project}/{scene}.png and returns a
permanent public_url (not the time-limited export presign); MINIO_UPLOAD_BUCKET +
MINIO_PUBLIC_URL config + compose env + /snapshot/:id/upload-url route.
Epic B — bind edited colours into the render:
- render-svc GetRenderBindings UNIONs studio.saved_shared_colors +
saved_scene_colors (type 'color') so the node writes them before render.
- node-agent binder.go routes type:"color" bindings into the bind-spec colors[]
array that bind.jsx already applies to the frshare colour layers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
165 lines
5.7 KiB
Go
165 lines
5.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"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
|
|
uploadBucket string // public-read bucket snapshots land in (e.g. user-uploads)
|
|
publicBase string // browser-reachable base, e.g. http://172.28.144.1:9000
|
|
}
|
|
|
|
func NewSnapshotJobHandler(store *db.Store, mc *minio.Client, templatesBucket, uploadBucket, publicBase string) *SnapshotJobHandler {
|
|
return &SnapshotJobHandler{store: store, minio: mc, templatesBucket: templatesBucket, uploadBucket: uploadBucket, publicBase: publicBase}
|
|
}
|
|
|
|
// 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/upload-url (node, HMAC)
|
|
// Presigns a PUT to the public-read uploads bucket and returns the permanent
|
|
// public URL the node should report back once the still is uploaded.
|
|
func (h *SnapshotJobHandler) UploadURL(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
|
|
}
|
|
pid, sceneKey, merr := h.store.GetSnapshotJobMeta(c.Request.Context(), id)
|
|
if merr != nil {
|
|
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "snapshot job not found"})
|
|
return
|
|
}
|
|
objectKey := fmt.Sprintf("snapshots/%s/%s.png", pid, sceneKey)
|
|
put, perr := h.minio.PresignedPutObject(c.Request.Context(), h.uploadBucket, objectKey, 15*time.Minute)
|
|
if perr != nil {
|
|
c.JSON(http.StatusInternalServerError, models.APIError{Code: "presign_failed", Message: perr.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"upload_url": put.String(),
|
|
"object_key": objectKey,
|
|
"public_url": fmt.Sprintf("%s/%s/%s", h.publicBase, h.uploadBucket, objectKey),
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|