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}