golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/httpdl/httpdl.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package httpdl downloads things from HTTP to local disk. 6 package httpdl 7 8 import ( 9 "fmt" 10 "io" 11 "net/http" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 ) 17 18 // Test hooks: 19 var ( 20 hookIsCurrent func() 21 // TODO(bradfitz): more? 22 ) 23 24 func resetHooks() { 25 hookIsCurrent = func() {} 26 } 27 28 func init() { 29 resetHooks() 30 } 31 32 // Download downloads url to the named local file. 33 // 34 // It stops after a HEAD request if the local file's modtime and size 35 // look correct. 36 func Download(file, url string) error { 37 // Special case hack to recognize GCS URLs and append a 38 // timestamp as a cache buster... 39 if strings.HasPrefix(url, "https://storage.googleapis.com") && !strings.Contains(url, "?") { 40 url += fmt.Sprintf("?%d", time.Now().Unix()) 41 } 42 43 if res, err := head(url); err != nil { 44 return err 45 } else if diskFileIsCurrent(file, res) { 46 hookIsCurrent() 47 return nil 48 } 49 50 res, err := http.Get(url) 51 if err != nil { 52 return err 53 } 54 if res.StatusCode != 200 { 55 return fmt.Errorf("HTTP status code of %s was %v", url, res.Status) 56 } 57 modStr := res.Header.Get("Last-Modified") 58 modTime, err := http.ParseTime(modStr) 59 if err != nil { 60 return fmt.Errorf("invalid or missing Last-Modified header %q: %v", modStr, err) 61 } 62 tmp := file + ".tmp" 63 os.Remove(tmp) 64 os.Remove(file) 65 f, err := os.Create(tmp) 66 if err != nil { 67 return err 68 } 69 _, err = io.Copy(f, res.Body) 70 res.Body.Close() 71 if err != nil { 72 return fmt.Errorf("error copying %v to %v: %v", url, file, err) 73 } 74 if err := f.Close(); err != nil { 75 return err 76 } 77 if err := os.Chtimes(tmp, modTime, modTime); err != nil { 78 return err 79 } 80 if err := os.Rename(tmp, file); err != nil { 81 return err 82 } 83 return nil 84 } 85 86 func head(url string) (*http.Response, error) { 87 res, err := http.Head(url) 88 if err != nil { 89 return nil, err 90 } 91 if res.StatusCode != 200 { 92 return nil, fmt.Errorf("HTTP response of %s was %v (after HEAD request)", url, res.Status) 93 } 94 return res, nil 95 } 96 97 func diskFileIsCurrent(file string, res *http.Response) bool { 98 fi, err := os.Stat(file) 99 if err != nil || !fi.Mode().IsRegular() { 100 return false 101 } 102 mod := res.Header.Get("Last-Modified") 103 clen := res.Header.Get("Content-Length") 104 if mod == "" || clen == "" { 105 return false 106 } 107 clen64, err := strconv.ParseInt(clen, 10, 64) 108 if err != nil || clen64 != fi.Size() { 109 return false 110 } 111 modTime, err := http.ParseTime(mod) 112 return err == nil && modTime.Equal(fi.ModTime()) 113 }