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 }