336 lines
8.4 KiB
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
|
|
}
|