github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/vfs/vfscache/vfscache.go (about)

     1  // Package vfscache deals with caching of files locally for the VFS layer
     2  package vfscache
     3  
     4  import (
     5  	"context"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"runtime"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/djherbis/times"
    16  	"github.com/pkg/errors"
    17  	"github.com/rclone/rclone/fs"
    18  	fscache "github.com/rclone/rclone/fs/cache"
    19  	"github.com/rclone/rclone/fs/config"
    20  	"github.com/rclone/rclone/fs/operations"
    21  	"github.com/rclone/rclone/vfs/vfscommon"
    22  )
    23  
    24  // Cache opened files
    25  type Cache struct {
    26  	fremote fs.Fs                 // fs for the remote we are caching
    27  	fcache  fs.Fs                 // fs for the cache directory
    28  	opt     *vfscommon.Options    // vfs Options
    29  	root    string                // root of the cache directory
    30  	itemMu  sync.Mutex            // protects the following variables
    31  	item    map[string]*cacheItem // files/directories in the cache
    32  	used    int64                 // total size of files in the cache
    33  }
    34  
    35  // cacheItem is stored in the item map
    36  type cacheItem struct {
    37  	opens  int       // number of times file is open
    38  	atime  time.Time // last time file was accessed
    39  	isFile bool      // if this is a file or a directory
    40  	size   int64     // size of the cached item
    41  }
    42  
    43  // newCacheItem returns an item for the cache
    44  func newCacheItem(isFile bool) *cacheItem {
    45  	return &cacheItem{atime: time.Now(), isFile: isFile}
    46  }
    47  
    48  // New creates a new cache heirachy for fremote
    49  //
    50  // This starts background goroutines which can be cancelled with the
    51  // context passed in.
    52  func New(ctx context.Context, fremote fs.Fs, opt *vfscommon.Options) (*Cache, error) {
    53  	fRoot := filepath.FromSlash(fremote.Root())
    54  	if runtime.GOOS == "windows" {
    55  		if strings.HasPrefix(fRoot, `\\?`) {
    56  			fRoot = fRoot[3:]
    57  		}
    58  		fRoot = strings.Replace(fRoot, ":", "", -1)
    59  	}
    60  	root := filepath.Join(config.CacheDir, "vfs", fremote.Name(), fRoot)
    61  	fs.Debugf(nil, "vfs cache root is %q", root)
    62  
    63  	fcache, err := fscache.Get(root)
    64  	if err != nil {
    65  		return nil, errors.Wrap(err, "failed to create cache remote")
    66  	}
    67  
    68  	c := &Cache{
    69  		fremote: fremote,
    70  		fcache:  fcache,
    71  		opt:     opt,
    72  		root:    root,
    73  		item:    make(map[string]*cacheItem),
    74  	}
    75  
    76  	go c.cleaner(ctx)
    77  
    78  	return c, nil
    79  }
    80  
    81  // clean returns the cleaned version of name for use in the index map
    82  //
    83  // name should be a remote path not an osPath
    84  func clean(name string) string {
    85  	name = strings.Trim(name, "/")
    86  	name = path.Clean(name)
    87  	if name == "." || name == "/" {
    88  		name = ""
    89  	}
    90  	return name
    91  }
    92  
    93  // ToOSPath turns a remote relative name into an OS path in the cache
    94  func (c *Cache) ToOSPath(name string) string {
    95  	return filepath.Join(c.root, filepath.FromSlash(name))
    96  }
    97  
    98  // Mkdir makes the directory for name in the cache and returns an os
    99  // path for the file
   100  //
   101  // name should be a remote path not an osPath
   102  func (c *Cache) Mkdir(name string) (string, error) {
   103  	parent := vfscommon.FindParent(name)
   104  	leaf := path.Base(name)
   105  	parentPath := c.ToOSPath(parent)
   106  	err := os.MkdirAll(parentPath, 0700)
   107  	if err != nil {
   108  		return "", errors.Wrap(err, "make cache directory failed")
   109  	}
   110  	c.cacheDir(parent)
   111  	return filepath.Join(parentPath, leaf), nil
   112  }
   113  
   114  // _get gets name from the cache or creates a new one
   115  //
   116  // It returns the item and found as to whether this item was found in
   117  // the cache (or just created).
   118  //
   119  // name should be a remote path not an osPath
   120  //
   121  // must be called with itemMu held
   122  func (c *Cache) _get(isFile bool, name string) (item *cacheItem, found bool) {
   123  	item = c.item[name]
   124  	found = item != nil
   125  	if !found {
   126  		item = newCacheItem(isFile)
   127  		c.item[name] = item
   128  	}
   129  	return item, found
   130  }
   131  
   132  // Opens returns the number of opens that are on the file
   133  //
   134  // name should be a remote path not an osPath
   135  func (c *Cache) Opens(name string) int {
   136  	name = clean(name)
   137  	c.itemMu.Lock()
   138  	defer c.itemMu.Unlock()
   139  	item := c.item[name]
   140  	if item == nil {
   141  		return 0
   142  	}
   143  	return item.opens
   144  }
   145  
   146  // get gets name from the cache or creates a new one
   147  //
   148  // name should be a remote path not an osPath
   149  func (c *Cache) get(name string) *cacheItem {
   150  	name = clean(name)
   151  	c.itemMu.Lock()
   152  	item, _ := c._get(true, name)
   153  	c.itemMu.Unlock()
   154  	return item
   155  }
   156  
   157  // updateStat sets the atime of the name to that passed in if it is
   158  // newer than the existing or there isn't an existing time.
   159  //
   160  // it also sets the size
   161  //
   162  // name should be a remote path not an osPath
   163  func (c *Cache) updateStat(name string, when time.Time, size int64) {
   164  	name = clean(name)
   165  	c.itemMu.Lock()
   166  	item, found := c._get(true, name)
   167  	if !found || when.Sub(item.atime) > 0 {
   168  		fs.Debugf(name, "updateTime: setting atime to %v", when)
   169  		item.atime = when
   170  	}
   171  	item.size = size
   172  	c.itemMu.Unlock()
   173  }
   174  
   175  // _open marks name as open, must be called with the lock held
   176  //
   177  // name should be a remote path not an osPath
   178  func (c *Cache) _open(isFile bool, name string) {
   179  	for {
   180  		item, _ := c._get(isFile, name)
   181  		item.opens++
   182  		item.atime = time.Now()
   183  		if name == "" {
   184  			break
   185  		}
   186  		isFile = false
   187  		name = vfscommon.FindParent(name)
   188  	}
   189  }
   190  
   191  // Open marks name as open
   192  //
   193  // name should be a remote path not an osPath
   194  func (c *Cache) Open(name string) {
   195  	name = clean(name)
   196  	c.itemMu.Lock()
   197  	c._open(true, name)
   198  	c.itemMu.Unlock()
   199  }
   200  
   201  // cacheDir marks a directory and its parents as being in the cache
   202  //
   203  // name should be a remote path not an osPath
   204  func (c *Cache) cacheDir(name string) {
   205  	name = clean(name)
   206  	c.itemMu.Lock()
   207  	defer c.itemMu.Unlock()
   208  	for {
   209  		item := c.item[name]
   210  		if item != nil {
   211  			break
   212  		}
   213  		c.item[name] = newCacheItem(false)
   214  		if name == "" {
   215  			break
   216  		}
   217  		name = vfscommon.FindParent(name)
   218  	}
   219  }
   220  
   221  // Exists checks to see if the file exists in the cache or not
   222  func (c *Cache) Exists(name string) bool {
   223  	osPath := c.ToOSPath(name)
   224  	fi, err := os.Stat(osPath)
   225  	if err != nil {
   226  		return false
   227  	}
   228  	// checks for non-regular files (e.g. directories, symlinks, devices, etc.)
   229  	if !fi.Mode().IsRegular() {
   230  		return false
   231  	}
   232  	return true
   233  }
   234  
   235  // Rename the file in cache
   236  func (c *Cache) Rename(name string, newName string) (err error) {
   237  	osOldPath := c.ToOSPath(name)
   238  	osNewPath := c.ToOSPath(newName)
   239  	sfi, err := os.Stat(osOldPath)
   240  	if err != nil {
   241  		return errors.Wrapf(err, "Failed to stat source: %s", osOldPath)
   242  	}
   243  	if !sfi.Mode().IsRegular() {
   244  		// cannot copy non-regular files (e.g., directories, symlinks, devices, etc.)
   245  		return errors.Errorf("Non-regular source file: %s (%q)", sfi.Name(), sfi.Mode().String())
   246  	}
   247  	dfi, err := os.Stat(osNewPath)
   248  	if err != nil {
   249  		if !os.IsNotExist(err) {
   250  			return errors.Wrapf(err, "Failed to stat destination: %s", osNewPath)
   251  		}
   252  		parent := vfscommon.OsFindParent(osNewPath)
   253  		err = os.MkdirAll(parent, 0700)
   254  		if err != nil {
   255  			return errors.Wrapf(err, "Failed to create parent dir: %s", parent)
   256  		}
   257  	} else {
   258  		if !(dfi.Mode().IsRegular()) {
   259  			return errors.Errorf("Non-regular destination file: %s (%q)", dfi.Name(), dfi.Mode().String())
   260  		}
   261  		if os.SameFile(sfi, dfi) {
   262  			return nil
   263  		}
   264  	}
   265  	if err = os.Rename(osOldPath, osNewPath); err != nil {
   266  		return errors.Wrapf(err, "Failed to rename in cache: %s to %s", osOldPath, osNewPath)
   267  	}
   268  	// Rename the cache item
   269  	c.itemMu.Lock()
   270  	if oldItem, ok := c.item[name]; ok {
   271  		c.item[newName] = oldItem
   272  		delete(c.item, name)
   273  	}
   274  	c.itemMu.Unlock()
   275  	fs.Infof(name, "Renamed in cache")
   276  	return nil
   277  }
   278  
   279  // _close marks name as closed - must be called with the lock held
   280  func (c *Cache) _close(isFile bool, name string) {
   281  	for {
   282  		item, _ := c._get(isFile, name)
   283  		item.opens--
   284  		item.atime = time.Now()
   285  		if item.opens < 0 {
   286  			fs.Errorf(name, "cache: double close")
   287  		}
   288  		osPath := c.ToOSPath(name)
   289  		fi, err := os.Stat(osPath)
   290  		// Update the size on close
   291  		if err == nil && !fi.IsDir() {
   292  			item.size = fi.Size()
   293  		}
   294  		if name == "" {
   295  			break
   296  		}
   297  		isFile = false
   298  		name = vfscommon.FindParent(name)
   299  	}
   300  }
   301  
   302  // Close marks name as closed
   303  //
   304  // name should be a remote path not an osPath
   305  func (c *Cache) Close(name string) {
   306  	name = clean(name)
   307  	c.itemMu.Lock()
   308  	c._close(true, name)
   309  	c.itemMu.Unlock()
   310  }
   311  
   312  // Remove should be called if name is deleted
   313  func (c *Cache) Remove(name string) {
   314  	osPath := c.ToOSPath(name)
   315  	err := os.Remove(osPath)
   316  	if err != nil && !os.IsNotExist(err) {
   317  		fs.Errorf(name, "Failed to remove from cache: %v", err)
   318  	} else {
   319  		fs.Infof(name, "Removed from cache")
   320  	}
   321  }
   322  
   323  // removeDir should be called if dir is deleted and returns true if
   324  // the directory is gone.
   325  func (c *Cache) removeDir(dir string) bool {
   326  	osPath := c.ToOSPath(dir)
   327  	err := os.Remove(osPath)
   328  	if err == nil || os.IsNotExist(err) {
   329  		if err == nil {
   330  			fs.Debugf(dir, "Removed empty directory")
   331  		}
   332  		return true
   333  	}
   334  	if !os.IsExist(err) {
   335  		fs.Errorf(dir, "Failed to remove cached dir: %v", err)
   336  	}
   337  	return false
   338  }
   339  
   340  // SetModTime should be called to set the modification time of the cache file
   341  func (c *Cache) SetModTime(name string, modTime time.Time) {
   342  	osPath := c.ToOSPath(name)
   343  	err := os.Chtimes(osPath, modTime, modTime)
   344  	if err != nil {
   345  		fs.Errorf(name, "Failed to set modification time of cached file: %v", err)
   346  	}
   347  }
   348  
   349  // CleanUp empties the cache of everything
   350  func (c *Cache) CleanUp() error {
   351  	return os.RemoveAll(c.root)
   352  }
   353  
   354  // walk walks the cache calling the function
   355  func (c *Cache) walk(fn func(osPath string, fi os.FileInfo, name string) error) error {
   356  	return filepath.Walk(c.root, func(osPath string, fi os.FileInfo, err error) error {
   357  		if err != nil {
   358  			return err
   359  		}
   360  		// Find path relative to the cache root
   361  		name, err := filepath.Rel(c.root, osPath)
   362  		if err != nil {
   363  			return errors.Wrap(err, "filepath.Rel failed in walk")
   364  		}
   365  		if name == "." {
   366  			name = ""
   367  		}
   368  		// And convert into slashes
   369  		name = filepath.ToSlash(name)
   370  
   371  		return fn(osPath, fi, name)
   372  	})
   373  }
   374  
   375  // updateStats walks the cache updating any atimes and sizes it finds
   376  //
   377  // it also updates used
   378  func (c *Cache) updateStats() error {
   379  	var newUsed int64
   380  	err := c.walk(func(osPath string, fi os.FileInfo, name string) error {
   381  		if !fi.IsDir() {
   382  			// Update the atime with that of the file
   383  			atime := times.Get(fi).AccessTime()
   384  			c.updateStat(name, atime, fi.Size())
   385  			newUsed += fi.Size()
   386  		} else {
   387  			c.cacheDir(name)
   388  		}
   389  		return nil
   390  	})
   391  	c.itemMu.Lock()
   392  	c.used = newUsed
   393  	c.itemMu.Unlock()
   394  	return err
   395  }
   396  
   397  // purgeOld gets rid of any files that are over age
   398  func (c *Cache) purgeOld(maxAge time.Duration) {
   399  	c._purgeOld(maxAge, c.Remove)
   400  }
   401  
   402  func (c *Cache) _purgeOld(maxAge time.Duration, remove func(name string)) {
   403  	c.itemMu.Lock()
   404  	defer c.itemMu.Unlock()
   405  	cutoff := time.Now().Add(-maxAge)
   406  	for name, item := range c.item {
   407  		if item.isFile && item.opens == 0 {
   408  			// If not locked and access time too long ago - delete the file
   409  			dt := item.atime.Sub(cutoff)
   410  			// fs.Debugf(name, "atime=%v cutoff=%v, dt=%v", item.atime, cutoff, dt)
   411  			if dt < 0 {
   412  				remove(name)
   413  				// Remove the entry
   414  				delete(c.item, name)
   415  			}
   416  		}
   417  	}
   418  }
   419  
   420  // Purge any empty directories
   421  func (c *Cache) purgeEmptyDirs() {
   422  	c._purgeEmptyDirs(c.removeDir)
   423  }
   424  
   425  func (c *Cache) _purgeEmptyDirs(removeDir func(name string) bool) {
   426  	c.itemMu.Lock()
   427  	defer c.itemMu.Unlock()
   428  	var dirs []string
   429  	for name, item := range c.item {
   430  		if !item.isFile && item.opens == 0 {
   431  			dirs = append(dirs, name)
   432  		}
   433  	}
   434  	// remove empty directories in reverse alphabetical order
   435  	sort.Strings(dirs)
   436  	for i := len(dirs) - 1; i >= 0; i-- {
   437  		dir := dirs[i]
   438  		// Remove the entry
   439  		if removeDir(dir) {
   440  			delete(c.item, dir)
   441  		}
   442  	}
   443  }
   444  
   445  // This is a cacheItem with a name for sorting
   446  type cacheNamedItem struct {
   447  	name string
   448  	item *cacheItem
   449  }
   450  type cacheNamedItems []cacheNamedItem
   451  
   452  func (v cacheNamedItems) Len() int           { return len(v) }
   453  func (v cacheNamedItems) Swap(i, j int)      { v[i], v[j] = v[j], v[i] }
   454  func (v cacheNamedItems) Less(i, j int) bool { return v[i].item.atime.Before(v[j].item.atime) }
   455  
   456  // Remove any files that are over quota starting from the
   457  // oldest first
   458  func (c *Cache) purgeOverQuota(quota int64) {
   459  	c._purgeOverQuota(quota, c.Remove)
   460  }
   461  
   462  func (c *Cache) _purgeOverQuota(quota int64, remove func(name string)) {
   463  	c.itemMu.Lock()
   464  	defer c.itemMu.Unlock()
   465  
   466  	if quota <= 0 || c.used < quota {
   467  		return
   468  	}
   469  
   470  	var items cacheNamedItems
   471  
   472  	// Make a slice of unused files
   473  	for name, item := range c.item {
   474  		if item.isFile && item.opens == 0 {
   475  			items = append(items, cacheNamedItem{
   476  				name: name,
   477  				item: item,
   478  			})
   479  		}
   480  	}
   481  	sort.Sort(items)
   482  
   483  	// Remove items until the quota is OK
   484  	for _, item := range items {
   485  		if c.used < quota {
   486  			break
   487  		}
   488  		remove(item.name)
   489  		// Remove the entry
   490  		delete(c.item, item.name)
   491  		c.used -= item.item.size
   492  	}
   493  }
   494  
   495  // clean empties the cache of stuff if it can
   496  func (c *Cache) clean() {
   497  	// Cache may be empty so end
   498  	_, err := os.Stat(c.root)
   499  	if os.IsNotExist(err) {
   500  		return
   501  	}
   502  
   503  	c.itemMu.Lock()
   504  	oldItems, oldUsed := len(c.item), fs.SizeSuffix(c.used)
   505  	c.itemMu.Unlock()
   506  
   507  	// first walk the FS to update the atimes and sizes
   508  	err = c.updateStats()
   509  	if err != nil {
   510  		fs.Errorf(nil, "Error traversing cache %q: %v", c.root, err)
   511  	}
   512  
   513  	// Remove any files that are over age
   514  	c.purgeOld(c.opt.CacheMaxAge)
   515  
   516  	// Now remove any files that are over quota starting from the
   517  	// oldest first
   518  	c.purgeOverQuota(int64(c.opt.CacheMaxSize))
   519  
   520  	// Remove any empty directories
   521  	c.purgeEmptyDirs()
   522  
   523  	// Stats
   524  	c.itemMu.Lock()
   525  	newItems, newUsed := len(c.item), fs.SizeSuffix(c.used)
   526  	c.itemMu.Unlock()
   527  
   528  	fs.Infof(nil, "Cleaned the cache: objects %d (was %d), total size %v (was %v)", newItems, oldItems, newUsed, oldUsed)
   529  }
   530  
   531  // cleaner calls clean at regular intervals
   532  //
   533  // doesn't return until context is cancelled
   534  func (c *Cache) cleaner(ctx context.Context) {
   535  	if c.opt.CachePollInterval <= 0 {
   536  		fs.Debugf(nil, "Cache cleaning thread disabled because poll interval <= 0")
   537  		return
   538  	}
   539  	// Start cleaning the cache immediately
   540  	c.clean()
   541  	// Then every interval specified
   542  	timer := time.NewTicker(c.opt.CachePollInterval)
   543  	defer timer.Stop()
   544  	for {
   545  		select {
   546  		case <-timer.C:
   547  			c.clean()
   548  		case <-ctx.Done():
   549  			fs.Debugf(nil, "cache cleaner exiting")
   550  			return
   551  		}
   552  	}
   553  }
   554  
   555  // copy an object to or from the remote while accounting for it
   556  func copyObj(f fs.Fs, dst fs.Object, remote string, src fs.Object) (newDst fs.Object, err error) {
   557  	if operations.NeedTransfer(context.TODO(), dst, src) {
   558  		newDst, err = operations.Copy(context.TODO(), f, dst, remote, src)
   559  	} else {
   560  		newDst = dst
   561  	}
   562  	return newDst, err
   563  }
   564  
   565  // Check the local file is up to date in the cache
   566  func (c *Cache) Check(ctx context.Context, o fs.Object, remote string) error {
   567  	cacheObj, err := c.fcache.NewObject(ctx, remote)
   568  	if err == nil && cacheObj != nil {
   569  		_, err = copyObj(c.fcache, cacheObj, remote, o)
   570  		if err != nil {
   571  			return errors.Wrap(err, "failed to update cached file")
   572  		}
   573  	}
   574  	return nil
   575  }
   576  
   577  // Fetch fetches the object to the cache file
   578  func (c *Cache) Fetch(ctx context.Context, o fs.Object, remote string) error {
   579  	_, err := copyObj(c.fcache, nil, remote, o)
   580  	return err
   581  }
   582  
   583  // Store stores the local cache file to the remote object, returning
   584  // the new remote object. objOld is the old object if known.
   585  func (c *Cache) Store(ctx context.Context, objOld fs.Object, remote string) (fs.Object, error) {
   586  	// Transfer the temp file to the remote
   587  	cacheObj, err := c.fcache.NewObject(ctx, remote)
   588  	if err != nil {
   589  		return nil, errors.Wrap(err, "failed to find cache file")
   590  	}
   591  
   592  	if objOld != nil {
   593  		remote = objOld.Remote() // use the path of the actual object if available
   594  	}
   595  	o, err := copyObj(c.fremote, objOld, remote, cacheObj)
   596  	if err != nil {
   597  		return nil, errors.Wrap(err, "failed to transfer file from cache to remote")
   598  	}
   599  	return o, nil
   600  }