|
| 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