Update .gitignore to include log files and database journal files. Modify go.mod to update dependencies for go-sqlite3 and cloud.google.com/go/compute/metadata. Enhance Makefile to include logging options for manager and runner commands. Introduce new job token handling in auth package and implement database migration scripts. Refactor manager and runner components to improve job processing and metadata extraction. Add support for video preview in frontend components and enhance WebSocket management for channel subscriptions.

This commit is contained in:
2026-01-02 13:55:19 -06:00
parent edc8ea160c
commit 94490237fe
44 changed files with 9463 additions and 7875 deletions

831
internal/manager/blender.go Normal file
View File

@@ -0,0 +1,831 @@
package api
import (
"archive/tar"
"compress/bzip2"
"compress/gzip"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
)
const (
BlenderDownloadBaseURL = "https://download.blender.org/release/"
BlenderVersionCacheTTL = 1 * time.Hour
)
// BlenderVersion represents a parsed Blender version
type BlenderVersion struct {
Major int `json:"major"`
Minor int `json:"minor"`
Patch int `json:"patch"`
Full string `json:"full"` // e.g., "4.2.3"
DirName string `json:"dir_name"` // e.g., "Blender4.2"
Filename string `json:"filename"` // e.g., "blender-4.2.3-linux-x64.tar.xz"
URL string `json:"url"` // Full download URL
}
// BlenderVersionCache caches available Blender versions
type BlenderVersionCache struct {
versions []BlenderVersion
fetchedAt time.Time
mu sync.RWMutex
}
var blenderVersionCache = &BlenderVersionCache{}
// FetchBlenderVersions fetches available Blender versions from download.blender.org
// Returns versions sorted by version number (newest first)
func (s *Manager) FetchBlenderVersions() ([]BlenderVersion, error) {
// Check cache first
blenderVersionCache.mu.RLock()
if time.Since(blenderVersionCache.fetchedAt) < BlenderVersionCacheTTL && len(blenderVersionCache.versions) > 0 {
versions := make([]BlenderVersion, len(blenderVersionCache.versions))
copy(versions, blenderVersionCache.versions)
blenderVersionCache.mu.RUnlock()
return versions, nil
}
blenderVersionCache.mu.RUnlock()
// Fetch from website with timeout
client := &http.Client{
Timeout: WSWriteDeadline,
}
resp, err := client.Get(BlenderDownloadBaseURL)
if err != nil {
return nil, fmt.Errorf("failed to fetch blender releases: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch blender releases: status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Parse directory listing for Blender version folders
// Looking for patterns like href="Blender4.2/" or href="Blender3.6/"
dirPattern := regexp.MustCompile(`href="Blender(\d+)\.(\d+)/"`)
log.Printf("Fetching Blender versions from %s", BlenderDownloadBaseURL)
matches := dirPattern.FindAllStringSubmatch(string(body), -1)
// Fetch sub-versions concurrently to speed up the process
type versionResult struct {
versions []BlenderVersion
err error
}
results := make(chan versionResult, len(matches))
var wg sync.WaitGroup
for _, match := range matches {
if len(match) < 3 {
continue
}
major := 0
minor := 0
fmt.Sscanf(match[1], "%d", &major)
fmt.Sscanf(match[2], "%d", &minor)
// Skip very old versions (pre-2.80)
if major < 2 || (major == 2 && minor < 80) {
continue
}
dirName := fmt.Sprintf("Blender%d.%d", major, minor)
// Fetch the specific version directory concurrently
wg.Add(1)
go func(dn string, maj, min int) {
defer wg.Done()
subVersions, err := fetchSubVersions(dn, maj, min)
results <- versionResult{versions: subVersions, err: err}
}(dirName, major, minor)
}
// Close results channel when all goroutines complete
go func() {
wg.Wait()
close(results)
}()
var versions []BlenderVersion
for result := range results {
if result.err != nil {
log.Printf("Warning: failed to fetch sub-versions: %v", result.err)
continue
}
versions = append(versions, result.versions...)
}
// Sort by version (newest first)
sort.Slice(versions, func(i, j int) bool {
if versions[i].Major != versions[j].Major {
return versions[i].Major > versions[j].Major
}
if versions[i].Minor != versions[j].Minor {
return versions[i].Minor > versions[j].Minor
}
return versions[i].Patch > versions[j].Patch
})
// Update cache
blenderVersionCache.mu.Lock()
blenderVersionCache.versions = versions
blenderVersionCache.fetchedAt = time.Now()
blenderVersionCache.mu.Unlock()
return versions, nil
}
// fetchSubVersions fetches specific version files from a Blender release directory
func fetchSubVersions(dirName string, major, minor int) ([]BlenderVersion, error) {
url := BlenderDownloadBaseURL + dirName + "/"
client := &http.Client{
Timeout: WSWriteDeadline,
}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Look for linux 64-bit tar.xz/bz2 files
// Various naming conventions across versions:
// - Modern (2.93+): blender-4.2.3-linux-x64.tar.xz
// - 2.83 early: blender-2.83.0-linux64.tar.xz
// - 2.80-2.82: blender-2.80-linux-glibc217-x86_64.tar.bz2
// Skip: rc versions, alpha/beta, i686 (32-bit)
filePatterns := []*regexp.Regexp{
// Modern format: blender-X.Y.Z-linux-x64.tar.xz
regexp.MustCompile(`blender-(\d+)\.(\d+)\.(\d+)-linux-x64\.tar\.(xz|bz2)`),
// Older format: blender-X.Y.Z-linux64.tar.xz
regexp.MustCompile(`blender-(\d+)\.(\d+)\.(\d+)-linux64\.tar\.(xz|bz2)`),
// glibc format: blender-X.Y.Z-linux-glibc217-x86_64.tar.bz2 (prefer glibc217 for compatibility)
regexp.MustCompile(`blender-(\d+)\.(\d+)\.(\d+)-linux-glibc217-x86_64\.tar\.(xz|bz2)`),
}
var versions []BlenderVersion
seen := make(map[string]bool)
for _, filePattern := range filePatterns {
matches := filePattern.FindAllStringSubmatch(string(body), -1)
for _, match := range matches {
if len(match) < 5 {
continue
}
patch := 0
fmt.Sscanf(match[3], "%d", &patch)
full := fmt.Sprintf("%d.%d.%d", major, minor, patch)
if seen[full] {
continue
}
seen[full] = true
filename := match[0]
versions = append(versions, BlenderVersion{
Major: major,
Minor: minor,
Patch: patch,
Full: full,
DirName: dirName,
Filename: filename,
URL: url + filename,
})
}
}
return versions, nil
}
// GetLatestBlenderForMajorMinor returns the latest patch version for a given major.minor
// If exact match not found, uses fuzzy matching to find the closest available version
func (s *Manager) GetLatestBlenderForMajorMinor(major, minor int) (*BlenderVersion, error) {
versions, err := s.FetchBlenderVersions()
if err != nil {
return nil, err
}
if len(versions) == 0 {
return nil, fmt.Errorf("no blender versions available")
}
// Try exact match first - find the highest patch for this major.minor
var exactMatch *BlenderVersion
for i := range versions {
v := &versions[i]
if v.Major == major && v.Minor == minor {
if exactMatch == nil || v.Patch > exactMatch.Patch {
exactMatch = v
}
}
}
if exactMatch != nil {
log.Printf("Found Blender %d.%d.%d for requested %d.%d", exactMatch.Major, exactMatch.Minor, exactMatch.Patch, major, minor)
return exactMatch, nil
}
// Fuzzy matching: find closest version
// Priority: same major with closest minor > closest major
log.Printf("No exact match for Blender %d.%d, using fuzzy matching", major, minor)
var bestMatch *BlenderVersion
bestScore := -1000000 // Large negative number
for i := range versions {
v := &versions[i]
score := 0
if v.Major == major {
// Same major version - prefer this
score = 10000
// Prefer lower minor versions (more stable/compatible)
// but not too far back
minorDiff := minor - v.Minor
if minorDiff >= 0 {
// v.Minor <= minor (older or same) - prefer closer
score += 1000 - minorDiff*10
} else {
// v.Minor > minor (newer) - less preferred but acceptable
score += 500 + minorDiff*10
}
// Higher patch is better
score += v.Patch
} else {
// Different major - less preferred
majorDiff := major - v.Major
if majorDiff > 0 {
// v.Major < major (older major) - acceptable fallback
score = 5000 - majorDiff*1000 + v.Minor*10 + v.Patch
} else {
// v.Major > major (newer major) - avoid if possible
score = -majorDiff * 1000
}
}
if score > bestScore {
bestScore = score
bestMatch = v
}
}
if bestMatch != nil {
log.Printf("Fuzzy match: requested %d.%d, using %d.%d.%d", major, minor, bestMatch.Major, bestMatch.Minor, bestMatch.Patch)
return bestMatch, nil
}
return nil, fmt.Errorf("no blender version found for %d.%d", major, minor)
}
// GetBlenderArchivePath returns the path to the cached blender archive for a specific version
// Downloads from blender.org and decompresses to .tar if not already cached
// The manager caches as uncompressed .tar to save decompression time on runners
func (s *Manager) GetBlenderArchivePath(version *BlenderVersion) (string, error) {
// Base directory for blender archives
blenderDir := filepath.Join(s.storage.BasePath(), "blender-versions")
if err := os.MkdirAll(blenderDir, 0755); err != nil {
return "", fmt.Errorf("failed to create blender directory: %w", err)
}
// Cache as uncompressed .tar for faster runner downloads
// Convert filename like "blender-4.2.3-linux-x64.tar.xz" to "blender-4.2.3-linux-x64.tar"
tarFilename := version.Filename
tarFilename = strings.TrimSuffix(tarFilename, ".xz")
tarFilename = strings.TrimSuffix(tarFilename, ".bz2")
archivePath := filepath.Join(blenderDir, tarFilename)
// Check if already cached as .tar
if _, err := os.Stat(archivePath); err == nil {
log.Printf("Using cached Blender %s at %s", version.Full, archivePath)
// Clean up any extracted folders that might exist
s.cleanupExtractedBlenderFolders(blenderDir, version)
return archivePath, nil
}
// Need to download and decompress
log.Printf("Downloading Blender %s from %s", version.Full, version.URL)
client := &http.Client{
Timeout: 0, // No timeout for large downloads
}
resp, err := client.Get(version.URL)
if err != nil {
return "", fmt.Errorf("failed to download blender: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download blender: status %d", resp.StatusCode)
}
// Download to temp file first
compressedPath := filepath.Join(blenderDir, "download-"+version.Filename)
compressedFile, err := os.Create(compressedPath)
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
if _, err := io.Copy(compressedFile, resp.Body); err != nil {
compressedFile.Close()
os.Remove(compressedPath)
return "", fmt.Errorf("failed to download blender: %w", err)
}
compressedFile.Close()
log.Printf("Downloaded Blender %s, decompressing to .tar...", version.Full)
// Decompress to .tar
if err := decompressToTar(compressedPath, archivePath); err != nil {
os.Remove(compressedPath)
os.Remove(archivePath)
return "", fmt.Errorf("failed to decompress blender archive: %w", err)
}
// Remove compressed file
os.Remove(compressedPath)
// Clean up any extracted folders for this version (if they exist)
s.cleanupExtractedBlenderFolders(blenderDir, version)
log.Printf("Blender %s cached at %s", version.Full, archivePath)
return archivePath, nil
}
// decompressToTar decompresses a .tar.xz or .tar.bz2 file to a plain .tar file
func decompressToTar(compressedPath, tarPath string) error {
if strings.HasSuffix(compressedPath, ".tar.xz") {
// Use xz command for decompression
cmd := exec.Command("xz", "-d", "-k", "-c", compressedPath)
outFile, err := os.Create(tarPath)
if err != nil {
return err
}
defer outFile.Close()
cmd.Stdout = outFile
if err := cmd.Run(); err != nil {
return fmt.Errorf("xz decompression failed: %w", err)
}
return nil
} else if strings.HasSuffix(compressedPath, ".tar.bz2") {
// Use bzip2 for decompression
inFile, err := os.Open(compressedPath)
if err != nil {
return err
}
defer inFile.Close()
bzReader := bzip2.NewReader(inFile)
outFile, err := os.Create(tarPath)
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, bzReader); err != nil {
return fmt.Errorf("bzip2 decompression failed: %w", err)
}
return nil
}
return fmt.Errorf("unsupported compression format: %s", compressedPath)
}
// cleanupExtractedBlenderFolders removes any extracted Blender folders for the given version
// This ensures we only keep the .tar file and not extracted folders
func (s *Manager) cleanupExtractedBlenderFolders(blenderDir string, version *BlenderVersion) {
// Look for folders matching the version (e.g., "4.2.3", "2.83.20")
versionDirs := []string{
filepath.Join(blenderDir, version.Full), // e.g., "4.2.3"
filepath.Join(blenderDir, fmt.Sprintf("%d.%d", version.Major, version.Minor)), // e.g., "4.2"
}
for _, dir := range versionDirs {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
log.Printf("Removing extracted Blender folder: %s", dir)
if err := os.RemoveAll(dir); err != nil {
log.Printf("Warning: failed to remove extracted folder %s: %v", dir, err)
} else {
log.Printf("Removed extracted Blender folder: %s", dir)
}
}
}
}
// ParseBlenderVersionFromFile parses the Blender version that a .blend file was saved with
// This reads the file header to determine the version
func ParseBlenderVersionFromFile(blendPath string) (major, minor int, err error) {
file, err := os.Open(blendPath)
if err != nil {
return 0, 0, fmt.Errorf("failed to open blend file: %w", err)
}
defer file.Close()
return ParseBlenderVersionFromReader(file)
}
// ParseBlenderVersionFromReader parses the Blender version from a reader
// Useful for reading from uploaded files without saving to disk first
func ParseBlenderVersionFromReader(r io.ReadSeeker) (major, minor int, err error) {
// Read the first 12 bytes of the blend file header
// Format: BLENDER-v<major><minor><patch> or BLENDER_v<major><minor><patch>
// The header is: "BLENDER" (7 bytes) + pointer size (1 byte: '-' for 64-bit, '_' for 32-bit)
// + endianness (1 byte: 'v' for little-endian, 'V' for big-endian)
// + version (3 bytes: e.g., "402" for 4.02)
header := make([]byte, 12)
n, err := r.Read(header)
if err != nil || n < 12 {
return 0, 0, fmt.Errorf("failed to read blend file header: %w", err)
}
// Check for BLENDER magic
if string(header[:7]) != "BLENDER" {
// Might be compressed - try to decompress
r.Seek(0, 0)
return parseCompressedBlendVersion(r)
}
// Parse version from bytes 9-11 (3 digits)
versionStr := string(header[9:12])
var vMajor, vMinor int
// Version format changed in Blender 3.0
// Pre-3.0: "279" = 2.79, "280" = 2.80
// 3.0+: "300" = 3.0, "402" = 4.02, "410" = 4.10
if len(versionStr) == 3 {
// First digit is major version
fmt.Sscanf(string(versionStr[0]), "%d", &vMajor)
// Next two digits are minor version
fmt.Sscanf(versionStr[1:3], "%d", &vMinor)
}
return vMajor, vMinor, nil
}
// parseCompressedBlendVersion handles gzip and zstd compressed blend files
func parseCompressedBlendVersion(r io.ReadSeeker) (major, minor int, err error) {
// Check for compression magic bytes
magic := make([]byte, 4)
if _, err := r.Read(magic); err != nil {
return 0, 0, err
}
r.Seek(0, 0)
if magic[0] == 0x1f && magic[1] == 0x8b {
// gzip compressed
gzReader, err := gzip.NewReader(r)
if err != nil {
return 0, 0, fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzReader.Close()
header := make([]byte, 12)
n, err := gzReader.Read(header)
if err != nil || n < 12 {
return 0, 0, fmt.Errorf("failed to read compressed blend header: %w", err)
}
if string(header[:7]) != "BLENDER" {
return 0, 0, fmt.Errorf("invalid blend file format")
}
versionStr := string(header[9:12])
var vMajor, vMinor int
if len(versionStr) == 3 {
fmt.Sscanf(string(versionStr[0]), "%d", &vMajor)
fmt.Sscanf(versionStr[1:3], "%d", &vMinor)
}
return vMajor, vMinor, nil
}
// Check for zstd magic (Blender 3.0+): 0x28 0xB5 0x2F 0xFD
if magic[0] == 0x28 && magic[1] == 0xb5 && magic[2] == 0x2f && magic[3] == 0xfd {
return parseZstdBlendVersion(r)
}
return 0, 0, fmt.Errorf("unknown blend file format")
}
// parseZstdBlendVersion handles zstd-compressed blend files (Blender 3.0+)
// Uses zstd command line tool since Go doesn't have native zstd support
func parseZstdBlendVersion(r io.ReadSeeker) (major, minor int, err error) {
r.Seek(0, 0)
// We need to decompress just enough to read the header
// Use zstd command to decompress from stdin
cmd := exec.Command("zstd", "-d", "-c")
cmd.Stdin = r
stdout, err := cmd.StdoutPipe()
if err != nil {
return 0, 0, fmt.Errorf("failed to create zstd stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return 0, 0, fmt.Errorf("failed to start zstd decompression: %w", err)
}
// Read just the header (12 bytes)
header := make([]byte, 12)
n, readErr := io.ReadFull(stdout, header)
// Kill the process early - we only need the header
cmd.Process.Kill()
cmd.Wait()
if readErr != nil || n < 12 {
return 0, 0, fmt.Errorf("failed to read zstd compressed blend header: %v", readErr)
}
if string(header[:7]) != "BLENDER" {
return 0, 0, fmt.Errorf("invalid blend file format in zstd archive")
}
versionStr := string(header[9:12])
var vMajor, vMinor int
if len(versionStr) == 3 {
fmt.Sscanf(string(versionStr[0]), "%d", &vMajor)
fmt.Sscanf(versionStr[1:3], "%d", &vMinor)
}
return vMajor, vMinor, nil
}
// handleGetBlenderVersions returns available Blender versions
func (s *Manager) handleGetBlenderVersions(w http.ResponseWriter, r *http.Request) {
versions, err := s.FetchBlenderVersions()
if err != nil {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to fetch blender versions: %v", err))
return
}
// Group by major.minor for easier frontend display
type VersionGroup struct {
MajorMinor string `json:"major_minor"`
Latest BlenderVersion `json:"latest"`
All []BlenderVersion `json:"all"`
}
groups := make(map[string]*VersionGroup)
for _, v := range versions {
key := fmt.Sprintf("%d.%d", v.Major, v.Minor)
if groups[key] == nil {
groups[key] = &VersionGroup{
MajorMinor: key,
Latest: v, // First one is latest due to sorting
All: []BlenderVersion{v},
}
} else {
groups[key].All = append(groups[key].All, v)
}
}
// Convert to slice and sort by version
var groupedResult []VersionGroup
for _, g := range groups {
groupedResult = append(groupedResult, *g)
}
sort.Slice(groupedResult, func(i, j int) bool {
// Parse major.minor for comparison
var iMaj, iMin, jMaj, jMin int
fmt.Sscanf(groupedResult[i].MajorMinor, "%d.%d", &iMaj, &iMin)
fmt.Sscanf(groupedResult[j].MajorMinor, "%d.%d", &jMaj, &jMin)
if iMaj != jMaj {
return iMaj > jMaj
}
return iMin > jMin
})
// Return both flat list and grouped for flexibility
response := map[string]interface{}{
"versions": versions, // Flat list of all versions (newest first)
"grouped": groupedResult, // Grouped by major.minor
}
s.respondJSON(w, http.StatusOK, response)
}
// handleDownloadBlender serves a cached Blender archive to runners
func (s *Manager) handleDownloadBlender(w http.ResponseWriter, r *http.Request) {
version := r.URL.Query().Get("version")
if version == "" {
s.respondError(w, http.StatusBadRequest, "version parameter required")
return
}
// Parse version string (e.g., "4.2.3" or "4.2")
var major, minor, patch int
parts := strings.Split(version, ".")
if len(parts) < 2 {
s.respondError(w, http.StatusBadRequest, "invalid version format, expected major.minor or major.minor.patch")
return
}
fmt.Sscanf(parts[0], "%d", &major)
fmt.Sscanf(parts[1], "%d", &minor)
if len(parts) >= 3 {
fmt.Sscanf(parts[2], "%d", &patch)
}
// Find the version
var blenderVersion *BlenderVersion
if len(parts) >= 3 {
// Exact patch version requested - find it
versions, err := s.FetchBlenderVersions()
if err != nil {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to fetch versions: %v", err))
return
}
for _, v := range versions {
if v.Major == major && v.Minor == minor && v.Patch == patch {
blenderVersion = &v
break
}
}
if blenderVersion == nil {
s.respondError(w, http.StatusNotFound, fmt.Sprintf("blender version %s not found", version))
return
}
} else {
// Major.minor only - use helper to get latest patch version
var err error
blenderVersion, err = s.GetLatestBlenderForMajorMinor(major, minor)
if err != nil {
s.respondError(w, http.StatusNotFound, fmt.Sprintf("blender version %s not found: %v", version, err))
return
}
}
// Get or download the archive
archivePath, err := s.GetBlenderArchivePath(blenderVersion)
if err != nil {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get blender archive: %v", err))
return
}
// Serve the file
file, err := os.Open(archivePath)
if err != nil {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to open archive: %v", err))
return
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to stat archive: %v", err))
return
}
// Filename is now .tar (decompressed)
tarFilename := blenderVersion.Filename
tarFilename = strings.TrimSuffix(tarFilename, ".xz")
tarFilename = strings.TrimSuffix(tarFilename, ".bz2")
w.Header().Set("Content-Type", "application/x-tar")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", tarFilename))
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
w.Header().Set("X-Blender-Version", blenderVersion.Full)
io.Copy(w, file)
}
// Unused functions from extraction - keeping for reference but not needed on manager
var _ = extractBlenderArchive
var _ = extractTarXz
var _ = extractTar
// extractBlenderArchive extracts a blender archive (already decompressed to .tar by GetBlenderArchivePath)
func extractBlenderArchive(archivePath string, version *BlenderVersion, destDir string) error {
file, err := os.Open(archivePath)
if err != nil {
return err
}
defer file.Close()
// The archive is already decompressed to .tar by GetBlenderArchivePath
// Just extract it directly
if strings.HasSuffix(archivePath, ".tar") {
tarReader := tar.NewReader(file)
return extractTar(tarReader, version, destDir)
}
// Fallback for any other format (shouldn't happen with current flow)
if strings.HasSuffix(archivePath, ".tar.xz") {
return extractTarXz(archivePath, version, destDir)
} else if strings.HasSuffix(archivePath, ".tar.bz2") {
bzReader := bzip2.NewReader(file)
tarReader := tar.NewReader(bzReader)
return extractTar(tarReader, version, destDir)
}
return fmt.Errorf("unsupported archive format: %s", archivePath)
}
// extractTarXz extracts a tar.xz archive using the xz command
func extractTarXz(archivePath string, version *BlenderVersion, destDir string) error {
versionDir := filepath.Join(destDir, version.Full)
if err := os.MkdirAll(versionDir, 0755); err != nil {
return err
}
cmd := exec.Command("tar", "-xJf", archivePath, "-C", versionDir, "--strip-components=1")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("tar extraction failed: %v, output: %s", err, string(output))
}
return nil
}
// extractTar extracts files from a tar reader
func extractTar(tarReader *tar.Reader, version *BlenderVersion, destDir string) error {
versionDir := filepath.Join(destDir, version.Full)
if err := os.MkdirAll(versionDir, 0755); err != nil {
return err
}
stripPrefix := ""
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if stripPrefix == "" {
parts := strings.SplitN(header.Name, "/", 2)
if len(parts) > 0 {
stripPrefix = parts[0] + "/"
}
}
name := strings.TrimPrefix(header.Name, stripPrefix)
if name == "" {
continue
}
targetPath := filepath.Join(versionDir, name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return err
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
return err
}
outFile.Close()
case tar.TypeSymlink:
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
if err := os.Symlink(header.Linkname, targetPath); err != nil {
return err
}
}
}
return nil
}