github.com/coreos/mantle@v0.13.0/sdk/download.go (about)

     1  // Copyright 2015 CoreOS, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package sdk
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	"net/url"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/coreos/pkg/capnslog"
    30  	"google.golang.org/api/storage/v1"
    31  
    32  	"github.com/coreos/mantle/system"
    33  	"github.com/coreos/mantle/util"
    34  )
    35  
    36  const (
    37  	urlHost = "storage.googleapis.com"
    38  	urlPath = "/builds.developer.core-os.net/sdk"
    39  )
    40  
    41  var plog = capnslog.NewPackageLogger("github.com/coreos/mantle", "sdk")
    42  
    43  func TarballName(version string) string {
    44  	arch := system.PortageArch()
    45  	return fmt.Sprintf("coreos-sdk-%s-%s.tar.bz2", arch, version)
    46  }
    47  
    48  func TarballURL(version string) string {
    49  	arch := system.PortageArch()
    50  	p := path.Join(urlPath, arch, version, TarballName(version))
    51  	u := url.URL{Scheme: "https", Host: urlHost, Path: p}
    52  	return u.String()
    53  }
    54  
    55  func DownloadFile(file, fileURL string, client *http.Client) error {
    56  	plog.Infof("Downloading %s to %s", fileURL, file)
    57  
    58  	// handle bucket urls by using api to get media link
    59  	parseURL, err := url.Parse(fileURL)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	resolveGS := func() error {
    64  		if client == nil {
    65  			client = http.DefaultClient
    66  		}
    67  		api, err := storage.New(client)
    68  		if err != nil {
    69  			return err
    70  		}
    71  		path := strings.TrimLeft(parseURL.Path, "/")
    72  		obj, err := api.Objects.Get(parseURL.Host, path).Do()
    73  		if err != nil {
    74  			return fmt.Errorf("%s: %s", err, fileURL)
    75  		}
    76  		fileURL = obj.MediaLink
    77  		return nil
    78  	}
    79  	if parseURL.Scheme == "gs" {
    80  		if err := util.Retry(5, 1*time.Second, resolveGS); err != nil {
    81  			return err
    82  		}
    83  	}
    84  
    85  	if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
    86  		return err
    87  	}
    88  
    89  	download := func() error {
    90  		return downloadFile(file, fileURL, client)
    91  	}
    92  	if err := util.Retry(5, 1*time.Second, download); err != nil {
    93  		return err
    94  	}
    95  	return nil
    96  }
    97  
    98  func downloadFile(file, url string, client *http.Client) error {
    99  	if client == nil {
   100  		client = http.DefaultClient
   101  	}
   102  
   103  	dst, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE, 0666)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	defer dst.Close()
   108  
   109  	pos, err := dst.Seek(0, os.SEEK_END)
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	req, err := http.NewRequest("GET", url, nil)
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	if pos != 0 {
   120  		req.Header.Add("Range", fmt.Sprintf("bytes=%d-", pos))
   121  	}
   122  
   123  	resp, err := client.Do(req)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	defer resp.Body.Close()
   128  
   129  	var length int64
   130  	switch resp.StatusCode {
   131  	case http.StatusOK:
   132  		if pos != 0 {
   133  			if _, err := dst.Seek(0, os.SEEK_SET); err != nil {
   134  				return err
   135  			}
   136  			if err := dst.Truncate(0); err != nil {
   137  				return err
   138  			}
   139  			pos = 0
   140  		}
   141  		length = resp.ContentLength
   142  	case http.StatusPartialContent:
   143  		var end int64
   144  		n, _ := fmt.Sscanf(resp.Header.Get("Content-Range"),
   145  			"bytes %d-%d/%d", &pos, &end, &length)
   146  		if n != 3 {
   147  			return fmt.Errorf("Bad Content-Range for %s", resp.Request.URL)
   148  		}
   149  
   150  		if _, err := dst.Seek(pos, os.SEEK_SET); err != nil {
   151  			return err
   152  		}
   153  		plog.Infof("Resuming from byte %d", pos)
   154  	case http.StatusRequestedRangeNotSatisfiable:
   155  		plog.Infof("Download already complete")
   156  		return nil
   157  	default:
   158  		return fmt.Errorf("%s: %s", resp.Status, resp.Request.URL)
   159  	}
   160  
   161  	prefix := filepath.Base(file)
   162  	if n, err := util.CopyProgress(capnslog.INFO, prefix, dst, resp.Body, resp.ContentLength); err != nil {
   163  		return err
   164  	} else if n != length-pos {
   165  		// unsure if this is worth caring about
   166  		plog.Infof("Downloaded %d bytes, expected %d", n, length-pos)
   167  		return nil
   168  	} else {
   169  		plog.Infof("Downloaded %d bytes", n)
   170  		return nil
   171  	}
   172  }
   173  
   174  func DownloadSignedFile(file, url string, client *http.Client, verifyKeyFile string) error {
   175  
   176  	if _, err := os.Stat(file + ".sig"); err == nil {
   177  		if e := VerifyFile(file, verifyKeyFile); e == nil {
   178  			plog.Infof("Verified existing file: %s", file)
   179  			return nil
   180  		}
   181  	}
   182  
   183  	if err := DownloadFile(file, url, client); err != nil {
   184  		return err
   185  	}
   186  
   187  	if err := DownloadFile(file+".sig", url+".sig", client); err != nil {
   188  		return err
   189  	}
   190  
   191  	if err := VerifyFile(file, verifyKeyFile); err != nil {
   192  		return err
   193  	}
   194  
   195  	plog.Infof("Verified file: %s", file)
   196  	return nil
   197  }
   198  
   199  func DownloadSDK(version, verifyKeyFile string) error {
   200  	tarFile := filepath.Join(RepoCache(), "sdks", TarballName(version))
   201  	tarURL := TarballURL(version)
   202  	return DownloadSignedFile(tarFile, tarURL, nil, verifyKeyFile)
   203  }
   204  
   205  // false if both files do not exist
   206  func cmpFileBytes(file1, file2 string) (bool, error) {
   207  	info1, err := os.Stat(file1)
   208  	if err != nil {
   209  		return false, err
   210  	}
   211  	info2, err := os.Stat(file2)
   212  	if err != nil {
   213  		return false, err
   214  	}
   215  	if info1.Size() != info2.Size() {
   216  		return false, nil
   217  	}
   218  
   219  	f1, err := os.Open(file1)
   220  	if err != nil {
   221  		return false, err
   222  	}
   223  	defer f1.Close()
   224  	f2, err := os.Open(file2)
   225  	if err != nil {
   226  		return false, err
   227  	}
   228  	defer f2.Close()
   229  
   230  	const defaultBufSize = 4096 // same as bufio
   231  	buf1 := make([]byte, defaultBufSize)
   232  	buf2 := make([]byte, defaultBufSize)
   233  
   234  	for {
   235  		n1, err1 := io.ReadFull(f1, buf1)
   236  		n2, err2 := io.ReadFull(f2, buf2)
   237  
   238  		if err1 == io.EOF && err2 == io.EOF {
   239  			return true, nil
   240  		} else if err1 == io.EOF || err2 == io.EOF {
   241  			return false, nil
   242  		}
   243  
   244  		if err1 == io.ErrUnexpectedEOF && err2 == io.ErrUnexpectedEOF {
   245  			return bytes.Equal(buf1[:n1], buf2[:n2]), nil
   246  		} else if err1 == io.ErrUnexpectedEOF || err2 == io.ErrUnexpectedEOF {
   247  			return false, nil
   248  		}
   249  
   250  		if err1 != nil {
   251  			return false, err1
   252  		}
   253  		if err2 != nil {
   254  			return false, err2
   255  		}
   256  
   257  		if !bytes.Equal(buf1, buf2) {
   258  			return false, nil
   259  		}
   260  	}
   261  }
   262  
   263  // UpdateFile downloads a file to temp dir and replaces the file only if
   264  // contents have changed. If tempDir is "" default will be os.TempDir().
   265  // Leave client nil to use default.
   266  func UpdateFile(file, url string, client *http.Client) error {
   267  	if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
   268  		return err
   269  	}
   270  
   271  	tempFile := file + ".part"
   272  	if err := DownloadFile(tempFile, url, client); err != nil {
   273  		return fmt.Errorf("%s: %s", url, err)
   274  	}
   275  	defer os.Remove(tempFile)
   276  
   277  	equal, err := cmpFileBytes(file, tempFile)
   278  	if os.IsExist(err) { // file may not exist, that is ok
   279  		return err
   280  	}
   281  	if equal {
   282  		plog.Infof("%v is up to date", file)
   283  		return nil
   284  	}
   285  
   286  	// not equal so delete any existing file and rename tempFile to file
   287  	if err := os.Rename(tempFile, file); err != nil {
   288  		return err
   289  	}
   290  	return nil
   291  }
   292  
   293  // UpdateSignedFile will download and replace the local file if the
   294  // published signature doesn't match the local copy. Leave client nil to
   295  // use default.
   296  func UpdateSignedFile(file, url string, client *http.Client, verifyKeyFile string) error {
   297  	sigFile := file + ".sig"
   298  	sigURL := url + ".sig"
   299  
   300  	// update local sig to latest
   301  	if err := UpdateFile(sigFile, sigURL, client); err != nil {
   302  		return err
   303  	}
   304  
   305  	// try to verify with latest sig
   306  	if e := VerifyFile(file, verifyKeyFile); e == nil {
   307  		plog.Infof("Verified existing file: %s", file)
   308  		return nil
   309  	}
   310  
   311  	// download image and try to verify again
   312  	if err := UpdateFile(file, url, client); err != nil {
   313  		return err
   314  	}
   315  	if err := VerifyFile(file, verifyKeyFile); err != nil {
   316  		return err
   317  	}
   318  
   319  	plog.Infof("Verified file: %s", file)
   320  	return nil
   321  }