Skip to content

Commit

Permalink
Merge pull request #1 from umlx5h/prune
Browse files Browse the repository at this point in the history
Add prune subcommand
umlx5h authored Jan 12, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 460a863 + d234410 commit dbc8775
Showing 11 changed files with 452 additions and 51 deletions.
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -87,7 +87,7 @@ go install github.com/umlx5h/gtrash@latest
### Build from source

```bash
git clone https://github.com/umlx5h/gtrash.git
git clone https://github.com/umlx5h/gtrash.git --depth 1
cd gtrash
go build
./gtrash
@@ -360,13 +360,14 @@ Not recommended due to potential risks, unintentionally executing actual `rm` co

As `gtrash` isn't fully compatible with `rm`, it's prudent to establish different aliases to avoid confusion and prevent accidental deletion of files.

Consider setting up alternative aliases, such as:
Consider setting up alternative short aliases, such as:

```bash
alias gp='gtrash put' # gtrash put
alias gm='gtrash put' # gtrash move (easy to change to rm)
alias tp='gtrash put' # trash put
alias tm='gtrash put' # trash move (easy to change to rm)
alias tt='gtrash put' # to trash
```

If you are in the habit of using rm, consider creating an alias that displays a cautionary message.
@@ -630,13 +631,18 @@ Currently possible only by day.

```bash
# Remove files deleted over a week ago
$ gtrash find --day-old 7 --rm
$ gtrash prune --day 7

# Remove files deleted within the last 24 hours
$ gtrash find --day-new 1 --rm
# Almost the same as prune
$ gtrash find --day-old 7 --rm
```

Size-based:

There are two methods.

`find` filters by the specified size and removes them.

```bash
# Remove trashed files larger than 10MB
$ gtrash find --size-large 10mb --rm
@@ -651,8 +657,16 @@ $ gtrash find --size-large 1gb --rm
$ gtrash find --size-small 0 --rm
```

Sizes and dates can be combined, and other filters can be applied.
`prune` removes large files first so that the overall trash size is smaller than the specified size:
```
# After this, the size of the trash can is guaranteed to be less than 5 GB.
$ gtrash prune --size 5GB
# If you want to exclude recently deleted files, you can also specify day.
$ gtrash prune --size 5GB --day 7
```

Sizes and dates can be combined in `find`, and other filters can be applied:
```bash
# Remove files older than a week and larger than 10MB
$ gtrash find --day-old 7 --size-large 10mb --rm
28 changes: 23 additions & 5 deletions internal/cmd/find.go
Original file line number Diff line number Diff line change
@@ -45,6 +45,8 @@ type findOptions struct {
showTrashPath bool

restoreTo string

trashDir string
}

func newFindCmd() *findCmd {
@@ -135,11 +137,22 @@ This is not necessary if running outside of a terminal`)
cmd.Flags().IntVar(&root.opts.dayOld, "day-old", 0, "Filter by deletion date (before X day)")
cmd.Flags().BoolVarP(&root.opts.showSize, "show-size", "S", false, `Show size always
Automatically enabled if --sort size, --size-large, --size-small specified
If the size could not be obtained, it will be displayed as '-'`)
If the size could not be obtained, it will be displayed as '-'
Note that this may take longer due to recursive size calcuration for directories.
The folder size is cached, so it will run faster the next time.
`)
cmd.Flags().BoolVar(&root.opts.showTrashPath, "show-trashpath", false, "Show trash path")
cmd.Flags().BoolVarP(&root.opts.reverse, "reverse", "r", false, "Reverse sort order (default: ascending)")
cmd.Flags().StringVar(&root.opts.restoreTo, "restore-to", "", "Restore to this path instead of original path")
cmd.Flags().IntVarP(&root.opts.last, "last", "n", 0, "Show n last files")
cmd.Flags().StringVar(&root.opts.trashDir, "trash-dir", "", `Specify a full path if you want to search only a specific trash can
By default, all trash cans are searched.
For $HOME trash only:
--trash-dir "$HOME/.local/share/Trash"
`)

