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:
2026-07-12 10:01:15 -05:00
parent a3defe5cf6
commit 1a69fcfd04
47 changed files with 2718 additions and 402 deletions
+92 -55
View File
@@ -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 {