github.com/anchore/syft@v1.38.2/internal/cache/filesystem.go (about) 1 package cache 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "net/url" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 "time" 13 14 "github.com/spf13/afero" 15 16 "github.com/anchore/syft/internal/log" 17 ) 18 19 // NewFromDir creates a new cache manager which returns caches stored on disk, rooted at the given directory 20 func NewFromDir(dir string, ttl time.Duration) (Manager, error) { 21 dir = filepath.Clean(dir) 22 fsys, err := subFs(afero.NewOsFs(), dir) 23 if err != nil { 24 return nil, err 25 } 26 return &filesystemCache{ 27 dir: dir, 28 fs: fsys, 29 ttl: ttl, 30 }, nil 31 } 32 33 const filePermissions = 0700 34 const dirPermissions = os.ModeDir | filePermissions 35 36 type filesystemCache struct { 37 dir string 38 fs afero.Fs 39 ttl time.Duration 40 } 41 42 func (d *filesystemCache) GetCache(name, version string) Cache { 43 fsys, err := subFs(d.fs, name, version) 44 if err != nil { 45 log.Warnf("error getting cache for: %s/%s: %v", name, version, err) 46 return &bypassedCache{} 47 } 48 return &filesystemCache{ 49 dir: filepath.Join(d.dir, name, version), 50 fs: fsys, 51 ttl: d.ttl, 52 } 53 } 54 55 func (d *filesystemCache) RootDirs() []string { 56 if d.dir == "" { 57 return nil 58 } 59 return []string{d.dir} 60 } 61 62 func (d *filesystemCache) Read(key string) (ReaderAtCloser, error) { 63 path := makeDiskKey(key) 64 f, err := d.fs.Open(path) 65 if err != nil { 66 log.Tracef("no cache entry for %s %s: %v", d.dir, key, err) 67 return nil, errNotFound 68 } else if stat, err := f.Stat(); err != nil || stat == nil || time.Since(stat.ModTime()) > d.ttl { 69 log.Tracef("cache entry is too old for %s %s", d.dir, key) 70 return nil, errExpired 71 } 72 log.Tracef("using cache for %s %s", d.dir, key) 73 return f, nil 74 } 75 76 func (d *filesystemCache) Write(key string, contents io.Reader) error { 77 path := makeDiskKey(key) 78 return afero.WriteReader(d.fs, path, contents) 79 } 80 81 // subFs returns a writable directory with the given name under the root cache directory returned from findRoot, 82 // the directory will be created if it does not exist 83 func subFs(fsys afero.Fs, subDirs ...string) (afero.Fs, error) { 84 dir := filepath.Join(subDirs...) 85 dir = filepath.Clean(dir) 86 stat, err := fsys.Stat(dir) 87 if errors.Is(err, afero.ErrFileNotFound) { 88 err = fsys.MkdirAll(dir, dirPermissions) 89 if err != nil { 90 return nil, fmt.Errorf("unable to create directory at '%s': %v", dir, err) 91 } 92 stat, err = fsys.Stat(dir) 93 if err != nil { 94 return nil, err 95 } 96 } 97 if err != nil || stat == nil || !stat.IsDir() { 98 return nil, fmt.Errorf("unable to verify directory '%s': %v", dir, err) 99 } 100 fsys = afero.NewBasePathFs(fsys, dir) 101 return fsys, err 102 } 103 104 var keyReplacer = regexp.MustCompile("[^-._/a-zA-Z0-9]") 105 106 // makeDiskKey makes a safe sub-path but not escape forward slashes, this allows for logical partitioning on disk 107 func makeDiskKey(key string) string { 108 // encode single dot directory 109 if key == "." { 110 return "%2E" 111 } 112 // replace any disallowed chars with encoded form 113 key = keyReplacer.ReplaceAllStringFunc(key, url.QueryEscape) 114 // allow . in names but not .. 115 key = strings.ReplaceAll(key, "..", "%2E%2E") 116 return key 117 } 118 119 var errNotFound = fmt.Errorf("not found") 120 var errExpired = fmt.Errorf("expired")