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