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  }