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 }