package workspace import ( "archive/tar" "fmt" "io" "log" "os" "path/filepath" "strings" ) // ExtractTar extracts a tar archive from a reader to a directory. func ExtractTar(reader io.Reader, destDir string) error { if err := os.MkdirAll(destDir, 0755); err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } tarReader := tar.NewReader(reader) for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return fmt.Errorf("failed to read tar header: %w", err) } // Sanitize path to prevent directory traversal targetPath := filepath.Join(destDir, header.Name) if !strings.HasPrefix(filepath.Clean(targetPath), filepath.Clean(destDir)+string(os.PathSeparator)) { return fmt.Errorf("invalid file path in tar: %s", header.Name) } switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { return fmt.Errorf("failed to create directory: %w", err) } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { return fmt.Errorf("failed to create parent directory: %w", err) } outFile, err := os.Create(targetPath) if err != nil { return fmt.Errorf("failed to create file: %w", err) } if _, err := io.Copy(outFile, tarReader); err != nil { outFile.Close() return fmt.Errorf("failed to write file: %w", err) } outFile.Close() if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil { log.Printf("Warning: failed to set file permissions: %v", err) } } } return nil } // ExtractTarStripPrefix extracts a tar archive, stripping the top-level directory. // Useful for Blender archives like "blender-4.2.3-linux-x64/". func ExtractTarStripPrefix(reader io.Reader, destDir string) error { if err := os.MkdirAll(destDir, 0755); err != nil { return err } tarReader := tar.NewReader(reader) stripPrefix := "" for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return err } // Determine strip prefix from first entry (e.g., "blender-4.2.3-linux-x64/") if stripPrefix == "" { parts := strings.SplitN(header.Name, "/", 2) if len(parts) > 0 { stripPrefix = parts[0] + "/" } } // Strip the top-level directory name := strings.TrimPrefix(header.Name, stripPrefix) if name == "" { continue } targetPath := filepath.Join(destDir, 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 } os.Remove(targetPath) // Remove existing symlink if present if err := os.Symlink(header.Linkname, targetPath); err != nil { return err } } } return nil } // ExtractTarFile extracts a tar file to a directory. func ExtractTarFile(tarPath, destDir string) error { file, err := os.Open(tarPath) if err != nil { return fmt.Errorf("failed to open tar file: %w", err) } defer file.Close() return ExtractTar(file, destDir) }