diff --git a/docs/reference/docker_runx_cache_prune.yaml b/docs/reference/docker_runx_cache_prune.yaml index 4b527a5..cf92e13 100644 --- a/docs/reference/docker_runx_cache_prune.yaml +++ b/docs/reference/docker_runx_cache_prune.yaml @@ -1,10 +1,22 @@ command: docker runx cache prune -short: Remove all cache entries -long: Remove all cache entries +short: Remove cache entries not accessed recently +long: | + By default remove cache entries not accessed in the last 30 days. Use --all/-a to remove all cache entries. usage: docker runx cache prune pname: docker runx cache plink: docker_runx_cache.yaml options: + - option: all + shorthand: a + value_type: bool + default_value: "false" + description: Remove all cache entries + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: force shorthand: f value_type: bool diff --git a/docs/reference/runx_cache.md b/docs/reference/runx_cache.md index bca100c..49eadde 100644 --- a/docs/reference/runx_cache.md +++ b/docs/reference/runx_cache.md @@ -5,10 +5,10 @@ Manage Docker RunX cache and temporary files ### Subcommands -| Name | Description | -|:-------------------------------|:-------------------------| -| [`df`](runx_cache_df.md) | Show disk usage | -| [`prune`](runx_cache_prune.md) | Remove all cache entries | +| Name | Description | +|:-------------------------------|:-------------------------------------------| +| [`df`](runx_cache_df.md) | Show disk usage | +| [`prune`](runx_cache_prune.md) | Remove cache entries not accessed recently | diff --git a/docs/reference/runx_cache_prune.md b/docs/reference/runx_cache_prune.md index c1e94ff..c977827 100644 --- a/docs/reference/runx_cache_prune.md +++ b/docs/reference/runx_cache_prune.md @@ -1,12 +1,13 @@ # docker runx cache prune -Remove all cache entries +By default remove cache entries not accessed in the last 30 days. Use --all/-a to remove all cache entries. ### Options | Name | Type | Default | Description | |:----------------|:-------|:--------|:-------------------------------| +| `-a`, `--all` | `bool` | | Remove all cache entries | | `-f`, `--force` | `bool` | | Do not prompt for confirmation | diff --git a/internal/commands/cache/df.go b/internal/commands/cache/df.go index bec4aa3..310bd0d 100644 --- a/internal/commands/cache/df.go +++ b/internal/commands/cache/df.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter/tabwriter" "github.com/eunomie/docker-runx/runkit" ) @@ -31,9 +32,21 @@ func dfNewCmd(dockerCli command.Cli) *cobra.Command { str := strings.Builder{} str.WriteString("Cache directory: " + cacheDir + "\n") str.WriteString("\n") + + w := tabwriter.NewWriter(&str, 0, 0, 1, ' ', 0) + _, _ = fmt.Fprintln(w, "Digest\tSize\tLast Access") for _, e := range entries { - str.WriteString(fmt.Sprintf("%s: %s\n", e.Digest, humanize.Bytes(uint64(e.Size)))) + t := "--" + if e.LastAccess != nil { + t = e.LastAccess.Format("2006-01-02 15:04:05") + } + _, _ = fmt.Fprintf(w, + "%s\t%s\t%s\n", + e.Digest, + humanize.Bytes(uint64(e.Size)), + t) } + _ = w.Flush() str.WriteString(fmt.Sprintf("Total: %s\n", humanize.Bytes(uint64(totalSize)))) _, _ = fmt.Fprintln(dockerCli.Out(), str.String()) diff --git a/internal/commands/cache/prune.go b/internal/commands/cache/prune.go index 212e750..8ab9623 100644 --- a/internal/commands/cache/prune.go +++ b/internal/commands/cache/prune.go @@ -7,22 +7,28 @@ import ( "github.com/spf13/cobra" "github.com/docker/cli/cli/command" + "github.com/eunomie/docker-runx/internal/sugar" "github.com/eunomie/docker-runx/runkit" ) -var force bool +var ( + force bool + all bool +) func pruneNewCmd(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "prune", - Short: "Remove all cache entries", + Short: "Remove cache entries not accessed recently", + Long: "By default remove cache entries not accessed in the last 30 days. Use --all/-a to remove all cache entries.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + var err error cache := runkit.NewLocalCache(dockerCli) if !force { - err := huh.NewConfirm(). - Title("Are you sure you want to remove all cache entries?"). + err = huh.NewConfirm(). + Title(sugar.If(all, "Are you sure you want to remove all cache entries?", "Are you sure you want to remove cache entries not accessed in the last 30 days?")). Value(&force).Run() if err != nil { return err @@ -34,7 +40,11 @@ func pruneNewCmd(dockerCli command.Cli) *cobra.Command { return nil } - err := cache.Erase() + if !all { + err = cache.EraseNotAccessedInLast30Days() + } else { + err = cache.EraseAll() + } if err != nil { return err } @@ -46,6 +56,7 @@ func pruneNewCmd(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation") + flags.BoolVarP(&all, "all", "a", false, "Remove all cache entries") return cmd } diff --git a/runkit/cache.go b/runkit/cache.go index bea6fd7..a45d01a 100644 --- a/runkit/cache.go +++ b/runkit/cache.go @@ -5,6 +5,8 @@ import ( "io/fs" "os" "path/filepath" + "strings" + "time" "github.com/docker/cli/cli/command" "github.com/eunomie/docker-runx/internal/constants" @@ -13,6 +15,7 @@ import ( const ( runxConfigFile = "runx.yaml" runxDocFile = "README.md" + accessFile = "access" ) var subCacheDir = filepath.Join(constants.SubCommandName, "cache", "sha256") @@ -23,8 +26,9 @@ type ( } CacheEntry struct { - Digest string - Size int64 + LastAccess *time.Time + Digest string + Size int64 } ) @@ -66,12 +70,21 @@ func (c *LocalCache) Get(digest, src string) (*RunKit, error) { } if found { + if err := c.writeAccessFile(digest); err != nil { + return nil, err + } + rk.src = src return rk, nil } return nil, nil } +func (c *LocalCache) writeAccessFile(digest string) error { + accessDate := time.Now().Format(time.RFC3339) + return os.WriteFile(filepath.Join(c.cacheDir, digest, accessFile), []byte(accessDate), 0o644) +} + func (c *LocalCache) Set(digest string, runxConfig, runxDoc []byte) error { digestDir := filepath.Join(c.cacheDir, digest) if err := os.MkdirAll(digestDir, 0o755); err != nil { @@ -89,6 +102,9 @@ func (c *LocalCache) Set(digest string, runxConfig, runxDoc []byte) error { return err } } + if err := c.writeAccessFile(digest); err != nil { + return err + } return nil } @@ -105,9 +121,11 @@ func (c *LocalCache) ListCache() (string, []CacheEntry, int64, error) { return e } totalSize += s + t := c.lastAccess(path) entries = append(entries, CacheEntry{ - Digest: filepath.Base(path), - Size: s, + Digest: filepath.Base(path), + Size: s, + LastAccess: t, }) return fs.SkipDir } @@ -122,10 +140,37 @@ func (c *LocalCache) ListCache() (string, []CacheEntry, int64, error) { return c.cacheDir, entries, totalSize, nil } -func (c *LocalCache) Erase() error { +func (c *LocalCache) EraseAll() error { return os.RemoveAll(c.cacheDir) } +func (c *LocalCache) EraseNotAccessedInLast30Days() error { + _, entries, _, err := c.ListCache() + if err != nil { + return err + } + for _, e := range entries { + if e.LastAccess != nil && time.Since(*e.LastAccess) > 30*24*time.Hour { + if err := os.RemoveAll(filepath.Join(c.cacheDir, e.Digest)); err != nil { + return err + } + } + } + return nil +} + +func (c *LocalCache) lastAccess(path string) *time.Time { + b, err := os.ReadFile(filepath.Join(path, accessFile)) + if err != nil { + return nil + } + if t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(b))); err != nil { + return nil + } else { + return &t + } +} + func dirSize(path string) (int64, error) { var size int64 err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {