github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/machine/pull.go (about)

     1  //go:build amd64 || arm64
     2  // +build amd64 arm64
     3  
     4  package machine
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	url2 "net/url"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/containers/image/v5/pkg/compression"
    20  	"github.com/containers/storage/pkg/archive"
    21  	"github.com/sirupsen/logrus"
    22  	"github.com/ulikunitz/xz"
    23  	"github.com/vbauerster/mpb/v7"
    24  	"github.com/vbauerster/mpb/v7/decor"
    25  )
    26  
    27  // GenericDownload is used when a user provides a URL
    28  // or path for an image
    29  type GenericDownload struct {
    30  	Download
    31  }
    32  
    33  // NewGenericDownloader is used when the disk image is provided by the user
    34  func NewGenericDownloader(vmType, vmName, pullPath string) (DistributionDownload, error) {
    35  	var (
    36  		imageName string
    37  	)
    38  	dataDir, err := GetDataDir(vmType)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  	dl := Download{}
    43  	// Is pullpath a file or url?
    44  	getURL, err := url2.Parse(pullPath)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	if len(getURL.Scheme) > 0 {
    49  		urlSplit := strings.Split(getURL.Path, "/")
    50  		imageName = urlSplit[len(urlSplit)-1]
    51  		dl.LocalUncompressedFile = filepath.Join(dataDir, imageName)
    52  		dl.URL = getURL
    53  		dl.LocalPath = filepath.Join(dataDir, imageName)
    54  	} else {
    55  		// Dealing with FilePath
    56  		imageName = filepath.Base(pullPath)
    57  		dl.LocalUncompressedFile = filepath.Join(dataDir, imageName)
    58  		dl.LocalPath = pullPath
    59  	}
    60  	dl.VMName = vmName
    61  	dl.ImageName = imageName
    62  	// The download needs to be pulled into the datadir
    63  
    64  	gd := GenericDownload{Download: dl}
    65  	gd.LocalUncompressedFile = gd.getLocalUncompressedName()
    66  	return gd, nil
    67  }
    68  
    69  func (d Download) getLocalUncompressedName() string {
    70  	var (
    71  		extension string
    72  	)
    73  	switch {
    74  	case strings.HasSuffix(d.LocalPath, ".bz2"):
    75  		extension = ".bz2"
    76  	case strings.HasSuffix(d.LocalPath, ".gz"):
    77  		extension = ".gz"
    78  	case strings.HasSuffix(d.LocalPath, ".xz"):
    79  		extension = ".xz"
    80  	}
    81  	uncompressedFilename := filepath.Join(filepath.Dir(d.LocalPath), d.VMName+"_"+d.ImageName)
    82  	return strings.TrimSuffix(uncompressedFilename, extension)
    83  }
    84  
    85  func (g GenericDownload) Get() *Download {
    86  	return &g.Download
    87  }
    88  
    89  func (g GenericDownload) HasUsableCache() (bool, error) {
    90  	// If we have a URL for this "downloader", we now pull it
    91  	return g.URL == nil, nil
    92  }
    93  
    94  func DownloadImage(d DistributionDownload) error {
    95  	// check if the latest image is already present
    96  	ok, err := d.HasUsableCache()
    97  	if err != nil {
    98  		return err
    99  	}
   100  	if !ok {
   101  		if err := DownloadVMImage(d.Get().URL, d.Get().LocalPath); err != nil {
   102  			return err
   103  		}
   104  	}
   105  	return Decompress(d.Get().LocalPath, d.Get().getLocalUncompressedName())
   106  }
   107  
   108  // DownloadVMImage downloads a VM image from url to given path
   109  // with download status
   110  func DownloadVMImage(downloadURL *url2.URL, localImagePath string) error {
   111  	out, err := os.Create(localImagePath)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	defer func() {
   116  		if err := out.Close(); err != nil {
   117  			logrus.Error(err)
   118  		}
   119  	}()
   120  
   121  	resp, err := http.Get(downloadURL.String())
   122  	if err != nil {
   123  		return err
   124  	}
   125  	defer func() {
   126  		if err := resp.Body.Close(); err != nil {
   127  			logrus.Error(err)
   128  		}
   129  	}()
   130  
   131  	if resp.StatusCode != http.StatusOK {
   132  		return fmt.Errorf("downloading VM image %s: %s", downloadURL, resp.Status)
   133  	}
   134  	size := resp.ContentLength
   135  	urlSplit := strings.Split(downloadURL.Path, "/")
   136  	prefix := "Downloading VM image: " + urlSplit[len(urlSplit)-1]
   137  	onComplete := prefix + ": done"
   138  
   139  	p := mpb.New(
   140  		mpb.WithWidth(60),
   141  		mpb.WithRefreshRate(180*time.Millisecond),
   142  	)
   143  
   144  	bar := p.AddBar(size,
   145  		mpb.BarFillerClearOnComplete(),
   146  		mpb.PrependDecorators(
   147  			decor.OnComplete(decor.Name(prefix), onComplete),
   148  		),
   149  		mpb.AppendDecorators(
   150  			decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), ""),
   151  		),
   152  	)
   153  
   154  	proxyReader := bar.ProxyReader(resp.Body)
   155  	defer func() {
   156  		if err := proxyReader.Close(); err != nil {
   157  			logrus.Error(err)
   158  		}
   159  	}()
   160  
   161  	if _, err := io.Copy(out, proxyReader); err != nil {
   162  		return err
   163  	}
   164  
   165  	p.Wait()
   166  	return nil
   167  }
   168  
   169  func Decompress(localPath, uncompressedPath string) error {
   170  	uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	sourceFile, err := ioutil.ReadFile(localPath)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	compressionType := archive.DetectCompression(sourceFile)
   180  	if compressionType != archive.Uncompressed {
   181  		fmt.Println("Extracting compressed file")
   182  	}
   183  	if compressionType == archive.Xz {
   184  		return decompressXZ(localPath, uncompressedFileWriter)
   185  	}
   186  	return decompressEverythingElse(localPath, uncompressedFileWriter)
   187  }
   188  
   189  // Will error out if file without .xz already exists
   190  // Maybe extracting then renameing is a good idea here..
   191  // depends on xz: not pre-installed on mac, so it becomes a brew dependency
   192  func decompressXZ(src string, output io.WriteCloser) error {
   193  	var read io.Reader
   194  	var cmd *exec.Cmd
   195  	// Prefer xz utils for fastest performance, fallback to go xi2 impl
   196  	if _, err := exec.LookPath("xzcat"); err == nil {
   197  		cmd = exec.Command("xzcat", "-k", src)
   198  		read, err = cmd.StdoutPipe()
   199  		if err != nil {
   200  			return err
   201  		}
   202  		cmd.Stderr = os.Stderr
   203  	} else {
   204  		file, err := os.Open(src)
   205  		if err != nil {
   206  			return err
   207  		}
   208  		defer file.Close()
   209  		// This XZ implementation is reliant on buffering. It is also 3x+ slower than XZ utils.
   210  		// Consider replacing with a faster implementation (e.g. xi2) if podman machine is
   211  		// updated with a larger image for the distribution base.
   212  		buf := bufio.NewReader(file)
   213  		read, err = xz.NewReader(buf)
   214  		if err != nil {
   215  			return err
   216  		}
   217  	}
   218  
   219  	done := make(chan bool)
   220  	go func() {
   221  		if _, err := io.Copy(output, read); err != nil {
   222  			logrus.Error(err)
   223  		}
   224  		output.Close()
   225  		done <- true
   226  	}()
   227  
   228  	if cmd != nil {
   229  		return cmd.Run()
   230  	}
   231  	<-done
   232  	return nil
   233  }
   234  
   235  func decompressEverythingElse(src string, output io.WriteCloser) error {
   236  	f, err := os.Open(src)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	uncompressStream, _, err := compression.AutoDecompress(f)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	defer func() {
   245  		if err := uncompressStream.Close(); err != nil {
   246  			logrus.Error(err)
   247  		}
   248  		if err := output.Close(); err != nil {
   249  			logrus.Error(err)
   250  		}
   251  	}()
   252  
   253  	_, err = io.Copy(output, uncompressStream)
   254  	return err
   255  }