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")