mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
bd1c1d260c
Signed-off-by: Keith Zantow <kzantow@gmail.com>
120 lines
3 KiB
Go
120 lines
3 KiB
Go
package cache
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/anchore/syft/internal/log"
|
|
)
|
|
|
|
// NewFromDir creates a new cache manager which returns caches stored on disk, rooted at the given directory
|
|
func NewFromDir(dir string, ttl time.Duration) (Manager, error) {
|
|
dir = filepath.Clean(dir)
|
|
fsys, err := subFs(afero.NewOsFs(), dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &filesystemCache{
|
|
dir: dir,
|
|
fs: fsys,
|
|
ttl: ttl,
|
|
}, nil
|
|
}
|
|
|
|
const filePermissions = 0700
|
|
const dirPermissions = os.ModeDir | filePermissions
|
|
|
|
type filesystemCache struct {
|
|
dir string
|
|
fs afero.Fs
|
|
ttl time.Duration
|
|
}
|
|
|
|
func (d *filesystemCache) GetCache(name, version string) Cache {
|
|
fsys, err := subFs(d.fs, name, version)
|
|
if err != nil {
|
|
log.Warnf("error getting cache for: %s/%s: %v", name, version, err)
|
|
return &bypassedCache{}
|
|
}
|
|
return &filesystemCache{
|
|
dir: filepath.Join(d.dir, name, version),
|
|
fs: fsys,
|
|
ttl: d.ttl,
|
|
}
|
|
}
|
|
|
|
func (d *filesystemCache) RootDirs() []string {
|
|
if d.dir == "" {
|
|
return nil
|
|
}
|
|
return []string{d.dir}
|
|
}
|
|
|
|
func (d *filesystemCache) Read(key string) (ReaderAtCloser, error) {
|
|
path := makeDiskKey(key)
|
|
f, err := d.fs.Open(path)
|
|
if err != nil {
|
|
log.Tracef("no cache entry for %s %s: %v", d.dir, key, err)
|
|
return nil, errNotFound
|
|
} else if stat, err := f.Stat(); err != nil || stat == nil || time.Since(stat.ModTime()) > d.ttl {
|
|
log.Tracef("cache entry is too old for %s %s", d.dir, key)
|
|
return nil, errExpired
|
|
}
|
|
log.Tracef("using cache for %s %s", d.dir, key)
|
|
return f, nil
|
|
}
|
|
|
|
func (d *filesystemCache) Write(key string, contents io.Reader) error {
|
|
path := makeDiskKey(key)
|
|
return afero.WriteReader(d.fs, path, contents)
|
|
}
|
|
|
|
// subFs returns a writable directory with the given name under the root cache directory returned from findRoot,
|
|
// the directory will be created if it does not exist
|
|
func subFs(fsys afero.Fs, subDirs ...string) (afero.Fs, error) {
|
|
dir := filepath.Join(subDirs...)
|
|
dir = filepath.Clean(dir)
|
|
stat, err := fsys.Stat(dir)
|
|
if errors.Is(err, afero.ErrFileNotFound) {
|
|
err = fsys.MkdirAll(dir, dirPermissions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create directory at '%s': %v", dir, err)
|
|
}
|
|
stat, err = fsys.Stat(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err != nil || stat == nil || !stat.IsDir() {
|
|
return nil, fmt.Errorf("unable to verify directory '%s': %v", dir, err)
|
|
}
|
|
fsys = afero.NewBasePathFs(fsys, dir)
|
|
return fsys, err
|
|
}
|
|
|
|
var keyReplacer = regexp.MustCompile("[^-._/a-zA-Z0-9]")
|
|
|
|
// makeDiskKey makes a safe sub-path but not escape forward slashes, this allows for logical partitioning on disk
|
|
func makeDiskKey(key string) string {
|
|
// encode single dot directory
|
|
if key == "." {
|
|
return "%2E"
|
|
}
|
|
// replace any disallowed chars with encoded form
|
|
key = keyReplacer.ReplaceAllStringFunc(key, url.QueryEscape)
|
|
// allow . in names but not ..
|
|
key = strings.ReplaceAll(key, "..", "%2E%2E")
|
|
return key
|
|
}
|
|
|
|
var errNotFound = fmt.Errorf("not found")
|
|
var errExpired = fmt.Errorf("expired")
|