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  }