ajhahn.de
← eeco
Go 142 lines
package selfupdate

import (
	"archive/tar"
	"archive/zip"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strings"
)

// extract unpacks archivePath into dstDir and returns the absolute path
// to the eeco binary inside. The archives produced by scripts/build.sh
// place a single directory `${GOOS}_${GOARCH}/` at the root containing
// `eeco{.exe}` + README + LICENSE; extract resolves whichever entry
// matches the binary name and ignores the others.
func extract(archivePath, dstDir, goos string) (string, error) {
	binName := "eeco"
	if goos == "windows" {
		binName = "eeco.exe"
	}
	switch {
	case strings.HasSuffix(archivePath, ".tar.gz"):
		return extractTarGz(archivePath, dstDir, binName)
	case strings.HasSuffix(archivePath, ".zip"):
		return extractZip(archivePath, dstDir, binName)
	default:
		return "", fmt.Errorf("unknown archive format: %s", archivePath)
	}
}

func extractTarGz(archivePath, dstDir, binName string) (string, error) {
	f, err := os.Open(archivePath)
	if err != nil {
		return "", err
	}
	defer f.Close()
	gz, err := gzip.NewReader(f)
	if err != nil {
		return "", err
	}
	defer gz.Close()
	tr := tar.NewReader(gz)
	for {
		h, err := tr.Next()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return "", err
		}
		if h.Typeflag != tar.TypeReg {
			continue
		}
		if path.Base(h.Name) != binName {
			continue
		}
		dst := filepath.Join(dstDir, binName)
		if err := writeBinary(dst, tr, modeFromHeader(h.Mode)); err != nil {
			return "", err
		}
		return dst, nil
	}
	return "", fmt.Errorf("binary %s not found in archive", binName)
}

func extractZip(archivePath, dstDir, binName string) (string, error) {
	zr, err := zip.OpenReader(archivePath)
	if err != nil {
		return "", err
	}
	defer zr.Close()
	for _, zf := range zr.File {
		if path.Base(zf.Name) != binName {
			continue
		}
		rc, err := zf.Open()
		if err != nil {
			return "", err
		}
		dst := filepath.Join(dstDir, binName)
		err = writeBinary(dst, rc, modeFromHeader(int64(zf.Mode())))
		rc.Close()
		if err != nil {
			return "", err
		}
		return dst, nil
	}
	return "", fmt.Errorf("binary %s not found in archive", binName)
}

func writeBinary(dst string, src io.Reader, mode os.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
		return err
	}
	f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
	if err != nil {
		return err
	}
	if _, err := io.Copy(f, src); err != nil {
		f.Close()
		return err
	}
	return f.Close()
}

func modeFromHeader(mode int64) os.FileMode {
	m := os.FileMode(mode & int64(os.ModePerm))
	if m == 0 {
		m = 0o755
	}
	return m | 0o100
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	info, err := in.Stat()
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
		return err
	}
	out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
	if err != nil {
		return err
	}
	if _, err := io.Copy(out, in); err != nil {
		out.Close()
		return err
	}
	return out.Close()
}