Implement context archive handling and metadata extraction for render jobs. Add functionality to check for Blender availability, create context archives, and extract metadata from .blend files. Update job creation and retrieval processes to support new metadata structure and context file management. Enhance client-side components to display context files and integrate new API endpoints for context handling.

This commit is contained in:
2025-11-24 10:02:13 -06:00
parent f9ff4d0138
commit a029714e08
13 changed files with 3887 additions and 856 deletions

View File

@@ -17,7 +17,6 @@ import (
"jiggablend/pkg/types"
"github.com/go-chi/chi/v5"
"github.com/gorilla/websocket"
)
@@ -294,51 +293,37 @@ func (s *Server) handleUpdateTaskStep(w http.ResponseWriter, r *http.Request) {
})
}
// handleDownloadFileForRunner allows runners to download job files
func (s *Server) handleDownloadFileForRunner(w http.ResponseWriter, r *http.Request) {
// handleDownloadJobContext allows runners to download the job context tar.gz
func (s *Server) handleDownloadJobContext(w http.ResponseWriter, r *http.Request) {
jobID, err := parseID(r, "jobId")
if err != nil {
s.respondError(w, http.StatusBadRequest, err.Error())
return
}
// Get the file path from the wildcard parameter (supports subdirectories)
filePathParam := chi.URLParam(r, "*")
if filePathParam == "" {
s.respondError(w, http.StatusBadRequest, "File path not specified")
return
}
// Remove leading slash if present
filePathParam = strings.TrimPrefix(filePathParam, "/")
// Construct the context file path
contextPath := filepath.Join(s.storage.JobPath(jobID), "context.tar.gz")
// Find the file in the database by matching file_name (which stores relative path)
var filePath string
var storedFileName string
err = s.db.QueryRow(
`SELECT file_path, file_name FROM job_files WHERE job_id = ? AND file_name = ?`,
jobID, filePathParam,
).Scan(&filePath, &storedFileName)
if err == sql.ErrNoRows {
s.respondError(w, http.StatusNotFound, "File not found")
return
}
if err != nil {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query file: %v", err))
// Check if context file exists
if !s.storage.FileExists(contextPath) {
log.Printf("Context archive not found for job %d", jobID)
s.respondError(w, http.StatusNotFound, "Context archive not found. The file may not have been uploaded successfully.")
return
}
// Open and serve file
file, err := s.storage.GetFile(filePath)
file, err := s.storage.GetFile(contextPath)
if err != nil {
s.respondError(w, http.StatusNotFound, "File not found on disk")
s.respondError(w, http.StatusNotFound, "Context file not found on disk")
return
}
defer file.Close()
// Use the stored file name for the download (preserves original filename)
downloadFileName := filepath.Base(storedFileName)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
// Set appropriate headers for tar.gz file
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", "attachment; filename=context.tar.gz")
// Stream the file to the response
io.Copy(w, file)
}
@@ -488,6 +473,43 @@ func (s *Server) handleGetJobFilesForRunner(w http.ResponseWriter, r *http.Reque
s.respondJSON(w, http.StatusOK, files)
}
// handleGetJobMetadataForRunner allows runners to get job metadata
func (s *Server) handleGetJobMetadataForRunner(w http.ResponseWriter, r *http.Request) {
jobID, err := parseID(r, "jobId")
if err != nil {
s.respondError(w, http.StatusBadRequest, err.Error())
return
}
var blendMetadataJSON sql.NullString
err = s.db.QueryRow(
`SELECT blend_metadata FROM jobs WHERE id = ?`,
jobID,
).Scan(&blendMetadataJSON)
if err == sql.ErrNoRows {
s.respondError(w, http.StatusNotFound, "Job not found")
return
}
if err != nil {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query job: %v", err))
return
}
if !blendMetadataJSON.Valid || blendMetadataJSON.String == "" {
s.respondJSON(w, http.StatusOK, nil)
return
}
var metadata types.BlendMetadata
if err := json.Unmarshal([]byte(blendMetadataJSON.String), &metadata); err != nil {
s.respondError(w, http.StatusInternalServerError, "Failed to parse metadata")
return
}
s.respondJSON(w, http.StatusOK, metadata)
}
// WebSocket message types
type WSMessage struct {
Type string `json:"type"`
@@ -1020,7 +1042,7 @@ func (s *Server) updateJobStatusFromTasks(jobID int64) {
log.Printf("Updated job %d status to %s (progress: %.1f%%, completed tasks: %d/%d)", jobID, jobStatus, progress, completedTasks, totalTasks)
}
if outputFormatStr == "MP4" {
if outputFormatStr == "EXR_264_MP4" || outputFormatStr == "EXR_AV1_MP4" {
// Check if a video generation task already exists for this job (any status)
var existingVideoTask int
s.db.QueryRow(
@@ -1603,6 +1625,9 @@ func (s *Server) assignTaskToRunner(runnerID int64, taskID int64) error {
task.JobName = jobName
if outputFormat.Valid {
task.OutputFormat = outputFormat.String
log.Printf("Task %d assigned with output_format: '%s' (from job %d)", taskID, outputFormat.String, task.JobID)
} else {
log.Printf("Task %d assigned with no output_format (job %d)", taskID, task.JobID)
}
task.TaskType = taskType