diff --git a/changelog/unreleased/issue-1721 b/changelog/unreleased/issue-1721 new file mode 100644 index 000000000..bf168369c --- /dev/null +++ b/changelog/unreleased/issue-1721 @@ -0,0 +1,8 @@ +Enhancement: Add `cache` command to list cache dirs + +The command `cache` was added, it allows listing restic's cache directoriers +together with the last usage. It also allows removing old cache dirs without +having to access a repo, via `restic cache --cleanup` + +https://github.com/restic/restic/issues/1721 +https://github.com/restic/restic/pull/1749 diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go new file mode 100644 index 000000000..1f19a40f2 --- /dev/null +++ b/cmd/restic/cmd_cache.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "path/filepath" + "sort" + "time" + + "github.com/restic/restic/internal/cache" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" + "github.com/spf13/cobra" +) + +var cmdCache = &cobra.Command{ + Use: "cache", + Short: "Operate on local cache directories", + Long: ` +The "cache" command allows listing and cleaning local cache directories. +`, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runCache(cacheOptions, globalOptions, args) + }, +} + +// CacheOptions bundles all options for the snapshots command. +type CacheOptions struct { + Cleanup bool + MaxAge uint +} + +var cacheOptions CacheOptions + +func init() { + cmdRoot.AddCommand(cmdCache) + + f := cmdCache.Flags() + f.BoolVar(&cacheOptions.Cleanup, "cleanup", false, "remove old cache directories") + f.UintVar(&cacheOptions.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old") +} + +func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { + if len(args) > 0 { + return errors.Fatal("the cache command has no arguments") + } + + if gopts.NoCache { + return errors.Fatal("Refusing to do anything, the cache is disabled") + } + + var ( + cachedir = gopts.CacheDir + err error + ) + + if cachedir == "" { + cachedir, err = cache.DefaultDir() + if err != nil { + return err + } + } + + if opts.Cleanup || gopts.CleanupCache { + oldDirs, err := cache.OlderThan(cachedir, time.Duration(opts.MaxAge)*24*time.Hour) + if err != nil { + return err + } + + if len(oldDirs) == 0 { + Verbosef("no old cache dirs found\n") + return nil + } + + Verbosef("remove %d old cache directories\n", len(oldDirs)) + + for _, item := range oldDirs { + dir := filepath.Join(cachedir, item.Name()) + err = fs.RemoveAll(dir) + if err != nil { + Warnf("unable to remove %v: %v\n", dir, err) + } + } + + return nil + } + + tab := NewTable() + tab.Header = fmt.Sprintf("%-14s %-16s %s", "Repository ID", "Last Used", "Old") + tab.RowFormat = "%-14s %-16s %s" + + dirs, err := cache.All(cachedir) + if err != nil { + return err + } + + if len(dirs) == 0 { + Printf("no cache dirs found, basedir is %v\n", cachedir) + return nil + } + + sort.Slice(dirs, func(i, j int) bool { + return dirs[i].ModTime().Before(dirs[j].ModTime()) + }) + + for _, entry := range dirs { + var old string + if cache.IsOld(entry.ModTime(), time.Duration(opts.MaxAge)*24*time.Hour) { + old = "yes" + } + + tab.Rows = append(tab.Rows, []interface{}{ + entry.Name()[:10], + fmt.Sprintf("%d days ago", uint(time.Since(entry.ModTime()).Hours()/24)), + old, + }) + } + + tab.Write(gopts.stdout) + + return nil +} diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 6fe026bc9..ba328bdaf 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -390,7 +390,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) { Printf("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base) for _, item := range oldCacheDirs { - dir := filepath.Join(c.Base, item) + dir := filepath.Join(c.Base, item.Name()) err = fs.RemoveAll(dir) if err != nil { Warnf("unable to remove %v: %v\n", dir, err) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index aacb8db65..33a507b4a 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "strconv" "time" @@ -156,14 +157,25 @@ func updateTimestamp(d string) error { return fs.Chtimes(d, t, t) } -const maxCacheAge = 30 * 24 * time.Hour +// MaxCacheAge is the default age (30 days) after which cache directories are considered old. +const MaxCacheAge = 30 * 24 * time.Hour -// Old returns a list of cache directories with a modification time of more -// than 30 days ago. -func Old(basedir string) ([]string, error) { - var oldCacheDirs []string +func validCacheDirName(s string) bool { + r := regexp.MustCompile(`^[a-fA-F0-9]{64}$`) + if !r.MatchString(s) { + return false + } + return true +} + +// listCacheDirs returns the list of cache directories. +func listCacheDirs(basedir string) ([]os.FileInfo, error) { f, err := fs.Open(basedir) + if err != nil && os.IsNotExist(errors.Cause(err)) { + return nil, nil + } + if err != nil { return nil, err } @@ -173,29 +185,65 @@ func Old(basedir string) ([]string, error) { return nil, err } - oldest := time.Now().Add(-maxCacheAge) - for _, fi := range entries { - if !fi.IsDir() { - continue - } - - if !fi.ModTime().Before(oldest) { - continue - } - - oldCacheDirs = append(oldCacheDirs, fi.Name()) - } - err = f.Close() if err != nil { return nil, err } + result := make([]os.FileInfo, 0, len(entries)) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + if !validCacheDirName(entry.Name()) { + continue + } + + result = append(result, entry) + } + + return result, nil +} + +// All returns a list of cache directories. +func All(basedir string) (dirs []os.FileInfo, err error) { + return listCacheDirs(basedir) +} + +// OlderThan returns the list of cache directories older than max. +func OlderThan(basedir string, max time.Duration) ([]os.FileInfo, error) { + entries, err := listCacheDirs(basedir) + if err != nil { + return nil, err + } + + var oldCacheDirs []os.FileInfo + for _, fi := range entries { + if !IsOld(fi.ModTime(), max) { + continue + } + + oldCacheDirs = append(oldCacheDirs, fi) + } + debug.Log("%d old cache dirs found", len(oldCacheDirs)) return oldCacheDirs, nil } +// Old returns a list of cache directories with a modification time of more +// than 30 days ago. +func Old(basedir string) ([]os.FileInfo, error) { + return OlderThan(basedir, MaxCacheAge) +} + +// IsOld returns true if the timestamp is considered old. +func IsOld(t time.Time, maxAge time.Duration) bool { + oldest := time.Now().Add(-maxAge) + return t.Before(oldest) +} + // errNoSuchFile is returned when a file is not cached. type errNoSuchFile struct { Type string