github.com/grafana/pyroscope@v1.18.0/pkg/embedded/grafana/assets.go (about)

     1  package grafana
     2  
     3  import (
     4  	"archive/tar"
     5  	"archive/zip"
     6  	"compress/gzip"
     7  	"context"
     8  	"crypto/sha256"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/fs"
    14  	"net/http"
    15  	"os"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"github.com/go-kit/log"
    20  	"github.com/go-kit/log/level"
    21  )
    22  
    23  type CompressType int
    24  
    25  const (
    26  	CompressTypeNone CompressType = iota
    27  	CompressTypeGzip
    28  	CompressTypeZip
    29  )
    30  
    31  const (
    32  	modeDir  = 0755
    33  	modeFile = 0644
    34  )
    35  
    36  type releaseArtifacts []releaseArtifact
    37  
    38  func (releases releaseArtifacts) selectBy(os, arch string) *releaseArtifact {
    39  	var nonArch *releaseArtifact
    40  	for idx, r := range releases {
    41  		if r.OS == "" && r.Arch == "" && nonArch == nil {
    42  			nonArch = &releases[idx]
    43  			continue
    44  		}
    45  		if r.OS == os && r.Arch == arch {
    46  			return &r
    47  		}
    48  	}
    49  	return nonArch
    50  }
    51  
    52  type releaseArtifact struct {
    53  	URL             string
    54  	Sha256Sum       []byte
    55  	OS              string
    56  	Arch            string
    57  	CompressType    CompressType
    58  	StripComponents int
    59  }
    60  
    61  func (release *releaseArtifact) download(ctx context.Context, logger log.Logger, destPath string) (string, error) {
    62  	targetPath := filepath.Join(destPath, "assets", hex.EncodeToString(release.Sha256Sum))
    63  
    64  	// check if already exists
    65  	if len(release.Sha256Sum) > 0 {
    66  		stat, err := os.Stat(targetPath)
    67  		if err != nil {
    68  			if !os.IsNotExist(err) {
    69  				return "", err
    70  			}
    71  		}
    72  		if err == nil && stat.IsDir() {
    73  			level.Info(logger).Log("msg", "release exists already", "url", release.URL, "hash", hex.EncodeToString(release.Sha256Sum))
    74  			return targetPath, nil
    75  		}
    76  	}
    77  
    78  	level.Info(logger).Log("msg", "download new release", "url", release.URL)
    79  	req, err := http.NewRequestWithContext(ctx, "GET", release.URL, nil)
    80  	req.Header.Set("User-Agent", "pyroscope/embedded-grafana")
    81  	if err != nil {
    82  		return "", err
    83  	}
    84  
    85  	resp, err := http.DefaultClient.Do(req)
    86  	if err != nil {
    87  		return "", err
    88  	}
    89  	defer resp.Body.Close()
    90  
    91  	if resp.StatusCode != http.StatusOK {
    92  		return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
    93  	}
    94  
    95  	file, err := os.CreateTemp("", "pyroscope-download")
    96  	if err != nil {
    97  		return "", err
    98  	}
    99  	defer os.Remove(file.Name())
   100  
   101  	hash := sha256.New()
   102  	r := io.TeeReader(resp.Body, hash)
   103  
   104  	_, err = io.Copy(file, r)
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  
   109  	err = file.Close()
   110  	if err != nil {
   111  		return "", err
   112  	}
   113  
   114  	actHashSum := hex.EncodeToString(hash.Sum(nil))
   115  	if expHashSum := hex.EncodeToString(release.Sha256Sum); actHashSum != expHashSum {
   116  		return "", fmt.Errorf("hash mismatch: expected %s, got %s", expHashSum, actHashSum)
   117  	}
   118  
   119  	switch release.CompressType {
   120  	case CompressTypeNone:
   121  		return targetPath, os.Rename(file.Name(), targetPath)
   122  	case CompressTypeGzip:
   123  		file, err = os.Open(file.Name())
   124  		if err != nil {
   125  			return "", err
   126  		}
   127  		defer file.Close()
   128  
   129  		err = extractTarGz(file, targetPath, release.StripComponents)
   130  		if err != nil {
   131  			return "", err
   132  		}
   133  	case CompressTypeZip:
   134  		file, err = os.Open(file.Name())
   135  		if err != nil {
   136  			return "", err
   137  		}
   138  		defer file.Close()
   139  
   140  		stat, err := file.Stat()
   141  		if err != nil {
   142  			return "", err
   143  		}
   144  
   145  		err = extractZip(file, stat.Size(), targetPath, release.StripComponents)
   146  		if err != nil {
   147  			return "", err
   148  		}
   149  	}
   150  
   151  	return targetPath, nil
   152  }
   153  
   154  func clearPath(name string, destPath string, stripComponents int) string {
   155  	isSeparator := func(r rune) bool {
   156  		return r == os.PathSeparator
   157  	}
   158  	list := strings.FieldsFunc(name, isSeparator)
   159  	if len(list) > stripComponents {
   160  		list = list[stripComponents:]
   161  	}
   162  	return filepath.Join(append([]string{destPath}, list...)...)
   163  }
   164  
   165  func extractZip(zipStream io.ReaderAt, size int64, destPath string, stripComponents int) error {
   166  	zipReader, err := zip.NewReader(zipStream, size)
   167  	if err != nil {
   168  		return fmt.Errorf("ExtractZip: NewReader failed: %s", err.Error())
   169  	}
   170  
   171  	for _, f := range zipReader.File {
   172  		p := clearPath(f.Name, destPath, stripComponents)
   173  		if f.FileInfo().IsDir() {
   174  			err := os.MkdirAll(p, modeDir)
   175  			if err != nil {
   176  				return fmt.Errorf("ExtractZip: MkdirAll() failed: %s", err.Error())
   177  			}
   178  			continue
   179  		}
   180  
   181  		dir, _ := filepath.Split(p)
   182  		if _, err := os.Stat(dir); os.IsNotExist(err) {
   183  			if err := os.MkdirAll(dir, modeDir); err != nil {
   184  				return fmt.Errorf("ExtractZip: MkdirAll() failed: %s", err.Error())
   185  			}
   186  		}
   187  
   188  		fileInArchive, err := f.Open()
   189  		if err != nil {
   190  			return fmt.Errorf("ExtractZip: Open() failed: %s", err.Error())
   191  		}
   192  
   193  		outFile, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, f.FileInfo().Mode())
   194  		if err != nil {
   195  			return fmt.Errorf("ExtractZip: OpenFile() failed: %s", err.Error())
   196  		}
   197  		if _, err := io.Copy(outFile, fileInArchive); err != nil {
   198  			return fmt.Errorf("ExtractZip: Copy() failed: %s", err.Error())
   199  		}
   200  	}
   201  
   202  	return nil
   203  
   204  }
   205  
   206  func extractTarGz(gzipStream io.Reader, destPath string, stripComponents int) error {
   207  	uncompressedStream, err := gzip.NewReader(gzipStream)
   208  	if err != nil {
   209  		return errors.New("ExtractTarGz: NewReader failed")
   210  	}
   211  
   212  	tarReader := tar.NewReader(uncompressedStream)
   213  
   214  	for {
   215  		header, err := tarReader.Next()
   216  
   217  		if err == io.EOF {
   218  			break
   219  		}
   220  
   221  		if err != nil {
   222  			return fmt.Errorf("ExtractTarGz: Next() failed: %s", err.Error())
   223  		}
   224  
   225  		p := clearPath(header.Name, destPath, stripComponents)
   226  		switch header.Typeflag {
   227  		case tar.TypeDir:
   228  			if err := os.MkdirAll(p, modeDir); err != nil {
   229  				return fmt.Errorf("ExtractTarGz: Mkdir() failed: %s", err.Error())
   230  			}
   231  		case tar.TypeReg:
   232  			dir, _ := filepath.Split(p)
   233  			if _, err := os.Stat(dir); os.IsNotExist(err) {
   234  				if err := os.MkdirAll(dir, modeDir); err != nil {
   235  					return fmt.Errorf("ExtractTarGz: MkdirAll() failed: %s", err.Error())
   236  				}
   237  			}
   238  			outFile, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fs.FileMode(header.Mode))
   239  			if err != nil {
   240  				return fmt.Errorf("ExtractTarGz: OpenFile() failed: %s", err.Error())
   241  			}
   242  			if _, err := io.Copy(outFile, tarReader); err != nil {
   243  				return fmt.Errorf("ExtractTarGz: Copy() failed: %s", err.Error())
   244  			}
   245  			outFile.Close()
   246  
   247  		default:
   248  			return fmt.Errorf(
   249  				"ExtractTarGz: unknown type: %v in %s",
   250  				header.Typeflag,
   251  				header.Name)
   252  		}
   253  	}
   254  
   255  	return nil
   256  }