Skip to content

Commit dbc8775

Browse files
authored
Merge pull request #1 from umlx5h/prune
Add prune subcommand
2 parents 460a863 + d234410 commit dbc8775

File tree

11 files changed

+452
-51
lines changed

11 files changed

+452
-51
lines changed

README.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ go install github.com/umlx5h/gtrash@latest
8787
### Build from source
8888

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

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

363-
Consider setting up alternative aliases, such as:
363+
Consider setting up alternative short aliases, such as:
364364

365365
```bash
366366
alias gp='gtrash put' # gtrash put
367367
alias gm='gtrash put' # gtrash move (easy to change to rm)
368368
alias tp='gtrash put' # trash put
369369
alias tm='gtrash put' # trash move (easy to change to rm)
370+
alias tt='gtrash put' # to trash
370371
```
371372

372373
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.
630631

631632
```bash
632633
# Remove files deleted over a week ago
633-
$ gtrash find --day-old 7 --rm
634+
$ gtrash prune --day 7
634635

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

639640
Size-based:
641+
642+
There are two methods.
643+
644+
`find` filters by the specified size and removes them.
645+
640646
```bash
641647
# Remove trashed files larger than 10MB
642648
$ gtrash find --size-large 10mb --rm
@@ -651,8 +657,16 @@ $ gtrash find --size-large 1gb --rm
651657
$ gtrash find --size-small 0 --rm
652658
```
653659

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

669+
Sizes and dates can be combined in `find`, and other filters can be applied:
656670
```bash
657671
# Remove files older than a week and larger than 10MB
658672
$ gtrash find --day-old 7 --size-large 10mb --rm

internal/cmd/find.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ type findOptions struct {
4545
showTrashPath bool
4646

4747
restoreTo string
48+
49+
trashDir string
4850
}
4951

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

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

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

