github.com/rsyabuta/packer@v1.1.4-0.20180119234903-5ef0c2280f0b/common/step_download.go (about)

     1  package common
     2  
     3  import (
     4  	"crypto/sha1"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"log"
     8  	"time"
     9  
    10  	"github.com/hashicorp/packer/packer"
    11  	"github.com/mitchellh/multistep"
    12  )
    13  
    14  // StepDownload downloads a remote file using the download client within
    15  // this package. This step handles setting up the download configuration,
    16  // progress reporting, interrupt handling, etc.
    17  //
    18  // Uses:
    19  //   cache packer.Cache
    20  //   ui    packer.Ui
    21  type StepDownload struct {
    22  	// The checksum and the type of the checksum for the download
    23  	Checksum     string
    24  	ChecksumType string
    25  
    26  	// A short description of the type of download being done. Example:
    27  	// "ISO" or "Guest Additions"
    28  	Description string
    29  
    30  	// The name of the key where the final path of the ISO will be put
    31  	// into the state.
    32  	ResultKey string
    33  
    34  	// The path where the result should go, otherwise it goes to the
    35  	// cache directory.
    36  	TargetPath string
    37  
    38  	// A list of URLs to attempt to download this thing.
    39  	Url []string
    40  
    41  	// Extension is the extension to force for the file that is downloaded.
    42  	// Some systems require a certain extension. If this isn't set, the
    43  	// extension on the URL is used. Otherwise, this will be forced
    44  	// on the downloaded file for every URL.
    45  	Extension string
    46  }
    47  
    48  func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
    49  	cache := state.Get("cache").(packer.Cache)
    50  	ui := state.Get("ui").(packer.Ui)
    51  
    52  	var checksum []byte
    53  	if s.Checksum != "" {
    54  		var err error
    55  		checksum, err = hex.DecodeString(s.Checksum)
    56  		if err != nil {
    57  			state.Put("error", fmt.Errorf("Error parsing checksum: %s", err))
    58  			return multistep.ActionHalt
    59  		}
    60  	}
    61  
    62  	ui.Say(fmt.Sprintf("Downloading or copying %s", s.Description))
    63  
    64  	// First try to use any already downloaded file
    65  	// If it fails, proceed to regualar download logic
    66  
    67  	var downloadConfigs = make([]*DownloadConfig, len(s.Url))
    68  	var finalPath string
    69  	for i, url := range s.Url {
    70  		targetPath := s.TargetPath
    71  		if targetPath == "" {
    72  			// Determine a cache key. This is normally just the URL but
    73  			// if we force a certain extension we hash the URL and add
    74  			// the extension to force it.
    75  			cacheKey := url
    76  			if s.Extension != "" {
    77  				hash := sha1.Sum([]byte(url))
    78  				cacheKey = fmt.Sprintf(
    79  					"%s.%s", hex.EncodeToString(hash[:]), s.Extension)
    80  			}
    81  
    82  			log.Printf("Acquiring lock to download: %s", url)
    83  			targetPath = cache.Lock(cacheKey)
    84  			defer cache.Unlock(cacheKey)
    85  		}
    86  
    87  		config := &DownloadConfig{
    88  			Url:        url,
    89  			TargetPath: targetPath,
    90  			CopyFile:   false,
    91  			Hash:       HashForType(s.ChecksumType),
    92  			Checksum:   checksum,
    93  			UserAgent:  "Packer",
    94  		}
    95  		downloadConfigs[i] = config
    96  
    97  		if match, _ := NewDownloadClient(config).VerifyChecksum(config.TargetPath); match {
    98  			ui.Message(fmt.Sprintf("Found already downloaded, initial checksum matched, no download needed: %s", url))
    99  			finalPath = config.TargetPath
   100  			break
   101  		}
   102  	}
   103  
   104  	if finalPath == "" {
   105  		for i, url := range s.Url {
   106  			ui.Message(fmt.Sprintf("Downloading or copying: %s", url))
   107  
   108  			config := downloadConfigs[i]
   109  
   110  			path, err, retry := s.download(config, state)
   111  			if err != nil {
   112  				ui.Message(fmt.Sprintf("Error downloading: %s", err))
   113  			}
   114  
   115  			if !retry {
   116  				return multistep.ActionHalt
   117  			}
   118  
   119  			if err == nil {
   120  				finalPath = path
   121  				break
   122  			}
   123  		}
   124  	}
   125  
   126  	if finalPath == "" {
   127  		err := fmt.Errorf("%s download failed.", s.Description)
   128  		state.Put("error", err)
   129  		ui.Error(err.Error())
   130  		return multistep.ActionHalt
   131  	}
   132  
   133  	state.Put(s.ResultKey, finalPath)
   134  	return multistep.ActionContinue
   135  }
   136  
   137  func (s *StepDownload) Cleanup(multistep.StateBag) {}
   138  
   139  func (s *StepDownload) download(config *DownloadConfig, state multistep.StateBag) (string, error, bool) {
   140  	var path string
   141  	ui := state.Get("ui").(packer.Ui)
   142  	download := NewDownloadClient(config)
   143  
   144  	downloadCompleteCh := make(chan error, 1)
   145  	go func() {
   146  		var err error
   147  		path, err = download.Get()
   148  		downloadCompleteCh <- err
   149  	}()
   150  
   151  	progressTicker := time.NewTicker(5 * time.Second)
   152  	defer progressTicker.Stop()
   153  
   154  	for {
   155  		select {
   156  		case err := <-downloadCompleteCh:
   157  			if err != nil {
   158  				return "", err, true
   159  			}
   160  
   161  			return path, nil, true
   162  		case <-progressTicker.C:
   163  			progress := download.PercentProgress()
   164  			if progress >= 0 {
   165  				ui.Message(fmt.Sprintf("Download progress: %d%%", progress))
   166  			}
   167  		case <-time.After(1 * time.Second):
   168  			if _, ok := state.GetOk(multistep.StateCancelled); ok {
   169  				ui.Say("Interrupt received. Cancelling download...")
   170  				return "", nil, false
   171  			}
   172  		}
   173  	}
   174  }