1package cli
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "unicode"
10
11 "github.com/bnema/dumber/internal/config"
12 "github.com/spf13/cobra"
13)
14
15// toTitle capitalizes the first letter of the string
16func toTitle(s string) string {
17 if len(s) == 0 {
18 return s
19 }
20 runes := []rune(s)
21 runes[0] = unicode.ToUpper(runes[0])
22 return string(runes)
23}
24
25// PurgeFlags holds all the purge command flags
26type PurgeFlags struct {
27 Database bool
28 HistoryCache bool
29 BrowserCache bool
30 BrowserData bool
31 FilterCache bool
32 State bool
33 Config bool
34 All bool
35 Force bool
36}
37
38// NewPurgeCmd creates the purge command
39func NewPurgeCmd() *cobra.Command {
40 var flags PurgeFlags
41
42 cmd := &cobra.Command{
43 Use: "purge",
44 Short: "Purge dumber data and cache files",
45 Long: `Purge various dumber data and cache files. By default, purges everything.
46
47Available purge targets:
48 --database, -d Purge the SQLite database (history, shortcuts, zoom levels)
49 --history-cache, -H Purge dmenu fuzzy search cache for history
50 --browser-cache, -c Purge WebKit browser cache (cached images, files, etc.)
51 --browser-data, -b Purge WebKit browser data (cookies, localStorage, sessionStorage)
52 --filter-cache, -F Purge compiled ad blocking filters cache
53 --state, -s Purge all state data (includes database and caches)
54 --config Purge configuration files
55 --all, -a Purge everything (default if no specific flags are provided)
56
57Use --force to skip the confirmation prompt.
58
59Examples:
60 dumber purge # Purge everything (with confirmation)
61 dumber purge --force # Purge everything (no confirmation)
62 dumber purge -d -H -c -F # Purge database and all caches
63 dumber purge --filter-cache -f # Force purge filter cache only`,
64 RunE: func(_ *cobra.Command, _ []string) error {
65 // Initialize CLI to get config paths
66 cli, err := NewCLI()
67 if err != nil {
68 return fmt.Errorf("failed to initialize CLI: %w", err)
69 }
70 defer func() {
71 if closeErr := cli.Close(); closeErr != nil {
72 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
73 }
74 }()
75
76 return executePurge(flags)
77 },
78 }
79
80 // Add flags
81 cmd.Flags().BoolVarP(&flags.Database, "database", "d", false, "Purge the SQLite database")
82 cmd.Flags().BoolVarP(&flags.HistoryCache, "history-cache", "H", false, "Purge dmenu fuzzy search cache")
83 cmd.Flags().BoolVarP(&flags.BrowserCache, "browser-cache", "c", false, "Purge WebKit browser cache")
84 cmd.Flags().BoolVarP(&flags.BrowserData, "browser-data", "b", false, "Purge WebKit browser data (cookies, localStorage)")
85 cmd.Flags().BoolVarP(&flags.FilterCache, "filter-cache", "F", false, "Purge compiled ad blocking filters cache")
86 cmd.Flags().BoolVarP(&flags.State, "state", "s", false, "Purge all state data")
87 cmd.Flags().BoolVar(&flags.Config, "config", false, "Purge configuration files")
88 cmd.Flags().BoolVarP(&flags.All, "all", "a", false, "Purge everything")
89 cmd.Flags().BoolVarP(&flags.Force, "force", "f", false, "Skip confirmation prompt")
90
91 return cmd
92}
93
94// executePurge handles the purge logic
95func executePurge(flags PurgeFlags) error {
96 // Determine what to purge
97 purgeItems := determinePurgeItems(flags)
98 if len(purgeItems) == 0 {
99 fmt.Println("Nothing to purge.")
100 return nil
101 }
102
103 // Get file paths for each item
104 paths, err := getPurgePaths(purgeItems)
105 if err != nil {
106 return fmt.Errorf("failed to get purge paths: %w", err)
107 }
108
109 // Show confirmation unless --force is used
110 if !flags.Force {
111 if !confirmPurge(purgeItems, paths) {
112 fmt.Println("Purge cancelled.")
113 return nil
114 }
115 }
116
117 // Execute the purge
118 return performPurge(purgeItems, paths)
119}
120
121// determinePurgeItems determines what should be purged based on flags
122func determinePurgeItems(flags PurgeFlags) []string {
123 var items []string
124
125 // If no specific flags are set, or --all is set, purge everything
126 if flags.All || (!flags.Database && !flags.HistoryCache && !flags.BrowserCache && !flags.BrowserData && !flags.FilterCache && !flags.State && !flags.Config) {
127 return []string{"database", "history-cache", "browser-cache", "browser-data", "filter-cache", "config"}
128 }
129
130 // Add items based on flags
131 if flags.Database {
132 items = append(items, "database")
133 }
134 if flags.HistoryCache {
135 items = append(items, "history-cache")
136 }
137 if flags.BrowserCache {
138 items = append(items, "browser-cache")
139 }
140 if flags.BrowserData {
141 items = append(items, "browser-data")
142 }
143 if flags.FilterCache {
144 items = append(items, "filter-cache")
145 }
146 if flags.State {
147 // State includes database and all caches
148 items = append(items, "database", "history-cache", "browser-cache", "filter-cache")
149 }
150 if flags.Config {
151 items = append(items, "config")
152 }
153
154 // Remove duplicates
155 seen := make(map[string]bool)
156 var result []string
157 for _, item := range items {
158 if !seen[item] {
159 seen[item] = true
160 result = append(result, item)
161 }
162 }
163
164 return result
165}
166
167// getPurgePaths gets the file/directory paths for each purge item
168func getPurgePaths(items []string) (map[string][]string, error) {
169 paths := make(map[string][]string)
170
171 for _, item := range items {
172 switch item {
173 case "database":
174 dbPath, err := config.GetDatabaseFile()
175 if err != nil {
176 return nil, fmt.Errorf("failed to get database path: %w", err)
177 }
178 paths[item] = []string{dbPath}
179
180 case "history-cache":
181 stateDir, err := config.GetStateDir()
182 if err != nil {
183 return nil, fmt.Errorf("failed to get state directory: %w", err)
184 }
185 dmenuCache := filepath.Join(stateDir, "dmenu_fuzzy_cache.bin")
186 paths[item] = []string{dmenuCache}
187
188 case "browser-cache":
189 stateDir, err := config.GetStateDir()
190 if err != nil {
191 return nil, fmt.Errorf("failed to get state directory: %w", err)
192 }
193 webkitCache := filepath.Join(stateDir, "webkit-cache")
194 paths[item] = []string{webkitCache}
195
196 case "browser-data":
197 dataDir, err := config.GetDataDir()
198 if err != nil {
199 return nil, fmt.Errorf("failed to get data directory: %w", err)
200 }
201 webkitData := filepath.Join(dataDir, "webkit")
202 paths[item] = []string{webkitData}
203
204 case "filter-cache":
205 filterCacheDir, err := config.GetFilterCacheDir()
206 if err != nil {
207 return nil, fmt.Errorf("failed to get filter cache directory: %w", err)
208 }
209 paths[item] = []string{filterCacheDir}
210
211 case "config":
212 configFile, err := config.GetConfigFile()
213 if err != nil {
214 return nil, fmt.Errorf("failed to get config file path: %w", err)
215 }
216 paths[item] = []string{configFile}
217 }
218 }
219
220 return paths, nil
221}
222
223// confirmPurge shows a confirmation prompt and returns true if user confirms
224func confirmPurge(items []string, paths map[string][]string) bool {
225 fmt.Printf("This will delete the following:\n\n")
226
227 for _, item := range items {
228 fmt.Printf("• %s:\n", toTitle(item))
229 for _, path := range paths[item] {
230 // Check if path exists
231 if _, err := os.Stat(path); err == nil {
232 fmt.Printf(" - %s\n", path)
233 } else {
234 fmt.Printf(" - %s (not found)\n", path)
235 }
236 }
237 }
238
239 fmt.Printf("\nAre you sure you want to continue? (y/N): ")
240
241 scanner := bufio.NewScanner(os.Stdin)
242 if scanner.Scan() {
243 response := strings.TrimSpace(strings.ToLower(scanner.Text()))
244 return response == "y" || response == "yes"
245 }
246
247 return false
248}
249
250// performPurge executes the actual deletion
251func performPurge(items []string, paths map[string][]string) error {
252 var errors []string
253
254 for _, item := range items {
255 fmt.Printf("Purging %s...\n", item)
256
257 for _, path := range paths[item] {
258 if err := deletePath(path); err != nil {
259 errors = append(errors, fmt.Sprintf("%s: %v", path, err))
260 fmt.Printf(" ✗ Failed to delete %s: %v\n", path, err)
261 } else {
262 fmt.Printf(" ✓ Deleted %s\n", path)
263 }
264 }
265 }
266
267 if len(errors) > 0 {
268 return fmt.Errorf("some items could not be deleted:\n%s", strings.Join(errors, "\n"))
269 }
270
271 fmt.Println("\nPurge completed successfully!")
272 return nil
273}
274
275// deletePath deletes a file or directory
276func deletePath(path string) error {
277 // Check if path exists
278 info, err := os.Stat(path)
279 if os.IsNotExist(err) {
280 // Path doesn't exist, consider it successful
281 return nil
282 }
283 if err != nil {
284 return fmt.Errorf("failed to check path: %w", err)
285 }
286
287 // Delete based on type
288 if info.IsDir() {
289 return os.RemoveAll(path)
290 }
291 return os.Remove(path)
292}