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