1package getter
2
3import (
4	"context"
5	"fmt"
6	"io/ioutil"
7	"os"
8	"path/filepath"
9	"strconv"
10	"strings"
11
12	urlhelper "github.com/hashicorp/go-getter/helper/url"
13	safetemp "github.com/hashicorp/go-safetemp"
14)
15
16// Client is a client for downloading things.
17//
18// Top-level functions such as Get are shortcuts for interacting with a client.
19// Using a client directly allows more fine-grained control over how downloading
20// is done, as well as customizing the protocols supported.
21type Client struct {
22	// Ctx for cancellation
23	Ctx context.Context
24
25	// Src is the source URL to get.
26	//
27	// Dst is the path to save the downloaded thing as. If Dir is set to
28	// true, then this should be a directory. If the directory doesn't exist,
29	// it will be created for you.
30	//
31	// Pwd is the working directory for detection. If this isn't set, some
32	// detection may fail. Client will not default pwd to the current
33	// working directory for security reasons.
34	Src string
35	Dst string
36	Pwd string
37
38	// Mode is the method of download the client will use. See ClientMode
39	// for documentation.
40	Mode ClientMode
41
42	// Umask is used to mask file permissions when storing local files or decompressing
43	// an archive
44	Umask os.FileMode
45
46	// Detectors is the list of detectors that are tried on the source.
47	// If this is nil, then the default Detectors will be used.
48	Detectors []Detector
49
50	// Decompressors is the map of decompressors supported by this client.
51	// If this is nil, then the default value is the Decompressors global.
52	Decompressors map[string]Decompressor
53
54	// Getters is the map of protocols supported by this client. If this
55	// is nil, then the default Getters variable will be used.
56	Getters map[string]Getter
57
58	// Dir, if true, tells the Client it is downloading a directory (versus
59	// a single file). This distinction is necessary since filenames and
60	// directory names follow the same format so disambiguating is impossible
61	// without knowing ahead of time.
62	//
63	// WARNING: deprecated. If Mode is set, that will take precedence.
64	Dir bool
65
66	// ProgressListener allows to track file downloads.
67	// By default a no op progress listener is used.
68	ProgressListener ProgressTracker
69
70	Options []ClientOption
71}
72
73// umask returns the effective umask for the Client, defaulting to the process umask
74func (c *Client) umask() os.FileMode {
75	if c == nil {
76		return 0
77	}
78	return c.Umask
79}
80
81// mode returns file mode umasked by the Client umask
82func (c *Client) mode(mode os.FileMode) os.FileMode {
83	m := mode & ^c.umask()
84	return m
85}
86
87// Get downloads the configured source to the destination.
88func (c *Client) Get() error {
89	if err := c.Configure(c.Options...); err != nil {
90		return err
91	}
92
93	// Store this locally since there are cases we swap this
94	mode := c.Mode
95	if mode == ClientModeInvalid {
96		if c.Dir {
97			mode = ClientModeDir
98		} else {
99			mode = ClientModeFile
100		}
101	}
102
103	src, err := Detect(c.Src, c.Pwd, c.Detectors)
104	if err != nil {
105		return err
106	}
107
108	// Determine if we have a forced protocol, i.e. "git::http://..."
109	force, src := getForcedGetter(src)
110
111	// If there is a subdir component, then we download the root separately
112	// and then copy over the proper subdir.
113	var realDst string
114	dst := c.Dst
115	src, subDir := SourceDirSubdir(src)
116	if subDir != "" {
117		td, tdcloser, err := safetemp.Dir("", "getter")
118		if err != nil {
119			return err
120		}
121		defer tdcloser.Close()
122
123		realDst = dst
124		dst = td
125	}
126
127	u, err := urlhelper.Parse(src)
128	if err != nil {
129		return err
130	}
131	if force == "" {
132		force = u.Scheme
133	}
134
135	g, ok := c.Getters[force]
136	if !ok {
137		return fmt.Errorf(
138			"download not supported for scheme '%s'", force)
139	}
140
141	// We have magic query parameters that we use to signal different features
142	q := u.Query()
143
144	// Determine if we have an archive type
145	archiveV := q.Get("archive")
146	if archiveV != "" {
147		// Delete the paramter since it is a magic parameter we don't
148		// want to pass on to the Getter
149		q.Del("archive")
150		u.RawQuery = q.Encode()
151
152		// If we can parse the value as a bool and it is false, then
153		// set the archive to "-" which should never map to a decompressor
154		if b, err := strconv.ParseBool(archiveV); err == nil && !b {
155			archiveV = "-"
156		}
157	}
158	if archiveV == "" {
159		// We don't appear to... but is it part of the filename?
160		matchingLen := 0
161		for k := range c.Decompressors {
162			if strings.HasSuffix(u.Path, "."+k) && len(k) > matchingLen {
163				archiveV = k
164				matchingLen = len(k)
165			}
166		}
167	}
168
169	// If we have a decompressor, then we need to change the destination
170	// to download to a temporary path. We unarchive this into the final,
171	// real path.
172	var decompressDst string
173	var decompressDir bool
174	decompressor := c.Decompressors[archiveV]
175	if decompressor != nil {
176		// Create a temporary directory to store our archive. We delete
177		// this at the end of everything.
178		td, err := ioutil.TempDir("", "getter")
179		if err != nil {
180			return fmt.Errorf(
181				"Error creating temporary directory for archive: %s", err)
182		}
183		defer os.RemoveAll(td)
184
185		// Swap the download directory to be our temporary path and
186		// store the old values.
187		decompressDst = dst
188		decompressDir = mode != ClientModeFile
189		dst = filepath.Join(td, "archive")
190		mode = ClientModeFile
191	}
192
193	// Determine checksum if we have one
194	checksum, err := c.extractChecksum(u)
195	if err != nil {
196		return fmt.Errorf("invalid checksum: %s", err)
197	}
198
199	// Delete the query parameter if we have it.
200	q.Del("checksum")
201	u.RawQuery = q.Encode()
202
203	if mode == ClientModeAny {
204		// Ask the getter which client mode to use
205		mode, err = g.ClientMode(u)
206		if err != nil {
207			return err
208		}
209
210		// Destination is the base name of the URL path in "any" mode when
211		// a file source is detected.
212		if mode == ClientModeFile {
213			filename := filepath.Base(u.Path)
214
215			// Determine if we have a custom file name
216			if v := q.Get("filename"); v != "" {
217				// Delete the query parameter if we have it.
218				q.Del("filename")
219				u.RawQuery = q.Encode()
220
221				filename = v
222			}
223
224			dst = filepath.Join(dst, filename)
225		}
226	}
227
228	// If we're not downloading a directory, then just download the file
229	// and return.
230	if mode == ClientModeFile {
231		getFile := true
232		if checksum != nil {
233			if err := checksum.checksum(dst); err == nil {
234				// don't get the file if the checksum of dst is correct
235				getFile = false
236			}
237		}
238		if getFile {
239			err := g.GetFile(dst, u)
240			if err != nil {
241				return err
242			}
243
244			if checksum != nil {
245				if err := checksum.checksum(dst); err != nil {
246					return err
247				}
248			}
249		}
250
251		if decompressor != nil {
252			// We have a decompressor, so decompress the current destination
253			// into the final destination with the proper mode.
254			err := decompressor.Decompress(decompressDst, dst, decompressDir, c.umask())
255			if err != nil {
256				return err
257			}
258
259			// Swap the information back
260			dst = decompressDst
261			if decompressDir {
262				mode = ClientModeAny
263			} else {
264				mode = ClientModeFile
265			}
266		}
267
268		// We check the dir value again because it can be switched back
269		// if we were unarchiving. If we're still only Get-ing a file, then
270		// we're done.
271		if mode == ClientModeFile {
272			return nil
273		}
274	}
275
276	// If we're at this point we're either downloading a directory or we've
277	// downloaded and unarchived a directory and we're just checking subdir.
278	// In the case we have a decompressor we don't Get because it was Get
279	// above.
280	if decompressor == nil {
281		// If we're getting a directory, then this is an error. You cannot
282		// checksum a directory. TODO: test
283		if checksum != nil {
284			return fmt.Errorf(
285				"checksum cannot be specified for directory download")
286		}
287
288		// We're downloading a directory, which might require a bit more work
289		// if we're specifying a subdir.
290		err := g.Get(dst, u)
291		if err != nil {
292			err = fmt.Errorf("error downloading '%s': %s", src, err)
293			return err
294		}
295	}
296
297	// If we have a subdir, copy that over
298	if subDir != "" {
299		if err := os.RemoveAll(realDst); err != nil {
300			return err
301		}
302		if err := os.MkdirAll(realDst, c.mode(0755)); err != nil {
303			return err
304		}
305
306		// Process any globs
307		subDir, err := SubdirGlob(dst, subDir)
308		if err != nil {
309			return err
310		}
311
312		return copyDir(c.Ctx, realDst, subDir, false, c.umask())
313	}
314
315	return nil
316}
317