github.com/paketoio/libpak@v1.3.1/dependency_cache.go (about)

     1  /*
     2   * Copyright 2018-2020 the original author or authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *      https://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package libpak
    18  
    19  import (
    20  	"crypto/sha256"
    21  	"encoding/hex"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"os"
    26  	"path/filepath"
    27  	"reflect"
    28  
    29  	"github.com/BurntSushi/toml"
    30  	"github.com/buildpacks/libcnb"
    31  	"github.com/heroku/color"
    32  	"github.com/paketoio/libpak/bard"
    33  )
    34  
    35  // DependencyCache allows a user to get an artifact either from a buildpack's cache, a previous download, or to download
    36  // directly.
    37  type DependencyCache struct {
    38  
    39  	// CachePath is the location where the buildpack has cached its dependencies.
    40  	CachePath string
    41  
    42  	// DownloadPath is the location of all downloads during this execution of the build.
    43  	DownloadPath string
    44  
    45  	// Logger is the logger used to write to the console.
    46  	Logger bard.Logger
    47  
    48  	// UserAgent is the User-Agent string to use with requests.
    49  	UserAgent string
    50  }
    51  
    52  // NewDependencyCache creates a new instance setting the default cache path (<BUILDPACK_PATH>/dependencies) and user
    53  // agent (<BUILDPACK_ID>/<BUILDPACK_VERSION>).
    54  func NewDependencyCache(buildpack libcnb.Buildpack) DependencyCache {
    55  	return DependencyCache{
    56  		CachePath:    filepath.Join(buildpack.Path, "dependencies"),
    57  		DownloadPath: os.TempDir(),
    58  		Logger:       bard.NewLogger(os.Stdout),
    59  		UserAgent:    filepath.Join("%s/%s", buildpack.Info.ID, buildpack.Info.Version),
    60  	}
    61  }
    62  
    63  // Artifact returns the path to the artifact.  Resolution of that path follows three tiers:
    64  //
    65  // 1. CachePath
    66  // 2. DownloadPath
    67  // 3. Download from URI
    68  //
    69  // If the BuildpackDependency's SHA256 is not set, the download can never be verified to be up to date and will always
    70  // download, skipping all of the caches.
    71  func (d *DependencyCache) Artifact(dependency BuildpackDependency) (*os.File, error) {
    72  	var (
    73  		actual   BuildpackDependency
    74  		artifact string
    75  		file     string
    76  	)
    77  
    78  	if dependency.SHA256 == "" {
    79  		d.Logger.Header("%s Dependency has no SHA256. Skipping cache.",
    80  			color.New(color.FgYellow, color.Bold).Sprint("Warning:"))
    81  
    82  		d.Logger.Body("%s from %s", color.YellowString("Downloading"), dependency.URI)
    83  		artifact = filepath.Join(d.DownloadPath, filepath.Base(dependency.URI))
    84  		if err := d.download(dependency.URI, artifact); err != nil {
    85  			return nil, fmt.Errorf("unable to download %s: %w", dependency.URI, err)
    86  		}
    87  
    88  		return os.Open(artifact)
    89  	}
    90  
    91  	file = filepath.Join(d.CachePath, fmt.Sprintf("%s.toml", dependency.SHA256))
    92  	if _, err := toml.DecodeFile(file, &actual); err != nil && !os.IsNotExist(err) {
    93  		return nil, fmt.Errorf("unable to decode download metadata %s: %w", file, err)
    94  	}
    95  
    96  	if reflect.DeepEqual(dependency, actual) {
    97  		d.Logger.Body("%s cached download from buildpack", color.GreenString("Reusing"))
    98  		return os.Open(filepath.Join(d.CachePath, dependency.SHA256, filepath.Base(dependency.URI)))
    99  	}
   100  
   101  	file = filepath.Join(d.DownloadPath, fmt.Sprintf("%s.toml", dependency.SHA256))
   102  	if _, err := toml.DecodeFile(file, &actual); err != nil && !os.IsNotExist(err) {
   103  		return nil, fmt.Errorf("unable to decode download metadata %s: %w", file, err)
   104  	}
   105  
   106  	if reflect.DeepEqual(dependency, actual) {
   107  		d.Logger.Body("%s previously cached download", color.GreenString("Reusing"))
   108  		return os.Open(filepath.Join(d.DownloadPath, dependency.SHA256, filepath.Base(dependency.URI)))
   109  	}
   110  
   111  	d.Logger.Body("%s from %s", color.YellowString("Downloading"), dependency.URI)
   112  	artifact = filepath.Join(d.DownloadPath, dependency.SHA256, filepath.Base(dependency.URI))
   113  	if err := d.download(dependency.URI, artifact); err != nil {
   114  		return nil, fmt.Errorf("unable to download %s: %w", dependency.URI, err)
   115  	}
   116  
   117  	d.Logger.Body("Verifying checksum")
   118  	if err := d.verify(artifact, dependency.SHA256); err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	file = filepath.Join(d.DownloadPath, fmt.Sprintf("%s.toml", dependency.SHA256))
   123  	if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
   124  		return nil, fmt.Errorf("unable to make directory %s: %w", filepath.Dir(file), err)
   125  	}
   126  
   127  	out, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
   128  	if err != nil {
   129  		return nil, fmt.Errorf("unable to open file %s: %w", file, err)
   130  	}
   131  	defer out.Close()
   132  
   133  	if err := toml.NewEncoder(out).Encode(dependency); err != nil {
   134  		return nil, fmt.Errorf("unable to write metadata %s: %w", file, err)
   135  	}
   136  
   137  	return os.Open(artifact)
   138  }
   139  
   140  func (d DependencyCache) download(uri string, destination string) error {
   141  	req, err := http.NewRequest("GET", uri, nil)
   142  	if err != nil {
   143  		return fmt.Errorf("unable to create new GET request for %s: %w", uri, err)
   144  	}
   145  
   146  	if d.UserAgent != "" {
   147  		req.Header.Set("User-Agent", d.UserAgent)
   148  	}
   149  
   150  	t := &http.Transport{Proxy: http.ProxyFromEnvironment}
   151  	t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
   152  
   153  	client := http.Client{Transport: t}
   154  	resp, err := client.Do(req)
   155  	if err != nil {
   156  		return fmt.Errorf("unable to request %s: %w", uri, err)
   157  	}
   158  	defer resp.Body.Close()
   159  
   160  	if resp.StatusCode < 200 || resp.StatusCode > 299 {
   161  		return fmt.Errorf("could not download %s: %d", uri, resp.StatusCode)
   162  	}
   163  
   164  	if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
   165  		return fmt.Errorf("unable to make directory %s: %w", filepath.Dir(destination), err)
   166  	}
   167  
   168  	out, err := os.OpenFile(destination, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   169  	if err != nil {
   170  		return fmt.Errorf("unable to open file %s: %w", destination, err)
   171  	}
   172  	defer out.Close()
   173  
   174  	if _, err := io.Copy(out, resp.Body); err != nil {
   175  		return fmt.Errorf("unable to copy from %s to %s: %w", uri, destination, err)
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func (DependencyCache) verify(path string, expected string) error {
   182  	s := sha256.New()
   183  
   184  	in, err := os.Open(path)
   185  	if err != nil {
   186  		return fmt.Errorf("unable to verify %s: %w", path, err)
   187  	}
   188  	defer in.Close()
   189  
   190  	if _, err := io.Copy(s, in); err != nil {
   191  		return fmt.Errorf("unable to read %s: %w", path, err)
   192  	}
   193  
   194  	actual := hex.EncodeToString(s.Sum(nil))
   195  
   196  	if expected != actual {
   197  		return fmt.Errorf("sha256 for %s %s does not match expected %s", path, actual, expected)
   198  	}
   199  
   200  	return nil
   201  }