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