v0.15.0 9006957

purge.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close