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  }