205223
} else if opts.doRestore {
206224
if opts.restoreTo != "" {

internal/cmd/metafix.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,16 @@ func metafixCmdRun(opts metafixOptions) error {
5757
trash.WithSortBy(trash.SortByName),
5858
)
5959
if err := box.Open(); err != nil {
60-
return err
60+
if errors.Is(err, trash.ErrNotFound) {
61+
fmt.Printf("do nothing: %s\n", err)
62+
return nil
63+
} else {
64+
return err
65+
}
6166
}
6267

6368
if len(box.OrphanMeta) == 0 {
64-
fmt.Println("Not found invalid metadata")
69+
fmt.Println("not found invalid metadata")
6570
return nil
6671
}
6772

internal/cmd/prune.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/dustin/go-humanize"
8+
"github.com/spf13/cobra"
9+
"github.com/umlx5h/gtrash/internal/glog"
10+
"github.com/umlx5h/gtrash/internal/trash"
11+
"github.com/umlx5h/gtrash/internal/tui"
12+
)
13+
14+
type pruneCmd struct {
15+
cmd *cobra.Command
16+
opts pruneOptions
17+
}
18+
19+
type pruneOptions struct {
20+
force bool
21+
22+
day int
23+
size string // human size (e.g. 10MB, 1G)
24+
25+
maxTotalSize uint64 // byte, parse from size
26+
27+
trashDir string // $HOME/.local/share/Trash
28+
}
29+
30+
func (o *pruneOptions) check() error {
31+
if o.size != "" {
32+
byte, err := humanize.ParseBytes(o.size)
33+
if err != nil {
34+
return fmt.Errorf("--size unit is invalid: %w", err)
35+
}
36+
o.maxTotalSize = byte
37+
}
38+
return nil
39+
}
40+
41+
func newPruneCmd() *pruneCmd {
42+
root := &pruneCmd{}
43+
cmd := &cobra.Command{
44+
Use: "prune",
45+
Short: "Prune trash cans by day or size",
46+
Long: `Description:
47+
Pruning trash cans by day or size criteria.
48+
Either the --day or --size option is required.
49+
50+
This command is also intended for use via cron.
51+
By default, you may be prompted multiple times for each trash can.
52+
53+
If the file to be pruned does not exist, the program exits normally without doing anything.`,
54+
Example: ` # Delete all files deleted a week ago
55+
$ gtrash prune --day 7
56+
57+
# Delete all files deleted a week ago only within $HOME trash
58+
$ gtrash prune --day 7 --trash-dir "$HOME/.local/share/Trash"
59+
60+
# Delete files in order from the largest to the smaller one so that the total size of the trash can is less than 5GB.
61+
# 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.
62+
$ gtrash prune --size 5GB
63+
64+
# Delete large files first to keep the total remaining size under 5GB, while excluding files deleted in the last week.
65+
# Note that adding the most recently deleted files may exceed 5GB.
66+
$ gtrash prune --size 5GB --day 7`,
67+
SilenceUsage: true,
68+
Args: cobra.NoArgs,
69+
ValidArgsFunction: cobra.NoFileCompletions,
70+
RunE: func(_ *cobra.Command, _ []string) error {
71+
if err := pruneCmdRun(root.opts); err != nil {
72+
return err
73+
}
74+
if glog.ExitCode() > 0 {
75+
return errContinue
76+
}
77+
return nil
78+
},
79+
}
80+
81+
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.
82+
If the total size of the trash can is smaller than the specified size, nothing is done.
83+
The total size is calculated by each trash can.
84+
85+
If you want to delete files larger than the specified size, use the "find --size-large XX --rm" command.
86+
87+
Can be specified in human format (e.g. 5MB, 1GB)
88+
89+
If --day and --size are specified at the same time, the most recent X days are excluded from the calculation.
90+
This may be useful when you do not want to delete large files that have been recently deleted.
91+
`)
92+
cmd.Flags().IntVar(&root.opts.day, "day", 0, "Remove all files deleted before X days")
93+
94+
cmd.Flags().BoolVarP(&root.opts.force, "force", "f", false, `Always execute without confirmation prompt
95+
This is not necessary if running outside of a terminal
96+
`)
97+
cmd.Flags().StringVar(&root.opts.trashDir, "trash-dir", "", `Specify a full path if you want to prune only a specific trash can
98+
By default, all trash cans are pruned.
99+
100+
For $HOME trash only:
101+
--trash-dir "$HOME/.local/share/Trash"
102+
`)
103+
cmd.Root().MarkFlagsOneRequired("size", "day")
104+
105+
root.cmd = cmd
106+
return root
107+
}
108+
109+
// Returns files to be deleted from files based on maxTotalSize
110+
// If maxTotalSize > total, nil is returned.
111+
//
112+
// Prerequisite: files are sorted in ascending order by size
113+
func getPruneFiles(files []trash.File, maxTotalSize uint64) (prune []trash.File, deleted uint64, total uint64) {
114+
for i, f := range files {
115+
// If the size cannot be obtained, it is treated as a minus value and should be at the top.
116+
// This is always skipped and is not considered for deletion.
117+
if f.Size == nil {
118+
continue
119+
}
120+
121+
size := uint64(*f.Size)
122+
total += size
123+
124+
if prune == nil {
125+
if total > maxTotalSize {
126+
prune = files[i:]
127+
}
128+
}
129+
130+
if prune != nil {
131+
deleted += size
132+
}
133+
}
134+
135+
if prune == nil {
136+
return nil, 0, total
137+
} else {
138+
return prune, deleted, total
139+
}
140+
}
141+
142+
func pruneCmdRun(opts pruneOptions) error {
143+
if err := opts.check(); err != nil {
144+
return err
145+
}
146+
147+
sortMethod := trash.SortByDeletedAt
148+
149+
sizeMode := opts.size != ""
150+
151+
if opts.size != "" {
152+
sortMethod = trash.SortBySize
153+
}
154+
155+
box := trash.NewBox(
156+
trash.WithSortBy(sortMethod),
157+
trash.WithGetSize(sizeMode),
158+
trash.WithAscend(true),
159+
trash.WithDay(0, opts.day),
160+
trash.WithTrashDir(opts.trashDir),
161+
)
162+
if err := box.Open(); err != nil {
163+
if errors.Is(err, trash.ErrNotFound) {
164+
fmt.Printf("do nothing: %s\n", err)
165+
return nil
166+
} else {
167+
return err
168+
}
169+
}
170+
171+
for i, trashDir := range box.TrashDirs {
172+
files := box.FilesByTrashDir[trashDir]
173+
if len(files) == 0 {
174+
continue
175+
}
176+
177+
var deleted, total uint64
178+
179+
if sizeMode {
180+
files, deleted, total = getPruneFiles(files, opts.maxTotalSize)
181+
if len(files) == 0 {
182+
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)
183+
continue
184+
}
185+
}
186+
187+
listFiles(files, sizeMode, false)
188+
189+
fmt.Printf("\nSelected %d files in %s\n", len(files), trashDir)
190+
191+
if sizeMode {
192+
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))
193+
}
194+
195+
if !opts.force && isTerminal && !tui.BoolPrompt("Are you sure you want to remove PERMENANTLY? ") {
196+
return errors.New("do nothing")
197+
}
198+
doRemove(files)
199+
200+
if i != len(box.TrashDirs)-1 {
201+
fmt.Println("")
202+
}
203+
}
204+
205+
return nil
206+
}

0 commit comments

Comments
 (0)