Refactor installation and runner scripts for enhanced usability and security
- Updated the installation script to clarify that production wrappers do not include fixed test secrets, improving user guidance for local testing. - Modified the jiggablend-manager and jiggablend-runner scripts to remove fixed test configurations, emphasizing the use of local test credentials via `make init-test`. - Enhanced error handling in the runner script to ensure that an API key is provided, improving security and user feedback. - Added new flags and options for the runner, including sandboxing capabilities and GPU ray tracing control, enhancing flexibility for users. - Improved README documentation to reflect changes in usage and configuration, ensuring users have clear instructions for setup and execution.
This commit is contained in:
+92
-55
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
authpkg "jiggablend/internal/auth"
|
||||
"jiggablend/internal/runner/blender"
|
||||
"jiggablend/internal/storage"
|
||||
"jiggablend/pkg/executils"
|
||||
"jiggablend/pkg/scripts"
|
||||
"jiggablend/pkg/types"
|
||||
@@ -232,7 +233,11 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var uploadSession *UploadSession
|
||||
var tempContextPath string
|
||||
if req.UploadSessionID != nil && *req.UploadSessionID != "" {
|
||||
if req.UploadSessionID == nil || *req.UploadSessionID == "" {
|
||||
s.respondError(w, http.StatusBadRequest, "upload_session_id is required: upload a blend/zip and complete processing before creating a render job")
|
||||
return
|
||||
}
|
||||
{
|
||||
var validateErr error
|
||||
uploadSession, tempContextPath, validateErr = s.validateUploadSessionForJobCreation(*req.UploadSessionID, userID)
|
||||
if validateErr != nil {
|
||||
@@ -246,33 +251,23 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Store render settings, unhide_objects, enable_execution, and blender_version in blend_metadata if provided.
|
||||
var blendMetadataJSON *string
|
||||
if req.RenderSettings != nil || req.UnhideObjects != nil || req.EnableExecution != nil || req.BlenderVersion != nil || req.OutputFormat != nil {
|
||||
metadata := types.BlendMetadata{
|
||||
FrameStart: *req.FrameStart,
|
||||
FrameEnd: *req.FrameEnd,
|
||||
RenderSettings: types.RenderSettings{},
|
||||
UnhideObjects: req.UnhideObjects,
|
||||
EnableExecution: req.EnableExecution,
|
||||
}
|
||||
if req.RenderSettings != nil {
|
||||
metadata.RenderSettings = *req.RenderSettings
|
||||
}
|
||||
// Always set output_format in metadata from job's output_format field
|
||||
if req.OutputFormat != nil {
|
||||
metadata.RenderSettings.OutputFormat = *req.OutputFormat
|
||||
}
|
||||
if req.BlenderVersion != nil {
|
||||
metadata.BlenderVersion = *req.BlenderVersion
|
||||
}
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err == nil {
|
||||
metadataStr := string(metadataBytes)
|
||||
blendMetadataJSON = &metadataStr
|
||||
}
|
||||
// Merge upload analysis metadata with explicit job creation overrides.
|
||||
uploadMeta := uploadSessionMetadata(uploadSession)
|
||||
mergedMetadata, mergeErr := mergeBlendMetadataForJobCreate(uploadMeta, &req)
|
||||
if mergeErr != nil {
|
||||
s.respondError(w, http.StatusBadRequest, mergeErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var blendMetadataJSON *string
|
||||
metadataBytes, err := json.Marshal(mergedMetadata)
|
||||
if err != nil {
|
||||
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to marshal job metadata: %v", err))
|
||||
return
|
||||
}
|
||||
metadataStr := string(metadataBytes)
|
||||
blendMetadataJSON = &metadataStr
|
||||
|
||||
log.Printf("Creating render job with output_format: '%s' (from user selection)", *req.OutputFormat)
|
||||
var jobID int64
|
||||
err = s.db.With(func(conn *sql.DB) error {
|
||||
@@ -402,17 +397,12 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
s.uploadSessionsMu.Lock()
|
||||
delete(s.uploadSessions, *req.UploadSessionID)
|
||||
s.uploadSessionsMu.Unlock()
|
||||
} else {
|
||||
log.Printf("Warning: No upload session ID provided for job %d - job created without input files", jobID)
|
||||
}
|
||||
|
||||
// Only create render tasks for render jobs
|
||||
if req.JobType == types.JobTypeRender {
|
||||
// Determine task timeout based on output format
|
||||
// Render tasks always use render timeout; encode tasks use video encode timeout.
|
||||
taskTimeout := s.renderTimeout
|
||||
if *req.OutputFormat == "EXR_264_MP4" || *req.OutputFormat == "EXR_AV1_MP4" || *req.OutputFormat == "EXR_VP9_WEBM" {
|
||||
taskTimeout = s.videoEncodeTimeout
|
||||
}
|
||||
|
||||
// Create tasks for the job (batch INSERT in a single transaction)
|
||||
// Chunk job frame range by frames_per_render_task config
|
||||
@@ -1393,11 +1383,18 @@ func (s *Manager) handleUploadJobFile(w http.ResponseWriter, r *http.Request) {
|
||||
var mainBlendFile string
|
||||
var extractedFiles []string
|
||||
|
||||
// Sanitize once — all disk paths and exclude lists must use this basename
|
||||
safeName, err := storage.SanitizeFilename(header.Filename)
|
||||
if err != nil {
|
||||
s.respondError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a ZIP file
|
||||
if strings.HasSuffix(strings.ToLower(header.Filename), ".zip") {
|
||||
log.Printf("Processing ZIP file '%s' for job %d", header.Filename, jobID)
|
||||
// Save ZIP to temporary directory
|
||||
zipPath := filepath.Join(tmpDir, header.Filename)
|
||||
if strings.HasSuffix(strings.ToLower(safeName), ".zip") {
|
||||
log.Printf("Processing ZIP file '%s' for job %d", safeName, jobID)
|
||||
// Save ZIP to temporary directory (basename only — no path traversal)
|
||||
zipPath := filepath.Join(tmpDir, safeName)
|
||||
log.Printf("Creating ZIP file at: %s", zipPath)
|
||||
zipFile, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
@@ -1428,8 +1425,13 @@ func (s *Manager) handleUploadJobFile(w http.ResponseWriter, r *http.Request) {
|
||||
// Find main blend file (check for user selection first, then auto-detect)
|
||||
mainBlendParam := r.FormValue("main_blend_file")
|
||||
if mainBlendParam != "" {
|
||||
// User specified main blend file
|
||||
mainBlendFile = filepath.Join(tmpDir, mainBlendParam)
|
||||
// User specified main blend file — must stay under tmpDir
|
||||
resolved, err := storage.SafePathUnderRoot(tmpDir, mainBlendParam)
|
||||
if err != nil {
|
||||
s.respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid main blend file path: %v", err))
|
||||
return
|
||||
}
|
||||
mainBlendFile = resolved
|
||||
if _, err := os.Stat(mainBlendFile); err != nil {
|
||||
s.respondError(w, http.StatusBadRequest, fmt.Sprintf("Specified main blend file not found: %s", mainBlendParam))
|
||||
return
|
||||
@@ -1472,7 +1474,7 @@ func (s *Manager) handleUploadJobFile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
} else {
|
||||
// Regular file upload (not ZIP) - save to temporary directory
|
||||
filePath := filepath.Join(tmpDir, header.Filename)
|
||||
filePath := filepath.Join(tmpDir, safeName)
|
||||
outFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create file: %v", err))
|
||||
@@ -1496,7 +1498,7 @@ func (s *Manager) handleUploadJobFile(w http.ResponseWriter, r *http.Request) {
|
||||
fileReader.Close()
|
||||
outFile.Close()
|
||||
|
||||
if strings.HasSuffix(strings.ToLower(header.Filename), ".blend") {
|
||||
if strings.HasSuffix(strings.ToLower(safeName), ".blend") {
|
||||
mainBlendFile = filePath
|
||||
}
|
||||
}
|
||||
@@ -1504,8 +1506,8 @@ func (s *Manager) handleUploadJobFile(w http.ResponseWriter, r *http.Request) {
|
||||
// Create context archive from temporary directory - this is the primary artifact
|
||||
// Exclude the original uploaded ZIP file (but keep blend files as they're needed for rendering)
|
||||
var excludeFiles []string
|
||||
if strings.HasSuffix(strings.ToLower(header.Filename), ".zip") {
|
||||
excludeFiles = append(excludeFiles, header.Filename)
|
||||
if strings.HasSuffix(strings.ToLower(safeName), ".zip") {
|
||||
excludeFiles = append(excludeFiles, safeName)
|
||||
}
|
||||
contextPath, err := s.storage.CreateJobContextFromDir(tmpDir, jobID, excludeFiles...)
|
||||
if err != nil {
|
||||
@@ -1679,14 +1681,17 @@ func (s *Manager) handleUploadFileForJobCreation(w http.ResponseWriter, r *http.
|
||||
}
|
||||
}
|
||||
|
||||
// Determine file path
|
||||
if strings.HasSuffix(strings.ToLower(header.Filename), ".zip") {
|
||||
filePath = filepath.Join(tmpDir, header.Filename)
|
||||
} else {
|
||||
filePath = filepath.Join(tmpDir, header.Filename)
|
||||
if strings.HasSuffix(strings.ToLower(header.Filename), ".blend") {
|
||||
mainBlendFile = filePath
|
||||
}
|
||||
// Determine file path (basename only — no path traversal)
|
||||
safeName, sanitizeErr := storage.SanitizeFilename(header.Filename)
|
||||
if sanitizeErr != nil {
|
||||
part.Close()
|
||||
os.RemoveAll(tmpDir)
|
||||
s.respondError(w, http.StatusBadRequest, sanitizeErr.Error())
|
||||
return
|
||||
}
|
||||
filePath = filepath.Join(tmpDir, safeName)
|
||||
if strings.HasSuffix(strings.ToLower(safeName), ".blend") {
|
||||
mainBlendFile = filePath
|
||||
}
|
||||
|
||||
// Create file and copy data immediately
|
||||
@@ -1734,7 +1739,18 @@ func (s *Manager) handleUploadFileForJobCreation(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
filename := header.Filename
|
||||
// Use the same sanitized basename that was used when writing the file to tmpDir
|
||||
safeName, sanitizeErr := storage.SanitizeFilename(header.Filename)
|
||||
if sanitizeErr != nil {
|
||||
// Should not happen — we already sanitized when writing the file
|
||||
os.RemoveAll(tmpDir)
|
||||
s.respondError(w, http.StatusBadRequest, sanitizeErr.Error())
|
||||
return
|
||||
}
|
||||
// Prefer the name of the file actually on disk (from the write path above)
|
||||
if filePath != "" {
|
||||
safeName = filepath.Base(filePath)
|
||||
}
|
||||
fileSize := header.Size
|
||||
mainBlendParam := formValues["main_blend_file"]
|
||||
|
||||
@@ -1746,18 +1762,19 @@ func (s *Manager) handleUploadFileForJobCreation(w http.ResponseWriter, r *http.
|
||||
|
||||
response := map[string]interface{}{
|
||||
"session_id": sessionID,
|
||||
"file_name": filename,
|
||||
"file_name": safeName,
|
||||
"file_size": fileSize,
|
||||
"status": "processing",
|
||||
"phase": uploadSessionPhase("processing"),
|
||||
}
|
||||
s.respondJSON(w, http.StatusOK, response)
|
||||
|
||||
go s.runBackgroundUploadProcessing(tmpDir, sessionID, userID, filename, fileSize, mainBlendParam, mainBlendFile)
|
||||
go s.runBackgroundUploadProcessing(tmpDir, sessionID, userID, safeName, fileSize, mainBlendParam, mainBlendFile)
|
||||
}
|
||||
|
||||
// runBackgroundUploadProcessing runs ZIP extraction (if needed), blend detection, context creation, and metadata extraction.
|
||||
// Called in a goroutine after the upload handler returns; updates upload session and broadcasts when done.
|
||||
// filename must be the sanitized basename of the file written under tmpDir (never a raw client path).
|
||||
func (s *Manager) runBackgroundUploadProcessing(tmpDir, sessionID string, userID int64, filename string, fileSize int64, mainBlendParam string, mainBlendFile string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -1769,6 +1786,17 @@ func (s *Manager) runBackgroundUploadProcessing(tmpDir, sessionID string, userID
|
||||
}
|
||||
}()
|
||||
|
||||
// Defense in depth: never Join unsanitized client filenames under tmpDir
|
||||
safeName, err := storage.SanitizeFilename(filename)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("invalid upload filename: %v", err)
|
||||
if ownerUserID, ok := s.failUploadSession(sessionID, errMsg); ok {
|
||||
s.broadcastUploadProgressSync(ownerUserID, sessionID, 0, "error", errMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
filename = safeName
|
||||
|
||||
var processedMainBlendFile string
|
||||
var excludeFiles []string
|
||||
extractedFilesCount := 0
|
||||
@@ -1790,7 +1818,16 @@ func (s *Manager) runBackgroundUploadProcessing(tmpDir, sessionID string, userID
|
||||
log.Printf("Successfully extracted %d files from ZIP", extractedFilesCount)
|
||||
|
||||
if mainBlendParam != "" {
|
||||
processedMainBlendFile = filepath.Join(tmpDir, mainBlendParam)
|
||||
resolved, err := storage.SafePathUnderRoot(tmpDir, mainBlendParam)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Invalid main blend file path: %v", err)
|
||||
errMsg := "Invalid main blend file path: " + mainBlendParam
|
||||
if ownerUserID, ok := s.failUploadSession(sessionID, errMsg); ok {
|
||||
s.broadcastUploadProgressSync(ownerUserID, sessionID, 0, "error", errMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
processedMainBlendFile = resolved
|
||||
if _, err := os.Stat(processedMainBlendFile); err != nil {
|
||||
log.Printf("ERROR: Specified main blend file not found: %s", mainBlendParam)
|
||||
errMsg := "Specified main blend file not found: " + mainBlendParam
|
||||
@@ -1850,7 +1887,7 @@ func (s *Manager) runBackgroundUploadProcessing(tmpDir, sessionID string, userID
|
||||
|
||||
s.broadcastUploadProgressSync(userID, sessionID, 0.4, "creating_context", "Creating context archive...")
|
||||
contextPath := filepath.Join(tmpDir, "context.tar")
|
||||
contextPath, err := s.createContextFromDir(tmpDir, contextPath, excludeFiles...)
|
||||
contextPath, err = s.createContextFromDir(tmpDir, contextPath, excludeFiles...)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Failed to create context archive: %v", err)
|
||||
if ownerUserID, ok := s.failUploadSession(sessionID, err.Error()); ok {
|
||||
|
||||
Reference in New Issue
Block a user