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