open-nomad/vendor/github.com/hashicorp/go-getter/client.go

336 lines
8.4 KiB
Go

package getter
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
urlhelper "github.com/hashicorp/go-getter/helper/url"
)
// Client is a client for downloading things.
//
// Top-level functions such as Get are shortcuts for interacting with a client.
// Using a client directly allows more fine-grained control over how downloading
// is done, as well as customizing the protocols supported.
type Client struct {
// Src is the source URL to get.
//
// Dst is the path to save the downloaded thing as. If Dir is set to
// true, then this should be a directory. If the directory doesn't exist,
// it will be created for you.
//
// Pwd is the working directory for detection. If this isn't set, some
// detection may fail. Client will not default pwd to the current
// working directory for security reasons.
Src string
Dst string
Pwd string
// Mode is the method of download the client will use. See ClientMode
// for documentation.
Mode ClientMode
// Detectors is the list of detectors that are tried on the source.
// If this is nil, then the default Detectors will be used.
Detectors []Detector
// Decompressors is the map of decompressors supported by this client.
// If this is nil, then the default value is the Decompressors global.
Decompressors map[string]Decompressor
// Getters is the map of protocols supported by this client. If this
// is nil, then the default Getters variable will be used.
Getters map[string]Getter
// Dir, if true, tells the Client it is downloading a directory (versus
// a single file). This distinction is necessary since filenames and
// directory names follow the same format so disambiguating is impossible
// without knowing ahead of time.
//
// WARNING: deprecated. If Mode is set, that will take precedence.
Dir bool
}
// Get downloads the configured source to the destination.
func (c *Client) Get() error {
// Store this locally since there are cases we swap this
mode := c.Mode
if mode == ClientModeInvalid {
if c.Dir {
mode = ClientModeDir
} else {
mode = ClientModeFile
}
}
// Default decompressor value
decompressors := c.Decompressors
if decompressors == nil {
decompressors = Decompressors
}
// Detect the URL. This is safe if it is already detected.
detectors := c.Detectors
if detectors == nil {
detectors = Detectors
}
src, err := Detect(c.Src, c.Pwd, detectors)
if err != nil {
return err
}
// Determine if we have a forced protocol, i.e. "git::http://..."
force, src := getForcedGetter(src)
// If there is a subdir component, then we download the root separately
// and then copy over the proper subdir.
var realDst string
dst := c.Dst
src, subDir := SourceDirSubdir(src)
if subDir != "" {
tmpDir, err := ioutil.TempDir("", "tf")
if err != nil {
return err
}
if err := os.RemoveAll(tmpDir); err != nil {
return err
}
defer os.RemoveAll(tmpDir)
realDst = dst
dst = tmpDir
}
u, err := urlhelper.Parse(src)
if err != nil {
return err
}
if force == "" {
force = u.Scheme
}
getters := c.Getters
if getters == nil {
getters = Getters
}
g, ok := getters[force]
if !ok {
return fmt.Errorf(
"download not supported for scheme '%s'", force)
}
// We have magic query parameters that we use to signal different features
q := u.Query()
// Determine if we have an archive type
archiveV := q.Get("archive")
if archiveV != "" {
// Delete the paramter since it is a magic parameter we don't
// want to pass on to the Getter
q.Del("archive")
u.RawQuery = q.Encode()
// If we can parse the value as a bool and it is false, then
// set the archive to "-" which should never map to a decompressor
if b, err := strconv.ParseBool(archiveV); err == nil && !b {
archiveV = "-"
}
}
if archiveV == "" {
// We don't appear to... but is it part of the filename?
matchingLen := 0
for k, _ := range decompressors {
if strings.HasSuffix(u.Path, "."+k) && len(k) > matchingLen {
archiveV = k
matchingLen = len(k)
}
}
}
// If we have a decompressor, then we need to change the destination
// to download to a temporary path. We unarchive this into the final,
// real path.
var decompressDst string
var decompressDir bool
decompressor := decompressors[archiveV]
if decompressor != nil {
// Create a temporary directory to store our archive. We delete
// this at the end of everything.
td, err := ioutil.TempDir("", "getter")
if err != nil {
return fmt.Errorf(
"Error creating temporary directory for archive: %s", err)
}
defer os.RemoveAll(td)
// Swap the download directory to be our temporary path and
// store the old values.
decompressDst = dst
decompressDir = mode != ClientModeFile
dst = filepath.Join(td, "archive")
mode = ClientModeFile
}
// Determine if we have a checksum
var checksumHash hash.Hash
var checksumValue []byte
if v := q.Get("checksum"); v != "" {
// Delete the query parameter if we have it.
q.Del("checksum")
u.RawQuery = q.Encode()
// Determine the checksum hash type
checksumType := ""
idx := strings.Index(v, ":")
if idx > -1 {
checksumType = v[:idx]
}
switch checksumType {
case "md5":
checksumHash = md5.New()
case "sha1":
checksumHash = sha1.New()
case "sha256":
checksumHash = sha256.New()
case "sha512":
checksumHash = sha512.New()
default:
return fmt.Errorf(
"unsupported checksum type: %s", checksumType)
}
// Get the remainder of the value and parse it into bytes
b, err := hex.DecodeString(v[idx+1:])
if err != nil {
return fmt.Errorf("invalid checksum: %s", err)
}
// Set our value
checksumValue = b
}
if mode == ClientModeAny {
// Ask the getter which client mode to use
mode, err = g.ClientMode(u)
if err != nil {
return err
}
// Destination is the base name of the URL path in "any" mode when
// a file source is detected.
if mode == ClientModeFile {
dst = filepath.Join(dst, filepath.Base(u.Path))
}
}
// If we're not downloading a directory, then just download the file
// and return.
if mode == ClientModeFile {
err := g.GetFile(dst, u)
if err != nil {
return err
}
if checksumHash != nil {
if err := checksum(dst, checksumHash, checksumValue); err != nil {
return err
}
}
if decompressor != nil {
// We have a decompressor, so decompress the current destination
// into the final destination with the proper mode.
err := decompressor.Decompress(decompressDst, dst, decompressDir)
if err != nil {
return err
}
// Swap the information back
dst = decompressDst
if decompressDir {
mode = ClientModeAny
} else {
mode = ClientModeFile
}
}
// We check the dir value again because it can be switched back
// if we were unarchiving. If we're still only Get-ing a file, then
// we're done.
if mode == ClientModeFile {
return nil
}
}
// If we're at this point we're either downloading a directory or we've
// downloaded and unarchived a directory and we're just checking subdir.
// In the case we have a decompressor we don't Get because it was Get
// above.
if decompressor == nil {
// If we're getting a directory, then this is an error. You cannot
// checksum a directory. TODO: test
if checksumHash != nil {
return fmt.Errorf(
"checksum cannot be specified for directory download")
}
// We're downloading a directory, which might require a bit more work
// if we're specifying a subdir.
err := g.Get(dst, u)
if err != nil {
err = fmt.Errorf("error downloading '%s': %s", src, err)
return err
}
}
// If we have a subdir, copy that over
if subDir != "" {
if err := os.RemoveAll(realDst); err != nil {
return err
}
if err := os.MkdirAll(realDst, 0755); err != nil {
return err
}
return copyDir(realDst, filepath.Join(dst, subDir), false)
}
return nil
}
// checksum is a simple method to compute the checksum of a source file
// and compare it to the given expected value.
func checksum(source string, h hash.Hash, v []byte) error {
f, err := os.Open(source)
if err != nil {
return fmt.Errorf("Failed to open file for checksum: %s", err)
}
defer f.Close()
if _, err := io.Copy(h, f); err != nil {
return fmt.Errorf("Failed to hash: %s", err)
}
if actual := h.Sum(nil); !bytes.Equal(actual, v) {
return fmt.Errorf(
"Checksums did not match.\nExpected: %s\nGot: %s",
hex.EncodeToString(v),
hex.EncodeToString(actual))
}
return nil
}