package getter
import (
+ "context"
"encoding/xml"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
+ "strconv"
"strings"
- "github.com/hashicorp/go-safetemp"
+ safetemp "github.com/hashicorp/go-safetemp"
)
// HttpGetter is a Getter implementation that will download from an HTTP
//
// For file downloads, HTTP is used directly.
//
-// The protocol for downloading a directory from an HTTP endpoing is as follows:
+// The protocol for downloading a directory from an HTTP endpoint is as follows:
//
// An HTTP GET request is made to the URL with the additional GET parameter
// "terraform-get=1". This lets you handle that scenario specially if you
// formed URL. The shorthand syntax of "github.com/foo/bar" or relative
// paths are not allowed.
type HttpGetter struct {
+ getter
+
// Netrc, if true, will lookup and use auth information found
// in the user's netrc file if available.
Netrc bool
// Client is the http.Client to use for Get requests.
// This defaults to a cleanhttp.DefaultClient if left unset.
Client *http.Client
+
+ // Header contains optional request header fields that should be included
+ // with every HTTP request. Note that the zero value of this field is nil,
+ // and as such it needs to be initialized before use, via something like
+ // make(http.Header).
+ Header http.Header
}
func (g *HttpGetter) ClientMode(u *url.URL) (ClientMode, error) {
}
func (g *HttpGetter) Get(dst string, u *url.URL) error {
+ ctx := g.Context()
// Copy the URL so we can modify it
var newU url.URL = *u
u = &newU
u.RawQuery = q.Encode()
// Get the URL
- resp, err := g.Client.Get(u.String())
+ req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return err
}
+
+ req.Header = g.Header
+ resp, err := g.Client.Do(req)
+ if err != nil {
+ return err
+ }
+
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("bad response code: %d", resp.StatusCode)
// into a temporary directory, then copy over the proper subdir.
source, subDir := SourceDirSubdir(source)
if subDir == "" {
- return Get(dst, source)
+ var opts []ClientOption
+ if g.client != nil {
+ opts = g.client.Options
+ }
+ return Get(dst, source, opts...)
}
// We have a subdir, time to jump some hoops
- return g.getSubdir(dst, source, subDir)
+ return g.getSubdir(ctx, dst, source, subDir)
}
-func (g *HttpGetter) GetFile(dst string, u *url.URL) error {
+func (g *HttpGetter) GetFile(dst string, src *url.URL) error {
+ ctx := g.Context()
if g.Netrc {
// Add auth from netrc if we can
- if err := addAuthFromNetrc(u); err != nil {
+ if err := addAuthFromNetrc(src); err != nil {
return err
}
}
- if g.Client == nil {
- g.Client = httpClient
+ // Create all the parent directories if needed
+ if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
+ return err
}
- resp, err := g.Client.Get(u.String())
+ f, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, os.FileMode(0666))
if err != nil {
return err
}
- defer resp.Body.Close()
- if resp.StatusCode != 200 {
- return fmt.Errorf("bad response code: %d", resp.StatusCode)
+ defer f.Close()
+
+ if g.Client == nil {
+ g.Client = httpClient
}
- // Create all the parent directories
- if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
+ var currentFileSize int64
+
+ // We first make a HEAD request so we can check
+ // if the server supports range queries. If the server/URL doesn't
+ // support HEAD requests, we just fall back to GET.
+ req, err := http.NewRequest("HEAD", src.String(), nil)
+ if err != nil {
return err
}
+ if g.Header != nil {
+ req.Header = g.Header
+ }
+ headResp, err := g.Client.Do(req)
+ if err == nil && headResp != nil {
+ headResp.Body.Close()
+ if headResp.StatusCode == 200 {
+ // If the HEAD request succeeded, then attempt to set the range
+ // query if we can.
+ if headResp.Header.Get("Accept-Ranges") == "bytes" {
+ if fi, err := f.Stat(); err == nil {
+ if _, err = f.Seek(0, os.SEEK_END); err == nil {
+ req.Header.Set("Range", fmt.Sprintf("bytes=%d-", fi.Size()))
+ currentFileSize = fi.Size()
+ totalFileSize, _ := strconv.ParseInt(headResp.Header.Get("Content-Length"), 10, 64)
+ if currentFileSize >= totalFileSize {
+ // file already present
+ return nil
+ }
+ }
+ }
+ }
+ }
+ }
+ req.Method = "GET"
- f, err := os.Create(dst)
+ resp, err := g.Client.Do(req)
if err != nil {
return err
}
+ switch resp.StatusCode {
+ case http.StatusOK, http.StatusPartialContent:
+ // all good
+ default:
+ resp.Body.Close()
+ return fmt.Errorf("bad response code: %d", resp.StatusCode)
+ }
+
+ body := resp.Body
- n, err := io.Copy(f, resp.Body)
+ if g.client != nil && g.client.ProgressListener != nil {
+ // track download
+ fn := filepath.Base(src.EscapedPath())
+ body = g.client.ProgressListener.TrackProgress(fn, currentFileSize, currentFileSize+resp.ContentLength, resp.Body)
+ }
+ defer body.Close()
+
+ n, err := Copy(ctx, f, body)
if err == nil && n < resp.ContentLength {
err = io.ErrShortWrite
}
- if err1 := f.Close(); err == nil {
- err = err1
- }
return err
}
// getSubdir downloads the source into the destination, but with
// the proper subdir.
-func (g *HttpGetter) getSubdir(dst, source, subDir string) error {
+func (g *HttpGetter) getSubdir(ctx context.Context, dst, source, subDir string) error {
// Create a temporary directory to store the full source. This has to be
// a non-existent directory.
td, tdcloser, err := safetemp.Dir("", "getter")
}
defer tdcloser.Close()
+ var opts []ClientOption
+ if g.client != nil {
+ opts = g.client.Options
+ }
// Download that into the given directory
- if err := Get(td, source); err != nil {
+ if err := Get(td, source, opts...); err != nil {
return err
}
return err
}
- return copyDir(dst, sourcePath, false)
+ return copyDir(ctx, dst, sourcePath, false)
}
// parseMeta looks for the first meta tag in the given reader that