github.com/pivotal-cf/go-pivnet/v6@v6.0.2/download/downloader.go (about) 1 package download 2 3 import ( 4 "fmt" 5 "github.com/pivotal-cf/go-pivnet/v6/logger" 6 "golang.org/x/sync/errgroup" 7 "io" 8 "net" 9 "net/http" 10 "os" 11 "strings" 12 "syscall" 13 "github.com/shirou/gopsutil/disk" 14 "github.com/onsi/gomega/gbytes" 15 "time" 16 "path" 17 ) 18 19 //go:generate counterfeiter -o ./fakes/ranger.go --fake-name Ranger . ranger 20 type ranger interface { 21 BuildRange(contentLength int64) ([]Range, error) 22 } 23 24 //go:generate counterfeiter -o ./fakes/http_client.go --fake-name HTTPClient . httpClient 25 type httpClient interface { 26 Do(*http.Request) (*http.Response, error) 27 } 28 29 type downloadLinkFetcher interface { 30 NewDownloadLink() (string, error) 31 } 32 33 //go:generate counterfeiter -o ./fakes/bar.go --fake-name Bar . bar 34 type bar interface { 35 SetTotal(contentLength int64) 36 SetOutput(output io.Writer) 37 Add(totalWritten int) int 38 Kickoff() 39 Finish() 40 NewProxyReader(reader io.Reader) io.Reader 41 } 42 43 type Client struct { 44 HTTPClient httpClient 45 Ranger ranger 46 Bar bar 47 Logger logger.Logger 48 Timeout time.Duration 49 } 50 51 type FileInfo struct { 52 Name string 53 Mode os.FileMode 54 } 55 56 func NewFileInfo(file *os.File) (*FileInfo, error) { 57 stat, err := file.Stat() 58 if err != nil { 59 return nil, err 60 } 61 62 fileInfo := &FileInfo { 63 Name: file.Name(), 64 Mode: stat.Mode(), 65 } 66 67 return fileInfo, nil 68 } 69 70 func (c Client) Get( 71 location *FileInfo, 72 downloadLinkFetcher downloadLinkFetcher, 73 progressWriter io.Writer, 74 ) error { 75 contentURL, err := downloadLinkFetcher.NewDownloadLink() 76 if err != nil { 77 return fmt.Errorf("could not create new download link in get: %s", err) 78 } 79 80 req, err := http.NewRequest("HEAD", contentURL, nil) 81 if err != nil { 82 return fmt.Errorf("failed to construct HEAD request: %s", err) 83 } 84 85 req.Header.Add("Referer","https://go-pivnet.network.pivotal.io") 86 87 resp, err := c.HTTPClient.Do(req) 88 if err != nil { 89 return fmt.Errorf("failed to make HEAD request: %s", err) 90 } 91 92 c.Logger.Debug(fmt.Sprintf("HEAD response content size: %d", resp.ContentLength)) 93 94 contentURL = resp.Request.URL.String() 95 96 if resp.ContentLength == -1 { 97 return fmt.Errorf("failed to find file on remote filestore") 98 } 99 100 ranges, err := c.Ranger.BuildRange(resp.ContentLength) 101 if err != nil { 102 return fmt.Errorf("failed to construct range: %s", err) 103 } 104 105 diskStats, err := disk.Usage(path.Dir(location.Name)) 106 if err != nil { 107 return fmt.Errorf("failed to get disk free space: %s", err) 108 } 109 110 if diskStats.Free < uint64(resp.ContentLength) { 111 return fmt.Errorf("file is too big to fit on this drive: %d bytes required, %d bytes free", uint64(resp.ContentLength), diskStats.Free) 112 } 113 114 c.Bar.SetOutput(progressWriter) 115 c.Bar.SetTotal(resp.ContentLength) 116 c.Bar.Kickoff() 117 118 defer c.Bar.Finish() 119 120 var g errgroup.Group 121 for _, r := range ranges { 122 byteRange := r 123 124 fileWriter, err := os.OpenFile(location.Name, os.O_RDWR, location.Mode) 125 if err != nil { 126 return fmt.Errorf("failed to open file %s for writing: %s", location.Name, err) 127 } 128 129 g.Go(func() error { 130 err := c.retryableRequest(contentURL, byteRange.HTTPHeader, fileWriter, byteRange.Lower, downloadLinkFetcher, c.Timeout) 131 if err != nil { 132 return fmt.Errorf("failed during retryable request: %s", err) 133 } 134 135 return nil 136 }) 137 } 138 139 if err := g.Wait(); err != nil { 140 return fmt.Errorf("problem while waiting for chunks to download: %s", err) 141 } 142 143 return nil 144 } 145 146 func (c Client) retryableRequest(contentURL string, rangeHeader http.Header, fileWriter *os.File, startingByte int64, downloadLinkFetcher downloadLinkFetcher, timeout time.Duration) error { 147 currentURL := contentURL 148 defer fileWriter.Close() 149 150 var err error 151 Retry: 152 _, err = fileWriter.Seek(startingByte, 0) 153 if err != nil { 154 return fmt.Errorf("failed to seek to correct byte of output file: %s", err) 155 } 156 157 req, err := http.NewRequest("GET", currentURL, nil) 158 if err != nil { 159 return fmt.Errorf("could not get new request: %s", err) 160 } 161 162 rangeHeader.Add("Referer", "https://go-pivnet.network.pivotal.io") 163 req.Header = rangeHeader 164 165 resp, err := c.HTTPClient.Do(req) 166 if err != nil { 167 if netErr, ok := err.(net.Error); ok { 168 if netErr.Temporary() { 169 goto Retry 170 } 171 } 172 173 return fmt.Errorf("download request failed: %s", err) 174 } 175 176 defer resp.Body.Close() 177 178 if resp.StatusCode == http.StatusForbidden { 179 c.Logger.Debug("received unsuccessful status code: %d", logger.Data{"statusCode": resp.StatusCode}) 180 currentURL, err = downloadLinkFetcher.NewDownloadLink() 181 if err != nil { 182 return fmt.Errorf("could not get new download link: %s", err) 183 } 184 c.Logger.Debug("fetched new download url: %d", logger.Data{"url": currentURL}) 185 186 goto Retry 187 } 188 189 if resp.StatusCode != http.StatusPartialContent { 190 return fmt.Errorf("during GET unexpected status code was returned: %d", resp.StatusCode) 191 } 192 193 var proxyReader io.Reader 194 proxyReader = c.Bar.NewProxyReader(resp.Body) 195 196 197 var timeoutReader io.Reader 198 timeoutReader = gbytes.TimeoutReader(proxyReader, timeout) 199 200 bytesWritten, err := io.Copy(fileWriter, timeoutReader) 201 if err != nil { 202 if err == io.ErrUnexpectedEOF || err == gbytes.ErrTimeout{ 203 c.Logger.Debug(fmt.Sprintf("retrying %v", err)) 204 c.Bar.Add(int(-1 * bytesWritten)) 205 goto Retry 206 } 207 oe, _ := err.(*net.OpError) 208 if strings.Contains(oe.Err.Error(), syscall.ECONNRESET.Error()) { 209 c.Bar.Add(int(-1 * bytesWritten)) 210 goto Retry 211 } 212 return fmt.Errorf("failed to write file during io.Copy: %s", err) 213 } 214 215 return nil 216 }