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