cmd.MarkFlagsMutuallyExclusive("rm", "restore")
cmd.MarkFlagsMutuallyExclusive("directory", "cwd")
@@ -175,9 +188,16 @@ func findCmdRun(args []string, opts findOptions) error {
trash.WithDay(opts.dayNew, opts.dayOld), // TODO: also set in restore?
trash.WithSize(opts.sizeLarge, opts.sizeSmall),
trash.WithLimitLast(opts.last),
trash.WithTrashDir(opts.trashDir),
)
if err := box.Open(); err != nil {
return err
// no error only remove mode (consider executing via batch)
if opts.doRemove && errors.Is(err, trash.ErrNotFound) {
fmt.Printf("do nothing: %s\n", err)
return nil
} else {
return err
}
}

listFiles(box.Files, box.GetSize, opts.showTrashPath)
@@ -198,9 +218,7 @@ func findCmdRun(args []string, opts findOptions) error {
if !opts.force && isTerminal && !tui.BoolPrompt("Are you sure you want to remove PERMENANTLY? ") {
return errors.New("do nothing")
}
if err := doRemove(box.Files); err != nil {
return err
}
doRemove(box.Files)

} else if opts.doRestore {
if opts.restoreTo != "" {
9 changes: 7 additions & 2 deletions internal/cmd/metafix.go
Original file line number Diff line number Diff line change
@@ -57,11 +57,16 @@ func metafixCmdRun(opts metafixOptions) error {
trash.WithSortBy(trash.SortByName),
)
if err := box.Open(); err != nil {
return err
if errors.Is(err, trash.ErrNotFound) {
fmt.Printf("do nothing: %s\n", err)
return nil
} else {
return err
}
}

if len(box.OrphanMeta) == 0 {
fmt.Println("Not found invalid metadata")
fmt.Println("not found invalid metadata")
return nil
}

206 changes: 206 additions & 0 deletions internal/cmd/prune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package cmd

import (
"errors"
"fmt"

"github.com/dustin/go-humanize"
"github.com/spf13/cobra"
"github.com/umlx5h/gtrash/internal/glog"
"github.com/umlx5h/gtrash/internal/trash"
"github.com/umlx5h/gtrash/internal/tui"
)

type pruneCmd struct {
cmd *cobra.Command
opts pruneOptions
}

type pruneOptions struct {
force bool

day int
size string // human size (e.g. 10MB, 1G)

maxTotalSize uint64 // byte, parse from size

trashDir string // $HOME/.local/share/Trash
}

func (o *pruneOptions) check() error {
if o.size != "" {
byte, err := humanize.ParseBytes(o.size)
if err != nil {
return fmt.Errorf("--size unit is invalid: %w", err)
}
o.maxTotalSize = byte
}
return nil
}

func newPruneCmd() *pruneCmd {
root := &pruneCmd{}
cmd := &cobra.Command{
Use: "prune",
Short: "Prune trash cans by day or size",
Long: `Description:
Pruning trash cans by day or size criteria.
Either the --day or --size option is required.
This command is also intended for use via cron.
By default, you may be prompted multiple times for each trash can.
If the file to be pruned does not exist, the program exits normally without doing anything.`,
Example: ` # Delete all files deleted a week ago
$ gtrash prune --day 7
# Delete all files deleted a week ago only within $HOME trash
$ gtrash prune --day 7 --trash-dir "$HOME/.local/share/Trash"
# Delete files in order from the largest to the smaller one so that the total size of the trash can is less than 5GB.
# This is useful when you want to keep as many files as possible, including old files, but want to reduce the size of the trash can below a certain level.
$ gtrash prune --size 5GB
# Delete large files first to keep the total remaining size under 5GB, while excluding files deleted in the last week.
# Note that adding the most recently deleted files may exceed 5GB.
$ gtrash prune --size 5GB --day 7`,
SilenceUsage: true,
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(_ *cobra.Command, _ []string) error {
if err := pruneCmdRun(root.opts); err != nil {
return err
}
if glog.ExitCode() > 0 {
return errContinue
}
return nil
},
}

cmd.Flags().StringVar(&root.opts.size, "size", "", `Remove files in order from the largest to the smaller one so that the overall size of the trash can is less than the specified size.
If the total size of the trash can is smaller than the specified size, nothing is done.
The total size is calculated by each trash can.
If you want to delete files larger than the specified size, use the "find --size-large XX --rm" command.
Can be specified in human format (e.g. 5MB, 1GB)
If --day and --size are specified at the same time, the most recent X days are excluded from the calculation.
This may be useful when you do not want to delete large files that have been recently deleted.
`)
cmd.Flags().IntVar(&root.opts.day, "day", 0, "Remove all files deleted before X days")

cmd.Flags().BoolVarP(&root.opts.force, "force", "f", false, `Always execute without confirmation prompt
This is not necessary if running outside of a terminal
`)
cmd.Flags().StringVar(&root.opts.trashDir, "trash-dir", "", `Specify a full path if you want to prune only a specific trash can
By default, all trash cans are pruned.
For $HOME trash only:
--trash-dir "$HOME/.local/share/Trash"
`)
cmd.Root().MarkFlagsOneRequired("size", "day")

root.cmd = cmd
return root
}

// Returns files to be deleted from files based on maxTotalSize
// If maxTotalSize > total, nil is returned.
//
// Prerequisite: files are sorted in ascending order by size
func getPruneFiles(files []trash.File, maxTotalSize uint64) (prune []trash.File, deleted uint64, total uint64) {
for i, f := range files {
// If the size cannot be obtained, it is treated as a minus value and should be at the top.
// This is always skipped and is not considered for deletion.
if f.Size == nil {
continue
}

size := uint64(*f.Size)
total += size

if prune == nil {
if total > maxTotalSize {
prune = files[i:]
}
}

if prune != nil {
deleted += size
}
}

if prune == nil {
return nil, 0, total
} else {
return prune, deleted, total
}
}

func pruneCmdRun(opts pruneOptions) error {
if err := opts.check(); err != nil {
return err
}

sortMethod := trash.SortByDeletedAt

sizeMode := opts.size != ""

if opts.size != "" {
sortMethod = trash.SortBySize
}

box := trash.NewBox(
trash.WithSortBy(sortMethod),
trash.WithGetSize(sizeMode),
trash.WithAscend(true),
trash.WithDay(0, opts.day),
trash.WithTrashDir(opts.trashDir),
)
if err := box.Open(); err != nil {
if errors.Is(err, trash.ErrNotFound) {
fmt.Printf("do nothing: %s\n", err)
return nil
} else {
return err
}
}

for i, trashDir := range box.TrashDirs {
files := box.FilesByTrashDir[trashDir]
if len(files) == 0 {
continue
}

var deleted, total uint64

if sizeMode {
files, deleted, total = getPruneFiles(files, opts.maxTotalSize)
if len(files) == 0 {
fmt.Printf("do nothing: trash size %s is smaller than %s (%s) in %s\n", humanize.Bytes(total), humanize.Bytes(opts.maxTotalSize), opts.size, trashDir)
continue
}
}

listFiles(files, sizeMode, false)

fmt.Printf("\nSelected %d files in %s\n", len(files), trashDir)

if sizeMode {
fmt.Printf("Current: %s, Deleted: %s, After: %s, Specified: %s\n\n", humanize.Bytes(total), humanize.Bytes(deleted), humanize.Bytes(total-deleted), humanize.Bytes(opts.maxTotalSize))
}

if !opts.force && isTerminal && !tui.BoolPrompt("Are you sure you want to remove PERMENANTLY? ") {
return errors.New("do nothing")
}
doRemove(files)

if i != len(box.TrashDirs)-1 {
fmt.Println("")
}
}

return nil
}
Loading

0 comments on commit dbc8775

Please sign in to comment.