feat(cli): implement purge command for data cleanup
Add comprehensive purge command with selective cleaning options: - Database purging (history, shortcuts, zoom levels) - Cache purging (WebKit cache, dmenu fuzzy cache) - WebKit data purging (cookies, local storage) - State and config purging - Confirmation prompt with --force override - Detailed help and usage examples
- commit
- d4c1f57c803daac45852782d66e09ef375338459
- author
- Brice · · 2025-09-10 20:12 UTC
- parent
- 884905a
- signature
- unsigned
4 files · +273 -0
M CHANGELOG.md +4 -0
| @@ -16,6 +16,10 @@ All notable changes to this project will be documented in this file. | |||
| 16 | 16 | - XDG Base Directory Specification compliance (cache stored in state directory) | |
| 17 | 17 | - Atomic operations for concurrent access control | |
| 18 | 18 | - Dependency injection using interfaces and gomock for testing | |
| 19 | + | - Purge command for cleaning up dumber data and cache files: | |
| 20 | + | - Selective purging with flags: `--database`, `--cache`, `--webkit-data`, `--state`, `--config` | |
| 21 | + | - Confirmation prompt with `--force` flag to skip | |
| 22 | + | - Targets all data locations: SQLite database, WebKit cache/data, dmenu fuzzy cache, configuration files | |
| 19 | 23 | ||
| 20 | 24 | ### Changed | |
| 21 | 25 | - Dmenu performance dramatically improved: 5 seconds → 4ms (99.92% faster) | |
| @@ -16,6 +16,10 @@ All notable changes to this project will be documented in this file. | |||||
| 16 | - XDG Base Directory Specification compliance (cache stored in state directory) | 16 | - XDG Base Directory Specification compliance (cache stored in state directory) | ||
| 17 | - Atomic operations for concurrent access control | 17 | - Atomic operations for concurrent access control | ||
| 18 | - Dependency injection using interfaces and gomock for testing | 18 | - Dependency injection using interfaces and gomock for testing | ||
| 19 | + | - Purge command for cleaning up dumber data and cache files: | |||
| 20 | + | - Selective purging with flags: `--database`, `--cache`, `--webkit-data`, `--state`, `--config` | |||
| 21 | + | - Confirmation prompt with `--force` flag to skip | |||
| 22 | + | - Targets all data locations: SQLite database, WebKit cache/data, dmenu fuzzy cache, configuration files | |||
| 19 | 23 | ||||
| 20 | ### Changed | 24 | ### Changed | ||
| 21 | - Dmenu performance dramatically improved: 5 seconds → 4ms (99.92% faster) | 25 | - Dmenu performance dramatically improved: 5 seconds → 4ms (99.92% faster) | ||
M README.md +5 -0
| @@ -103,6 +103,11 @@ Development: | |||
| 103 | 103 | - Launcher integration (dmenu‑style examples): | |
| 104 | 104 | - rofi: `dumber dmenu | rofi -dmenu -p "Browse: " | dumber dmenu --select` | |
| 105 | 105 | - fuzzel: `dumber dmenu | fuzzel --dmenu -p "Browse: " | dumber dmenu --select` | |
| 106 | + | - Clean up data and cache: | |
| 107 | + | - `dumber purge` # purge all data (with confirmation) | |
| 108 | + | - `dumber purge --force` # purge all data (no confirmation) | |
| 109 | + | - `dumber purge --database --cache` # purge only database and caches | |
| 110 | + | - `dumber purge --webkit-data` # purge only WebKit data (cookies, etc.) | |
| 106 | 111 | ||
| 107 | 112 | ### Dmenu mode invocation | |
| 108 | 113 | You can invoke dmenu mode in two ways: | |
| @@ -103,6 +103,11 @@ Development: | |||||
| 103 | - Launcher integration (dmenu‑style examples): | 103 | - Launcher integration (dmenu‑style examples): | ||
| 104 | - rofi: `dumber dmenu | rofi -dmenu -p "Browse: " | dumber dmenu --select` | 104 | - rofi: `dumber dmenu | rofi -dmenu -p "Browse: " | dumber dmenu --select` | ||
| 105 | - fuzzel: `dumber dmenu | fuzzel --dmenu -p "Browse: " | dumber dmenu --select` | 105 | - fuzzel: `dumber dmenu | fuzzel --dmenu -p "Browse: " | dumber dmenu --select` | ||
| 106 | + | - Clean up data and cache: | |||
| 107 | + | - `dumber purge` # purge all data (with confirmation) | |||
| 108 | + | - `dumber purge --force` # purge all data (no confirmation) | |||
| 109 | + | - `dumber purge --database --cache` # purge only database and caches | |||
| 110 | + | - `dumber purge --webkit-data` # purge only WebKit data (cookies, etc.) | |||
| 106 | 111 | ||||
| 107 | ### Dmenu mode invocation | 112 | ### Dmenu mode invocation | ||
| 108 | You can invoke dmenu mode in two ways: | 113 | You can invoke dmenu mode in two ways: | ||
A internal/cli/purge.go +263 -0
| @@ -0,0 +1,263 @@ | |||
| 1 | + | package cli | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bufio" | |
| 5 | + | "fmt" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "strings" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/dumber/internal/config" | |
| 11 | + | "github.com/spf13/cobra" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // PurgeFlags holds all the purge command flags | |
| 15 | + | type PurgeFlags struct { | |
| 16 | + | Database bool | |
| 17 | + | Cache bool | |
| 18 | + | State bool | |
| 19 | + | WebkitData bool | |
| 20 | + | Config bool | |
| 21 | + | All bool | |
| 22 | + | Force bool | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // NewPurgeCmd creates the purge command | |
| 26 | + | func NewPurgeCmd() *cobra.Command { | |
| 27 | + | var flags PurgeFlags | |
| 28 | + | ||
| 29 | + | cmd := &cobra.Command{ | |
| 30 | + | Use: "purge", | |
| 31 | + | Short: "Purge dumber data and cache files", | |
| 32 | + | Long: `Purge various dumber data and cache files. By default, purges everything. | |
| 33 | + | ||
| 34 | + | Available purge targets: | |
| 35 | + | --database, -d Purge the SQLite database (history, shortcuts, zoom levels) | |
| 36 | + | --cache, -c Purge WebKit cache and dmenu fuzzy search cache | |
| 37 | + | --state, -s Purge all state data (includes database and caches) | |
| 38 | + | --webkit-data, -w Purge WebKit data (cookies, local storage, session data) | |
| 39 | + | --config Purge configuration files | |
| 40 | + | --all, -a Purge everything (default if no specific flags are provided) | |
| 41 | + | ||
| 42 | + | Use --force to skip the confirmation prompt. | |
| 43 | + | ||
| 44 | + | Examples: | |
| 45 | + | dumber purge # Purge everything (with confirmation) | |
| 46 | + | dumber purge --force # Purge everything (no confirmation) | |
| 47 | + | dumber purge -d -c # Purge only database and caches | |
| 48 | + | dumber purge --webkit-data -f # Force purge WebKit data only`, | |
| 49 | + | RunE: func(_ *cobra.Command, _ []string) error { | |
| 50 | + | // Initialize CLI to get config paths | |
| 51 | + | cli, err := NewCLI() | |
| 52 | + | if err != nil { | |
| 53 | + | return fmt.Errorf("failed to initialize CLI: %w", err) | |
| 54 | + | } | |
| 55 | + | defer func() { | |
| 56 | + | if closeErr := cli.Close(); closeErr != nil { | |
| 57 | + | fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr) | |
| 58 | + | } | |
| 59 | + | }() | |
| 60 | + | ||
| 61 | + | return executePurge(flags) | |
| 62 | + | }, | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // Add flags | |
| 66 | + | cmd.Flags().BoolVarP(&flags.Database, "database", "d", false, "Purge the SQLite database") | |
| 67 | + | cmd.Flags().BoolVarP(&flags.Cache, "cache", "c", false, "Purge WebKit cache and dmenu fuzzy cache") | |
| 68 | + | cmd.Flags().BoolVarP(&flags.State, "state", "s", false, "Purge all state data") | |
| 69 | + | cmd.Flags().BoolVarP(&flags.WebkitData, "webkit-data", "w", false, "Purge WebKit data (cookies, local storage)") | |
| 70 | + | cmd.Flags().BoolVar(&flags.Config, "config", false, "Purge configuration files") | |
| 71 | + | cmd.Flags().BoolVarP(&flags.All, "all", "a", false, "Purge everything") | |
| 72 | + | cmd.Flags().BoolVarP(&flags.Force, "force", "f", false, "Skip confirmation prompt") | |
| 73 | + | ||
| 74 | + | return cmd | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // executePurge handles the purge logic | |
| 78 | + | func executePurge(flags PurgeFlags) error { | |
| 79 | + | // Determine what to purge | |
| 80 | + | purgeItems := determinePurgeItems(flags) | |
| 81 | + | if len(purgeItems) == 0 { | |
| 82 | + | fmt.Println("Nothing to purge.") | |
| 83 | + | return nil | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // Get file paths for each item | |
| 87 | + | paths, err := getPurgePaths(purgeItems) | |
| 88 | + | if err != nil { | |
| 89 | + | return fmt.Errorf("failed to get purge paths: %w", err) | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // Show confirmation unless --force is used | |
| 93 | + | if !flags.Force { | |
| 94 | + | if !confirmPurge(purgeItems, paths) { | |
| 95 | + | fmt.Println("Purge cancelled.") | |
| 96 | + | return nil | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // Execute the purge | |
| 101 | + | return performPurge(purgeItems, paths) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // determinePurgeItems determines what should be purged based on flags | |
| 105 | + | func determinePurgeItems(flags PurgeFlags) []string { | |
| 106 | + | var items []string | |
| 107 | + | ||
| 108 | + | // If no specific flags are set, or --all is set, purge everything | |
| 109 | + | if flags.All || (!flags.Database && !flags.Cache && !flags.State && !flags.WebkitData && !flags.Config) { | |
| 110 | + | return []string{"database", "cache", "webkit-data", "config"} | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // Add items based on flags | |
| 114 | + | if flags.Database { | |
| 115 | + | items = append(items, "database") | |
| 116 | + | } | |
| 117 | + | if flags.Cache { | |
| 118 | + | items = append(items, "cache") | |
| 119 | + | } | |
| 120 | + | if flags.State { | |
| 121 | + | // State includes database and caches | |
| 122 | + | items = append(items, "database", "cache") | |
| 123 | + | } | |
| 124 | + | if flags.WebkitData { | |
| 125 | + | items = append(items, "webkit-data") | |
| 126 | + | } | |
| 127 | + | if flags.Config { | |
| 128 | + | items = append(items, "config") | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // Remove duplicates | |
| 132 | + | seen := make(map[string]bool) | |
| 133 | + | var result []string | |
| 134 | + | for _, item := range items { | |
| 135 | + | if !seen[item] { | |
| 136 | + | seen[item] = true | |
| 137 | + | result = append(result, item) | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | return result | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // getPurgePaths gets the file/directory paths for each purge item | |
| 145 | + | func getPurgePaths(items []string) (map[string][]string, error) { | |
| 146 | + | paths := make(map[string][]string) | |
| 147 | + | ||
| 148 | + | for _, item := range items { | |
| 149 | + | switch item { | |
| 150 | + | case "database": | |
| 151 | + | dbPath, err := config.GetDatabaseFile() | |
| 152 | + | if err != nil { | |
| 153 | + | return nil, fmt.Errorf("failed to get database path: %w", err) | |
| 154 | + | } | |
| 155 | + | paths[item] = []string{dbPath} | |
| 156 | + | ||
| 157 | + | case "cache": | |
| 158 | + | var cachePaths []string | |
| 159 | + | ||
| 160 | + | // WebKit cache | |
| 161 | + | stateDir, err := config.GetStateDir() | |
| 162 | + | if err != nil { | |
| 163 | + | return nil, fmt.Errorf("failed to get state directory: %w", err) | |
| 164 | + | } | |
| 165 | + | webkitCache := filepath.Join(stateDir, "webkit-cache") | |
| 166 | + | cachePaths = append(cachePaths, webkitCache) | |
| 167 | + | ||
| 168 | + | // Dmenu fuzzy cache | |
| 169 | + | dmenuCache := filepath.Join(stateDir, "dmenu_fuzzy_cache.bin") | |
| 170 | + | cachePaths = append(cachePaths, dmenuCache) | |
| 171 | + | ||
| 172 | + | paths[item] = cachePaths | |
| 173 | + | ||
| 174 | + | case "webkit-data": | |
| 175 | + | dataDir, err := config.GetDataDir() | |
| 176 | + | if err != nil { | |
| 177 | + | return nil, fmt.Errorf("failed to get data directory: %w", err) | |
| 178 | + | } | |
| 179 | + | webkitData := filepath.Join(dataDir, "webkit") | |
| 180 | + | paths[item] = []string{webkitData} | |
| 181 | + | ||
| 182 | + | case "config": | |
| 183 | + | configFile, err := config.GetConfigFile() | |
| 184 | + | if err != nil { | |
| 185 | + | return nil, fmt.Errorf("failed to get config file path: %w", err) | |
| 186 | + | } | |
| 187 | + | paths[item] = []string{configFile} | |
| 188 | + | } | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | return paths, nil | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // confirmPurge shows a confirmation prompt and returns true if user confirms | |
| 195 | + | func confirmPurge(items []string, paths map[string][]string) bool { | |
| 196 | + | fmt.Printf("This will delete the following:\n\n") | |
| 197 | + | ||
| 198 | + | for _, item := range items { | |
| 199 | + | fmt.Printf("• %s:\n", strings.Title(item)) | |
| 200 | + | for _, path := range paths[item] { | |
| 201 | + | // Check if path exists | |
| 202 | + | if _, err := os.Stat(path); err == nil { | |
| 203 | + | fmt.Printf(" - %s\n", path) | |
| 204 | + | } else { | |
| 205 | + | fmt.Printf(" - %s (not found)\n", path) | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | fmt.Printf("\nAre you sure you want to continue? (y/N): ") | |
| 211 | + | ||
| 212 | + | scanner := bufio.NewScanner(os.Stdin) | |
| 213 | + | if scanner.Scan() { | |
| 214 | + | response := strings.TrimSpace(strings.ToLower(scanner.Text())) | |
| 215 | + | return response == "y" || response == "yes" | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | return false | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // performPurge executes the actual deletion | |
| 222 | + | func performPurge(items []string, paths map[string][]string) error { | |
| 223 | + | var errors []string | |
| 224 | + | ||
| 225 | + | for _, item := range items { | |
| 226 | + | fmt.Printf("Purging %s...\n", item) | |
| 227 | + | ||
| 228 | + | for _, path := range paths[item] { | |
| 229 | + | if err := deletePath(path); err != nil { | |
| 230 | + | errors = append(errors, fmt.Sprintf("%s: %v", path, err)) | |
| 231 | + | fmt.Printf(" ✗ Failed to delete %s: %v\n", path, err) | |
| 232 | + | } else { | |
| 233 | + | fmt.Printf(" ✓ Deleted %s\n", path) | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | if len(errors) > 0 { | |
| 239 | + | return fmt.Errorf("some items could not be deleted:\n%s", strings.Join(errors, "\n")) | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | fmt.Println("\nPurge completed successfully!") | |
| 243 | + | return nil | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | // deletePath deletes a file or directory | |
| 247 | + | func deletePath(path string) error { | |
| 248 | + | // Check if path exists | |
| 249 | + | info, err := os.Stat(path) | |
| 250 | + | if os.IsNotExist(err) { | |
| 251 | + | // Path doesn't exist, consider it successful | |
| 252 | + | return nil | |
| 253 | + | } | |
| 254 | + | if err != nil { | |
| 255 | + | return fmt.Errorf("failed to check path: %w", err) | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | // Delete based on type | |
| 259 | + | if info.IsDir() { | |
| 260 | + | return os.RemoveAll(path) | |
| 261 | + | } | |
| 262 | + | return os.Remove(path) | |
| 263 | + | }⏎̸ | |
| @@ -0,0 +1,263 @@ | |||||
| 1 | + | package cli | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bufio" | |||
| 5 | + | "fmt" | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "strings" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/dumber/internal/config" | |||
| 11 | + | "github.com/spf13/cobra" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // PurgeFlags holds all the purge command flags | |||
| 15 | + | type PurgeFlags struct { | |||
| 16 | + | Database bool | |||
| 17 | + | Cache bool | |||
| 18 | + | State bool | |||
| 19 | + | WebkitData bool | |||
| 20 | + | Config bool | |||
| 21 | + | All bool | |||
| 22 | + | Force bool | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // NewPurgeCmd creates the purge command | |||
| 26 | + | func NewPurgeCmd() *cobra.Command { | |||
| 27 | + | var flags PurgeFlags | |||
| 28 | + | ||||
| 29 | + | cmd := &cobra.Command{ | |||
| 30 | + | Use: "purge", | |||
| 31 | + | Short: "Purge dumber data and cache files", | |||
| 32 | + | Long: `Purge various dumber data and cache files. By default, purges everything. | |||
| 33 | + | ||||
| 34 | + | Available purge targets: | |||
| 35 | + | --database, -d Purge the SQLite database (history, shortcuts, zoom levels) | |||
| 36 | + | --cache, -c Purge WebKit cache and dmenu fuzzy search cache | |||
| 37 | + | --state, -s Purge all state data (includes database and caches) | |||
| 38 | + | --webkit-data, -w Purge WebKit data (cookies, local storage, session data) | |||
| 39 | + | --config Purge configuration files | |||
| 40 | + | --all, -a Purge everything (default if no specific flags are provided) | |||
| 41 | + | ||||
| 42 | + | Use --force to skip the confirmation prompt. | |||
| 43 | + | ||||
| 44 | + | Examples: | |||
| 45 | + | dumber purge # Purge everything (with confirmation) | |||
| 46 | + | dumber purge --force # Purge everything (no confirmation) | |||
| 47 | + | dumber purge -d -c # Purge only database and caches | |||
| 48 | + | dumber purge --webkit-data -f # Force purge WebKit data only`, | |||
| 49 | + | RunE: func(_ *cobra.Command, _ []string) error { | |||
| 50 | + | // Initialize CLI to get config paths | |||
| 51 | + | cli, err := NewCLI() | |||
| 52 | + | if err != nil { | |||
| 53 | + | return fmt.Errorf("failed to initialize CLI: %w", err) | |||
| 54 | + | } | |||
| 55 | + | defer func() { | |||
| 56 | + | if closeErr := cli.Close(); closeErr != nil { | |||
| 57 | + | fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr) | |||
| 58 | + | } | |||
| 59 | + | }() | |||
| 60 | + | ||||
| 61 | + | return executePurge(flags) | |||
| 62 | + | }, | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // Add flags | |||
| 66 | + | cmd.Flags().BoolVarP(&flags.Database, "database", "d", false, "Purge the SQLite database") | |||
| 67 | + | cmd.Flags().BoolVarP(&flags.Cache, "cache", "c", false, "Purge WebKit cache and dmenu fuzzy cache") | |||
| 68 | + | cmd.Flags().BoolVarP(&flags.State, "state", "s", false, "Purge all state data") | |||
| 69 | + | cmd.Flags().BoolVarP(&flags.WebkitData, "webkit-data", "w", false, "Purge WebKit data (cookies, local storage)") | |||
| 70 | + | cmd.Flags().BoolVar(&flags.Config, "config", false, "Purge configuration files") | |||
| 71 | + | cmd.Flags().BoolVarP(&flags.All, "all", "a", false, "Purge everything") | |||
| 72 | + | cmd.Flags().BoolVarP(&flags.Force, "force", "f", false, "Skip confirmation prompt") | |||
| 73 | + | ||||
| 74 | + | return cmd | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // executePurge handles the purge logic | |||
| 78 | + | func executePurge(flags PurgeFlags) error { | |||
| 79 | + | // Determine what to purge | |||
| 80 | + | purgeItems := determinePurgeItems(flags) | |||
| 81 | + | if len(purgeItems) == 0 { | |||
| 82 | + | fmt.Println("Nothing to purge.") | |||
| 83 | + | return nil | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // Get file paths for each item | |||
| 87 | + | paths, err := getPurgePaths(purgeItems) | |||
| 88 | + | if err != nil { | |||
| 89 | + | return fmt.Errorf("failed to get purge paths: %w", err) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // Show confirmation unless --force is used | |||
| 93 | + | if !flags.Force { | |||
| 94 | + | if !confirmPurge(purgeItems, paths) { | |||
| 95 | + | fmt.Println("Purge cancelled.") | |||
| 96 | + | return nil | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // Execute the purge | |||
| 101 | + | return performPurge(purgeItems, paths) | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // determinePurgeItems determines what should be purged based on flags | |||
| 105 | + | func determinePurgeItems(flags PurgeFlags) []string { | |||
| 106 | + | var items []string | |||
| 107 | + | ||||
| 108 | + | // If no specific flags are set, or --all is set, purge everything | |||
| 109 | + | if flags.All || (!flags.Database && !flags.Cache && !flags.State && !flags.WebkitData && !flags.Config) { | |||
| 110 | + | return []string{"database", "cache", "webkit-data", "config"} | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // Add items based on flags | |||
| 114 | + | if flags.Database { | |||
| 115 | + | items = append(items, "database") | |||
| 116 | + | } | |||
| 117 | + | if flags.Cache { | |||
| 118 | + | items = append(items, "cache") | |||
| 119 | + | } | |||
| 120 | + | if flags.State { | |||
| 121 | + | // State includes database and caches | |||
| 122 | + | items = append(items, "database", "cache") | |||
| 123 | + | } | |||
| 124 | + | if flags.WebkitData { | |||
| 125 | + | items = append(items, "webkit-data") | |||
| 126 | + | } | |||
| 127 | + | if flags.Config { | |||
| 128 | + | items = append(items, "config") | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // Remove duplicates | |||
| 132 | + | seen := make(map[string]bool) | |||
| 133 | + | var result []string | |||
| 134 | + | for _, item := range items { | |||
| 135 | + | if !seen[item] { | |||
| 136 | + | seen[item] = true | |||
| 137 | + | result = append(result, item) | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | return result | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | // getPurgePaths gets the file/directory paths for each purge item | |||
| 145 | + | func getPurgePaths(items []string) (map[string][]string, error) { | |||
| 146 | + | paths := make(map[string][]string) | |||
| 147 | + | ||||
| 148 | + | for _, item := range items { | |||
| 149 | + | switch item { | |||
| 150 | + | case "database": | |||
| 151 | + | dbPath, err := config.GetDatabaseFile() | |||
| 152 | + | if err != nil { | |||
| 153 | + | return nil, fmt.Errorf("failed to get database path: %w", err) | |||
| 154 | + | } | |||
| 155 | + | paths[item] = []string{dbPath} | |||
| 156 | + | ||||
| 157 | + | case "cache": | |||
| 158 | + | var cachePaths []string | |||
| 159 | + | ||||
| 160 | + | // WebKit cache | |||
| 161 | + | stateDir, err := config.GetStateDir() | |||
| 162 | + | if err != nil { | |||
| 163 | + | return nil, fmt.Errorf("failed to get state directory: %w", err) | |||
| 164 | + | } | |||
| 165 | + | webkitCache := filepath.Join(stateDir, "webkit-cache") | |||
| 166 | + | cachePaths = append(cachePaths, webkitCache) | |||
| 167 | + | ||||
| 168 | + | // Dmenu fuzzy cache | |||
| 169 | + | dmenuCache := filepath.Join(stateDir, "dmenu_fuzzy_cache.bin") | |||
| 170 | + | cachePaths = append(cachePaths, dmenuCache) | |||
| 171 | + | ||||
| 172 | + | paths[item] = cachePaths | |||
| 173 | + | ||||
| 174 | + | case "webkit-data": | |||
| 175 | + | dataDir, err := config.GetDataDir() | |||
| 176 | + | if err != nil { | |||
| 177 | + | return nil, fmt.Errorf("failed to get data directory: %w", err) | |||
| 178 | + | } | |||
| 179 | + | webkitData := filepath.Join(dataDir, "webkit") | |||
| 180 | + | paths[item] = []string{webkitData} | |||
| 181 | + | ||||
| 182 | + | case "config": | |||
| 183 | + | configFile, err := config.GetConfigFile() | |||
| 184 | + | if err != nil { | |||
| 185 | + | return nil, fmt.Errorf("failed to get config file path: %w", err) | |||
| 186 | + | } | |||
| 187 | + | paths[item] = []string{configFile} | |||
| 188 | + | } | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | return paths, nil | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // confirmPurge shows a confirmation prompt and returns true if user confirms | |||
| 195 | + | func confirmPurge(items []string, paths map[string][]string) bool { | |||
| 196 | + | fmt.Printf("This will delete the following:\n\n") | |||
| 197 | + | ||||
| 198 | + | for _, item := range items { | |||
| 199 | + | fmt.Printf("• %s:\n", strings.Title(item)) | |||
| 200 | + | for _, path := range paths[item] { | |||
| 201 | + | // Check if path exists | |||
| 202 | + | if _, err := os.Stat(path); err == nil { | |||
| 203 | + | fmt.Printf(" - %s\n", path) | |||
| 204 | + | } else { | |||
| 205 | + | fmt.Printf(" - %s (not found)\n", path) | |||
| 206 | + | } | |||
| 207 | + | } | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | fmt.Printf("\nAre you sure you want to continue? (y/N): ") | |||
| 211 | + | ||||
| 212 | + | scanner := bufio.NewScanner(os.Stdin) | |||
| 213 | + | if scanner.Scan() { | |||
| 214 | + | response := strings.TrimSpace(strings.ToLower(scanner.Text())) | |||
| 215 | + | return response == "y" || response == "yes" | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | return false | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // performPurge executes the actual deletion | |||
| 222 | + | func performPurge(items []string, paths map[string][]string) error { | |||
| 223 | + | var errors []string | |||
| 224 | + | ||||
| 225 | + | for _, item := range items { | |||
| 226 | + | fmt.Printf("Purging %s...\n", item) | |||
| 227 | + | ||||
| 228 | + | for _, path := range paths[item] { | |||
| 229 | + | if err := deletePath(path); err != nil { | |||
| 230 | + | errors = append(errors, fmt.Sprintf("%s: %v", path, err)) | |||
| 231 | + | fmt.Printf(" ✗ Failed to delete %s: %v\n", path, err) | |||
| 232 | + | } else { | |||
| 233 | + | fmt.Printf(" ✓ Deleted %s\n", path) | |||
| 234 | + | } | |||
| 235 | + | } | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | if len(errors) > 0 { | |||
| 239 | + | return fmt.Errorf("some items could not be deleted:\n%s", strings.Join(errors, "\n")) | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | fmt.Println("\nPurge completed successfully!") | |||
| 243 | + | return nil | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | // deletePath deletes a file or directory | |||
| 247 | + | func deletePath(path string) error { | |||
| 248 | + | // Check if path exists | |||
| 249 | + | info, err := os.Stat(path) | |||
| 250 | + | if os.IsNotExist(err) { | |||
| 251 | + | // Path doesn't exist, consider it successful | |||
| 252 | + | return nil | |||
| 253 | + | } | |||
| 254 | + | if err != nil { | |||
| 255 | + | return fmt.Errorf("failed to check path: %w", err) | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | // Delete based on type | |||
| 259 | + | if info.IsDir() { | |||
| 260 | + | return os.RemoveAll(path) | |||
| 261 | + | } | |||
| 262 | + | return os.Remove(path) | |||
| 263 | + | }⏎̸ | |||
M internal/cli/root.go +1 -0
| @@ -161,6 +161,7 @@ func NewRootCmd(version, commit, buildDate string) *cobra.Command { | |||
| 161 | 161 | rootCmd.AddCommand(NewBrowseCmd()) | |
| 162 | 162 | rootCmd.AddCommand(NewDmenuCmd()) | |
| 163 | 163 | rootCmd.AddCommand(NewHistoryCmd()) | |
| 164 | + | rootCmd.AddCommand(NewPurgeCmd()) | |
| 164 | 165 | ||
| 165 | 166 | return rootCmd | |
| 166 | 167 | } | |
| @@ -161,6 +161,7 @@ func NewRootCmd(version, commit, buildDate string) *cobra.Command { | |||||
| 161 | rootCmd.AddCommand(NewBrowseCmd()) | 161 | rootCmd.AddCommand(NewBrowseCmd()) | ||
| 162 | rootCmd.AddCommand(NewDmenuCmd()) | 162 | rootCmd.AddCommand(NewDmenuCmd()) | ||
| 163 | rootCmd.AddCommand(NewHistoryCmd()) | 163 | rootCmd.AddCommand(NewHistoryCmd()) | ||
| 164 | + | rootCmd.AddCommand(NewPurgeCmd()) | |||
| 164 | 165 | ||||
| 165 | return rootCmd | 166 | return rootCmd | ||
| 166 | } | 167 | } | ||