// Unpack: extract a source tarball. // // Supports .tar.gz, .tar.bz2, .tar.xz, .tar.lz, .tgz, .txz, .tbz2, and // plain .tar. The destination directory is created if it doesn't exist. // We shell out to `tar` because it's universally available on every // Source Mage box and is far faster than a pure-Go reimplementation // (it uses splice() for zero-copy extraction on recent kernels). package cast import ( "fmt" "os" "os/exec" "path/filepath" "strings" "dcos.net/sorcery-go/pkg/eventbus" ) // Unpack extracts `archivePath` into `destDir`. destDir is created. // Returns the path to the top-level source directory inside destDir. func Unpack(archivePath, destDir string, bus *eventbus.Bus, taskID string) error { if bus != nil { bus.Log(taskID, "📦 Unpacking "+filepath.Base(archivePath)) } if err := os.MkdirAll(destDir, 0755); err != nil { return err } // `tar` auto-detects compression from the file content, so we don't // need to inspect the extension — `tar -xf` does the right thing. cmd := exec.Command("tar", "-xf", archivePath, "-C", destDir, "--no-same-owner", "--no-same-permissions") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("unpack: %w (output: %s)", err, string(out)) } if bus != nil { bus.Log(taskID, "✓ Unpacked into "+destDir) } return nil } // GuessSourceDir inspects destDir after extraction and returns the // single top-level subdirectory if there is one (the typical // ${SPELL}-${VERSION} directory), otherwise returns destDir itself. func GuessSourceDir(destDir string) (string, error) { entries, err := os.ReadDir(destDir) if err != nil { return "", err } if len(entries) == 1 && entries[0].IsDir() { return filepath.Join(destDir, entries[0].Name()), nil } return destDir, nil } // ArchiveExtension returns the recognised archive extension of path. func ArchiveExtension(path string) string { low := strings.ToLower(path) for _, ext := range []string{".tar.gz", ".tar.bz2", ".tar.xz", ".tar.lz", ".tgz", ".txz", ".tbz2", ".tar"} { if strings.HasSuffix(low, ext) { return ext } } return "" }