github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/store/cache.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package store 21 22 import ( 23 "fmt" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "sort" 28 "syscall" 29 "time" 30 31 "github.com/snapcore/snapd/logger" 32 "github.com/snapcore/snapd/osutil" 33 ) 34 35 // overridden in the unit tests 36 var osRemove = os.Remove 37 38 // downloadCache is the interface that a store download cache must provide 39 type downloadCache interface { 40 // Get gets the given cacheKey content and puts it into targetPath 41 Get(cacheKey, targetPath string) error 42 // Put adds a new file to the cache 43 Put(cacheKey, sourcePath string) error 44 // Get full path of the file in cache 45 GetPath(cacheKey string) string 46 } 47 48 // nullCache is cache that does not cache 49 type nullCache struct{} 50 51 func (cm *nullCache) Get(cacheKey, targetPath string) error { 52 return fmt.Errorf("cannot get items from the nullCache") 53 } 54 func (cm *nullCache) GetPath(cacheKey string) string { 55 return "" 56 } 57 func (cm *nullCache) Put(cacheKey, sourcePath string) error { return nil } 58 59 // changesByMtime sorts by the mtime of files 60 type changesByMtime []os.FileInfo 61 62 func (s changesByMtime) Len() int { return len(s) } 63 func (s changesByMtime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 64 func (s changesByMtime) Less(i, j int) bool { return s[i].ModTime().Before(s[j].ModTime()) } 65 66 // cacheManager implements a downloadCache via content based hard linking 67 type CacheManager struct { 68 cacheDir string 69 maxItems int 70 } 71 72 // NewCacheManager returns a new CacheManager with the given cacheDir 73 // and the given maximum amount of items. The idea behind it is the 74 // following algorithm: 75 // 76 // 1. When starting a download, check if it exists in $cacheDir 77 // 2. If found, update its mtime, hardlink into target location, and 78 // return success 79 // 3. If not found, download the snap 80 // 4. On success, hardlink into $cacheDir/<digest> 81 // 5. If cache dir has more than maxItems entries, remove oldest mtimes 82 // until it has maxItems 83 // 84 // The caching part is done here, the downloading happens in the store.go 85 // code. 86 func NewCacheManager(cacheDir string, maxItems int) *CacheManager { 87 return &CacheManager{ 88 cacheDir: cacheDir, 89 maxItems: maxItems, 90 } 91 } 92 93 // GetPath returns the full path of the given content in the cache 94 // or empty string 95 func (cm *CacheManager) GetPath(cacheKey string) string { 96 if _, err := os.Stat(cm.path(cacheKey)); os.IsNotExist(err) { 97 return "" 98 } 99 return cm.path(cacheKey) 100 } 101 102 // Get gets the given cacheKey content and puts it into targetPath 103 func (cm *CacheManager) Get(cacheKey, targetPath string) error { 104 if err := os.Link(cm.path(cacheKey), targetPath); err != nil { 105 return err 106 } 107 logger.Debugf("using cache for %s", targetPath) 108 now := time.Now() 109 return os.Chtimes(targetPath, now, now) 110 } 111 112 // Put adds a new file to the cache with the given cacheKey 113 func (cm *CacheManager) Put(cacheKey, sourcePath string) error { 114 // always try to create the cache dir first or the following 115 // osutil.IsWritable will always fail if the dir is missing 116 _ = os.MkdirAll(cm.cacheDir, 0700) 117 118 // happens on e.g. `snap download` which runs as the user 119 if !osutil.IsWritable(cm.cacheDir) { 120 return nil 121 } 122 123 err := os.Link(sourcePath, cm.path(cacheKey)) 124 if os.IsExist(err) { 125 now := time.Now() 126 err := os.Chtimes(cm.path(cacheKey), now, now) 127 // this can happen if a cleanup happens in parallel, ie. 128 // the file was there but cleanup() removed it between 129 // the os.Link/os.Chtimes - no biggie, just link it again 130 if os.IsNotExist(err) { 131 return os.Link(sourcePath, cm.path(cacheKey)) 132 } 133 return err 134 } 135 if err != nil { 136 return err 137 } 138 return cm.cleanup() 139 } 140 141 // count returns the number of items in the cache 142 func (cm *CacheManager) count() int { 143 // TODO: Use something more effective than a list of all entries 144 // here. This will waste a lot of memory on large dirs. 145 if l, err := ioutil.ReadDir(cm.cacheDir); err == nil { 146 return len(l) 147 } 148 return 0 149 } 150 151 // path returns the full path of the given content in the cache 152 func (cm *CacheManager) path(cacheKey string) string { 153 return filepath.Join(cm.cacheDir, cacheKey) 154 } 155 156 // cleanup ensures that only maxItems are stored in the cache 157 func (cm *CacheManager) cleanup() error { 158 fil, err := ioutil.ReadDir(cm.cacheDir) 159 if err != nil { 160 return err 161 } 162 if len(fil) <= cm.maxItems { 163 return nil 164 } 165 166 numOwned := 0 167 for _, fi := range fil { 168 n, err := hardLinkCount(fi) 169 if err != nil { 170 logger.Noticef("cannot inspect cache: %s", err) 171 } 172 // Only count the file if it is not referenced elsewhere in the filesystem 173 if n <= 1 { 174 numOwned++ 175 } 176 } 177 178 if numOwned <= cm.maxItems { 179 return nil 180 } 181 182 var lastErr error 183 sort.Sort(changesByMtime(fil)) 184 deleted := 0 185 for _, fi := range fil { 186 path := cm.path(fi.Name()) 187 n, err := hardLinkCount(fi) 188 if err != nil { 189 logger.Noticef("cannot inspect cache: %s", err) 190 } 191 // If the file is referenced in the filesystem somewhere 192 // else our copy is "free" so skip it. If there is any 193 // error we cleanup the file (it is just a cache afterall). 194 if n > 1 { 195 continue 196 } 197 if err := osRemove(path); err != nil { 198 if !os.IsNotExist(err) { 199 logger.Noticef("cannot cleanup cache: %s", err) 200 lastErr = err 201 } 202 continue 203 } 204 deleted++ 205 if numOwned-deleted <= cm.maxItems { 206 break 207 } 208 } 209 return lastErr 210 } 211 212 // hardLinkCount returns the number of hardlinks for the given path 213 func hardLinkCount(fi os.FileInfo) (uint64, error) { 214 if stat, ok := fi.Sys().(*syscall.Stat_t); ok && stat != nil { 215 return uint64(stat.Nlink), nil 216 } 217 return 0, fmt.Errorf("internal error: cannot read hardlink count from %s", fi.Name()) 218 }