| // Copyright 2016 The Go Authors. All rights reserved. | 
 | // Use of this source code is governed by a BSD-style | 
 | // license that can be found in the LICENSE file. | 
 |  | 
 | // Package httpdl downloads things from HTTP to local disk. | 
 | package httpdl | 
 |  | 
 | import ( | 
 | 	"fmt" | 
 | 	"io" | 
 | 	"net/http" | 
 | 	"os" | 
 | 	"strconv" | 
 | 	"strings" | 
 | 	"time" | 
 | ) | 
 |  | 
 | // Test hooks: | 
 | var ( | 
 | 	hookIsCurrent func() | 
 | 	// TODO(bradfitz): more? | 
 | ) | 
 |  | 
 | func resetHooks() { | 
 | 	hookIsCurrent = func() {} | 
 | } | 
 |  | 
 | func init() { | 
 | 	resetHooks() | 
 | } | 
 |  | 
 | // Download downloads url to the named local file. | 
 | // | 
 | // It stops after a HEAD request if the local file's modtime and size | 
 | // look correct. | 
 | func Download(file, url string) error { | 
 | 	// Special case hack to recognize GCS URLs and append a | 
 | 	// timestamp as a cache buster... | 
 | 	if strings.HasPrefix(url, "https://storage.googleapis.com") && !strings.Contains(url, "?") { | 
 | 		url += fmt.Sprintf("?%d", time.Now().Unix()) | 
 | 	} | 
 |  | 
 | 	if res, err := head(url); err != nil { | 
 | 		return err | 
 | 	} else if diskFileIsCurrent(file, res) { | 
 | 		hookIsCurrent() | 
 | 		return nil | 
 | 	} | 
 |  | 
 | 	res, err := http.Get(url) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if res.StatusCode != 200 { | 
 | 		return fmt.Errorf("HTTP status code of %s was %v", url, res.Status) | 
 | 	} | 
 | 	modStr := res.Header.Get("Last-Modified") | 
 | 	modTime, err := http.ParseTime(modStr) | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("invalid or missing Last-Modified header %q: %v", modStr, err) | 
 | 	} | 
 | 	tmp := file + ".tmp" | 
 | 	os.Remove(tmp) | 
 | 	os.Remove(file) | 
 | 	f, err := os.Create(tmp) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	_, err = io.Copy(f, res.Body) | 
 | 	res.Body.Close() | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("error copying %v to %v: %v", url, file, err) | 
 | 	} | 
 | 	if err := f.Close(); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := os.Chtimes(tmp, modTime, modTime); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := os.Rename(tmp, file); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | func head(url string) (*http.Response, error) { | 
 | 	res, err := http.Head(url) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	if res.StatusCode != 200 { | 
 | 		return nil, fmt.Errorf("HTTP response of %s was %v (after HEAD request)", url, res.Status) | 
 | 	} | 
 | 	return res, nil | 
 | } | 
 |  | 
 | func diskFileIsCurrent(file string, res *http.Response) bool { | 
 | 	fi, err := os.Stat(file) | 
 | 	if err != nil || !fi.Mode().IsRegular() { | 
 | 		return false | 
 | 	} | 
 | 	mod := res.Header.Get("Last-Modified") | 
 | 	clen := res.Header.Get("Content-Length") | 
 | 	if mod == "" || clen == "" { | 
 | 		return false | 
 | 	} | 
 | 	clen64, err := strconv.ParseInt(clen, 10, 64) | 
 | 	if err != nil || clen64 != fi.Size() { | 
 | 		return false | 
 | 	} | 
 | 	modTime, err := http.ParseTime(mod) | 
 | 	return err == nil && modTime.Equal(fi.ModTime()) | 
 | } |