// Package workspace manages runner workspace directories. package workspace import ( "fmt" "log" "os" "path/filepath" "strings" ) // Manager handles workspace directory operations. type Manager struct { baseDir string runnerName string } // NewManager creates a new workspace manager. func NewManager(runnerName string) *Manager { m := &Manager{ runnerName: sanitizeName(runnerName), } m.init() return m } func sanitizeName(name string) string { name = strings.ReplaceAll(name, " ", "_") name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "\\", "_") name = strings.ReplaceAll(name, ":", "_") return name } func (m *Manager) init() { // Prefer current directory if writable, otherwise use temp baseDir := os.TempDir() if cwd, err := os.Getwd(); err == nil { baseDir = cwd } m.baseDir = filepath.Join(baseDir, "jiggablend-workspaces", m.runnerName) if err := os.MkdirAll(m.baseDir, 0755); err != nil { log.Printf("Warning: Failed to create workspace directory %s: %v", m.baseDir, err) // Fallback to temp directory m.baseDir = filepath.Join(os.TempDir(), "jiggablend-workspaces", m.runnerName) if err := os.MkdirAll(m.baseDir, 0755); err != nil { log.Printf("Error: Failed to create fallback workspace directory: %v", err) // Last resort m.baseDir = filepath.Join(os.TempDir(), "jiggablend-runner") os.MkdirAll(m.baseDir, 0755) } } log.Printf("Runner workspace initialized at: %s", m.baseDir) } // BaseDir returns the base workspace directory. func (m *Manager) BaseDir() string { return m.baseDir } // JobDir returns the directory for a specific job. func (m *Manager) JobDir(jobID int64) string { return filepath.Join(m.baseDir, fmt.Sprintf("job-%d", jobID)) } // VideoDir returns the directory for encoding. func (m *Manager) VideoDir(jobID int64) string { return filepath.Join(m.baseDir, fmt.Sprintf("job-%d-video", jobID)) } // BlenderDir returns the directory for Blender installations. func (m *Manager) BlenderDir() string { return filepath.Join(m.baseDir, "blender-versions") } // CreateJobDir creates and returns the job directory. func (m *Manager) CreateJobDir(jobID int64) (string, error) { dir := m.JobDir(jobID) if err := os.MkdirAll(dir, 0755); err != nil { return "", fmt.Errorf("failed to create job directory: %w", err) } return dir, nil } // CreateVideoDir creates and returns the encode directory. func (m *Manager) CreateVideoDir(jobID int64) (string, error) { dir := m.VideoDir(jobID) if err := os.MkdirAll(dir, 0755); err != nil { return "", fmt.Errorf("failed to create video directory: %w", err) } return dir, nil } // CleanupJobDir removes a job directory. func (m *Manager) CleanupJobDir(jobID int64) error { dir := m.JobDir(jobID) return os.RemoveAll(dir) } // CleanupVideoDir removes an encode directory. func (m *Manager) CleanupVideoDir(jobID int64) error { dir := m.VideoDir(jobID) return os.RemoveAll(dir) } // Cleanup removes the entire workspace directory. func (m *Manager) Cleanup() { if m.baseDir != "" { log.Printf("Cleaning up workspace directory: %s", m.baseDir) if err := os.RemoveAll(m.baseDir); err != nil { log.Printf("Warning: Failed to remove workspace directory %s: %v", m.baseDir, err) } else { log.Printf("Successfully removed workspace directory: %s", m.baseDir) } } // Also clean up any orphaned jiggablend directories cleanupOrphanedWorkspaces() } // cleanupOrphanedWorkspaces removes any jiggablend workspace directories // that might be left behind from previous runs or crashes. func cleanupOrphanedWorkspaces() { log.Printf("Cleaning up orphaned jiggablend workspace directories...") dirsToCheck := []string{".", os.TempDir()} for _, baseDir := range dirsToCheck { workspaceDir := filepath.Join(baseDir, "jiggablend-workspaces") if _, err := os.Stat(workspaceDir); err == nil { log.Printf("Removing orphaned workspace directory: %s", workspaceDir) if err := os.RemoveAll(workspaceDir); err != nil { log.Printf("Warning: Failed to remove workspace directory %s: %v", workspaceDir, err) } else { log.Printf("Successfully removed workspace directory: %s", workspaceDir) } } } } // FindBlendFiles finds all .blend files in a directory. func FindBlendFiles(dir string) ([]string, error) { var blendFiles []string err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".blend") { // Check it's not a Blender save file (.blend1, .blend2, etc.) lower := strings.ToLower(info.Name()) idx := strings.LastIndex(lower, ".blend") if idx != -1 { suffix := lower[idx+len(".blend"):] isSaveFile := false if len(suffix) > 0 { isSaveFile = true for _, r := range suffix { if r < '0' || r > '9' { isSaveFile = false break } } } if !isSaveFile { relPath, _ := filepath.Rel(dir, path) blendFiles = append(blendFiles, relPath) } } } return nil }) return blendFiles, err } // FindFirstBlendFile finds the first .blend file in a directory. func FindFirstBlendFile(dir string) (string, error) { var blendFile string err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".blend") { lower := strings.ToLower(info.Name()) idx := strings.LastIndex(lower, ".blend") if idx != -1 { suffix := lower[idx+len(".blend"):] isSaveFile := false if len(suffix) > 0 { isSaveFile = true for _, r := range suffix { if r < '0' || r > '9' { isSaveFile = false break } } } if !isSaveFile { blendFile = path return filepath.SkipAll } } } return nil }) if err != nil { return "", err } if blendFile == "" { return "", fmt.Errorf("no .blend file found in %s", dir) } return blendFile, nil }