v0.15.0 9006957

store.go RawHistoryFolder

1package filtering
2
3import (
4	"crypto/sha256"
5	"encoding/json"
6	"fmt"
7	"os"
8	"path/filepath"
9	"time"
10
11	"github.com/bnema/dumber/internal/config"
12	"github.com/bnema/dumber/internal/logging"
13)
14
15const (
16	cacheVersion    = 1    // Cache format version for compatibility
17	dirPermissions  = 0755 // Directory permissions
18	filePermissions = 0644 // File permissions
19)
20
21// FileFilterStore implements FilterStore using filesystem storage
22type FileFilterStore struct {
23	cacheFile string
24	metaFile  string
25}
26
27// CacheMetadata stores information about the cache
28type CacheMetadata struct {
29	Version        int               `json:"version"`
30	CreatedAt      time.Time         `json:"created_at"`
31	LastUsed       time.Time         `json:"last_used"`
32	DataHash       string            `json:"data_hash"`
33	FilterHashes   []string          `json:"filter_hashes"`   // Hashes of source filter lists
34	SourceVersions map[string]string `json:"source_versions"` // URL -> filter list version
35	LastCheckTime  time.Time         `json:"last_check_time"` // Last version check time
36}
37
38// NewFileFilterStore creates a new file-based filter store
39func NewFileFilterStore() (*FileFilterStore, error) {
40	cacheFile, err := config.GetFilterCacheFile()
41	if err != nil {
42		return nil, fmt.Errorf("failed to get cache file path: %w", err)
43	}
44
45	cacheDir := filepath.Dir(cacheFile)
46	metaFile := filepath.Join(cacheDir, "metadata.json")
47
48	// Ensure cache directory exists
49	if err := os.MkdirAll(cacheDir, dirPermissions); err != nil {
50		return nil, fmt.Errorf("failed to create cache directory: %w", err)
51	}
52
53	return &FileFilterStore{
54		cacheFile: cacheFile,
55		metaFile:  metaFile,
56	}, nil
57}
58
59// LoadCached loads compiled filters from cache
60func (fs *FileFilterStore) LoadCached() (*CompiledFilters, error) {
61	// Check if cache file exists
62	if _, err := os.Stat(fs.cacheFile); os.IsNotExist(err) {
63		return nil, fmt.Errorf("cache file does not exist")
64	}
65
66	// Load metadata
67	metadata, err := fs.loadMetadata()
68	if err != nil {
69		return nil, fmt.Errorf("failed to load cache metadata: %w", err)
70	}
71
72	// Verify cache version compatibility
73	if metadata.Version != cacheVersion {
74		return nil, fmt.Errorf("cache version mismatch: got %d, expected %d", metadata.Version, cacheVersion)
75	}
76
77	// Load cache data
78	data, err := os.ReadFile(fs.cacheFile)
79	if err != nil {
80		return nil, fmt.Errorf("failed to read cache file: %w", err)
81	}
82
83	// Verify data integrity
84	dataHash := fmt.Sprintf("%x", sha256.Sum256(data))
85	if dataHash != metadata.DataHash {
86		return nil, fmt.Errorf("cache data corrupted: hash mismatch")
87	}
88
89	// Deserialize compiled filters
90	var compiled CompiledFilters
91	if err := json.Unmarshal(data, &compiled); err != nil {
92		return nil, fmt.Errorf("failed to deserialize cache data: %w", err)
93	}
94
95	// Update last used time
96	metadata.LastUsed = time.Now()
97	if err := fs.saveMetadata(metadata); err != nil {
98		logging.Error(fmt.Sprintf("Failed to update cache metadata: %v", err))
99	}
100
101	logging.Info(fmt.Sprintf("Loaded filter cache from %s", fs.cacheFile))
102	return &compiled, nil
103}
104
105// SaveCache saves compiled filters to cache
106func (fs *FileFilterStore) SaveCache(filters *CompiledFilters) error {
107	// Serialize filters
108	data, err := json.Marshal(filters)
109	if err != nil {
110		return fmt.Errorf("failed to serialize filters: %w", err)
111	}
112
113	// Create metadata
114	metadata := &CacheMetadata{
115		Version:   cacheVersion,
116		CreatedAt: time.Now(),
117		LastUsed:  time.Now(),
118		DataHash:  fmt.Sprintf("%x", sha256.Sum256(data)),
119		// FilterHashes would be populated with source list hashes in a full implementation
120		FilterHashes: []string{},
121	}
122
123	// Write cache data atomically
124	tempFile := fs.cacheFile + ".tmp"
125	if err := os.WriteFile(tempFile, data, filePermissions); err != nil {
126		return fmt.Errorf("failed to write cache data: %w", err)
127	}
128
129	if err := os.Rename(tempFile, fs.cacheFile); err != nil {
130		if removeErr := os.Remove(tempFile); removeErr != nil {
131			logging.Error(fmt.Sprintf("Failed to cleanup temp file %s: %v", tempFile, removeErr))
132		}
133		return fmt.Errorf("failed to move cache file: %w", err)
134	}
135
136	// Save metadata
137	if err := fs.saveMetadata(metadata); err != nil {
138		return fmt.Errorf("failed to save cache metadata: %w", err)
139	}
140
141	logging.Info(fmt.Sprintf("Saved filter cache to %s", fs.cacheFile))
142	return nil
143}
144
145// GetCacheInfo returns cache existence and modification time
146func (fs *FileFilterStore) GetCacheInfo() (bool, time.Time, error) {
147	info, err := os.Stat(fs.cacheFile)
148	if os.IsNotExist(err) {
149		return false, time.Time{}, nil
150	}
151	if err != nil {
152		return false, time.Time{}, err
153	}
154
155	return true, info.ModTime(), nil
156}
157
158// InvalidateCache removes the cache file
159func (fs *FileFilterStore) InvalidateCache() error {
160	// Remove cache file
161	if err := os.Remove(fs.cacheFile); err != nil && !os.IsNotExist(err) {
162		return fmt.Errorf("failed to remove cache file: %w", err)
163	}
164
165	// Remove metadata file
166	if err := os.Remove(fs.metaFile); err != nil && !os.IsNotExist(err) {
167		return fmt.Errorf("failed to remove metadata file: %w", err)
168	}
169
170	logging.Info("Filter cache invalidated")
171	return nil
172}
173
174// loadMetadata loads cache metadata from file
175func (fs *FileFilterStore) loadMetadata() (*CacheMetadata, error) {
176	data, err := os.ReadFile(fs.metaFile)
177	if err != nil {
178		return nil, err
179	}
180
181	var metadata CacheMetadata
182	if err := json.Unmarshal(data, &metadata); err != nil {
183		return nil, err
184	}
185
186	return &metadata, nil
187}
188
189// saveMetadata saves cache metadata to file
190func (fs *FileFilterStore) saveMetadata(metadata *CacheMetadata) error {
191	data, err := json.Marshal(metadata)
192	if err != nil {
193		return err
194	}
195
196	// Write metadata atomically
197	tempFile := fs.metaFile + ".tmp"
198	if err := os.WriteFile(tempFile, data, filePermissions); err != nil {
199		return err
200	}
201
202	if err := os.Rename(tempFile, fs.metaFile); err != nil {
203		if removeErr := os.Remove(tempFile); removeErr != nil {
204			logging.Error(fmt.Sprintf("Failed to cleanup temp metadata file %s: %v", tempFile, removeErr))
205		}
206		return err
207	}
208
209	return nil
210}
211
212// GetSourceVersion returns the stored version for a filter list URL
213func (fs *FileFilterStore) GetSourceVersion(url string) string {
214	metadata, err := fs.loadMetadata()
215	if err != nil {
216		return ""
217	}
218	if metadata.SourceVersions == nil {
219		return ""
220	}
221	return metadata.SourceVersions[url]
222}
223
224// SetSourceVersion stores the version for a filter list URL
225func (fs *FileFilterStore) SetSourceVersion(url string, version string) error {
226	metadata, err := fs.loadMetadata()
227	if err != nil {
228		// Create new metadata if none exists
229		metadata = &CacheMetadata{
230			Version:        cacheVersion,
231			CreatedAt:      time.Now(),
232			SourceVersions: make(map[string]string),
233		}
234	}
235	if metadata.SourceVersions == nil {
236		metadata.SourceVersions = make(map[string]string)
237	}
238	metadata.SourceVersions[url] = version
239	metadata.LastCheckTime = time.Now()
240	return fs.saveMetadata(metadata)
241}
242
243// GetLastCheckTime returns when versions were last checked
244func (fs *FileFilterStore) GetLastCheckTime() time.Time {
245	metadata, err := fs.loadMetadata()
246	if err != nil {
247		return time.Time{}
248	}
249	return metadata.LastCheckTime
250}

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