github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/greenhouse/diskcache/cache.go (about) 1 /* 2 Copyright 2018 The Kubernetes 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 http://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 diskcache implements disk backed cache storage for use in greenhouse 18 package diskcache 19 20 import ( 21 "crypto/sha256" 22 "encoding/hex" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strings" 29 "time" 30 31 "k8s.io/test-infra/greenhouse/diskutil" 32 33 "github.com/sirupsen/logrus" 34 ) 35 36 // ReadHandler should be implemented by cache users for use with Cache.Get 37 type ReadHandler func(exists bool, contents io.ReadSeeker) error 38 39 // Cache implements disk backed cache storage 40 type Cache struct { 41 diskRoot string 42 logger *logrus.Entry 43 } 44 45 // NewCache returns a new Cache given the root directory that should be used 46 // on disk for cache storage 47 func NewCache(diskRoot string) *Cache { 48 return &Cache{ 49 diskRoot: strings.TrimSuffix(diskRoot, string(os.PathListSeparator)), 50 } 51 } 52 53 // KeyToPath converts a cache entry key to a path on disk 54 func (c *Cache) KeyToPath(key string) string { 55 return filepath.Join(c.diskRoot, key) 56 } 57 58 // PathToKey converts a path on disk to a key, assuming the path is actually 59 // under DiskRoot() ... 60 func (c *Cache) PathToKey(key string) string { 61 return strings.TrimPrefix(key, c.diskRoot+string(os.PathSeparator)) 62 } 63 64 // DiskRoot returns the root directory containing all on-disk cache entries 65 func (c *Cache) DiskRoot() string { 66 return c.diskRoot 67 } 68 69 // file path helper 70 func exists(path string) bool { 71 _, err := os.Stat(path) 72 return !os.IsNotExist(err) 73 } 74 75 // file path helper 76 func ensureDir(dir string) error { 77 if exists(dir) { 78 return nil 79 } 80 return os.MkdirAll(dir, os.FileMode(0744)) 81 } 82 83 func removeTemp(path string) { 84 err := os.Remove(path) 85 if err != nil { 86 logrus.WithError(err).Errorf("Failed to remove a temp file: %v", path) 87 } 88 } 89 90 // Put copies the content reader until the end into the cache at key 91 // if contentSHA256 is not "" then the contents will only be stored in the 92 // cache if the content's hex string SHA256 matches 93 func (c *Cache) Put(key string, content io.Reader, contentSHA256 string) error { 94 // make sure directory exists 95 path := c.KeyToPath(key) 96 dir := filepath.Dir(path) 97 err := ensureDir(dir) 98 if err != nil { 99 logrus.WithError(err).Errorf("error ensuring directory '%s' exists", dir) 100 } 101 102 // create a temp file to get the content on disk 103 temp, err := ioutil.TempFile(dir, "temp-put") 104 if err != nil { 105 return fmt.Errorf("failed to create cache entry: %v", err) 106 } 107 108 // fast path copying when not hashing content,s 109 if contentSHA256 == "" { 110 _, err = io.Copy(temp, content) 111 if err != nil { 112 removeTemp(temp.Name()) 113 return fmt.Errorf("failed to copy into cache entry: %v", err) 114 } 115 116 } else { 117 hasher := sha256.New() 118 _, err = io.Copy(io.MultiWriter(temp, hasher), content) 119 if err != nil { 120 removeTemp(temp.Name()) 121 return fmt.Errorf("failed to copy into cache entry: %v", err) 122 } 123 actualContentSHA256 := hex.EncodeToString(hasher.Sum(nil)) 124 if actualContentSHA256 != contentSHA256 { 125 removeTemp(temp.Name()) 126 return fmt.Errorf( 127 "hashes did not match for '%s', given: '%s' actual: '%s", 128 key, contentSHA256, actualContentSHA256) 129 } 130 } 131 132 // move the content to the key location 133 err = temp.Sync() 134 if err != nil { 135 removeTemp(temp.Name()) 136 return fmt.Errorf("failed to sync cache entry: %v", err) 137 } 138 temp.Close() 139 err = os.Rename(temp.Name(), path) 140 if err != nil { 141 removeTemp(temp.Name()) 142 return fmt.Errorf("failed to insert contents into cache: %v", err) 143 } 144 return nil 145 } 146 147 // Get provides your readHandler with the contents at key 148 func (c *Cache) Get(key string, readHandler ReadHandler) error { 149 path := c.KeyToPath(key) 150 f, err := os.Open(path) 151 if err != nil { 152 if os.IsNotExist(err) { 153 return readHandler(false, nil) 154 } 155 return fmt.Errorf("failed to get key: %v", err) 156 } 157 return readHandler(true, f) 158 } 159 160 // EntryInfo are returned when getting entries from the cache 161 type EntryInfo struct { 162 Path string 163 LastAccess time.Time 164 } 165 166 // GetEntries walks the cache dir and returns all paths that exist 167 // In the future this *may* be made smarter 168 func (c *Cache) GetEntries() []EntryInfo { 169 entries := []EntryInfo{} 170 // note we swallow errors because we just need to know what keys exist 171 // some keys missing is OK since this is used for eviction, but not returning 172 // any of the keys due to some error is NOT 173 _ = filepath.Walk(c.diskRoot, func(path string, f os.FileInfo, err error) error { 174 if err != nil { 175 logrus.WithError(err).Error("error getting some entries") 176 return nil 177 } 178 if !f.IsDir() { 179 atime := diskutil.GetATime(path, time.Now()) 180 entries = append(entries, EntryInfo{ 181 Path: path, 182 LastAccess: atime, 183 }) 184 } 185 return nil 186 }) 187 return entries 188 } 189 190 // Delete deletes the file at key 191 func (c *Cache) Delete(key string) error { 192 return os.Remove(c.KeyToPath(key)) 193 }