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}
35
36// NewFileFilterStore creates a new file-based filter store
37func NewFileFilterStore() (*FileFilterStore, error) {
38 cacheFile, err := config.GetFilterCacheFile()
39 if err != nil {
40 return nil, fmt.Errorf("failed to get cache file path: %w", err)
41 }
42
43 cacheDir := filepath.Dir(cacheFile)
44 metaFile := filepath.Join(cacheDir, "metadata.json")
45
46 // Ensure cache directory exists
47 if err := os.MkdirAll(cacheDir, dirPermissions); err != nil {
48 return nil, fmt.Errorf("failed to create cache directory: %w", err)
49 }
50
51 return &FileFilterStore{
52 cacheFile: cacheFile,
53 metaFile: metaFile,
54 }, nil
55}
56
57// LoadCached loads compiled filters from cache
58func (fs *FileFilterStore) LoadCached() (*CompiledFilters, error) {
59 // Check if cache file exists
60 if _, err := os.Stat(fs.cacheFile); os.IsNotExist(err) {
61 return nil, fmt.Errorf("cache file does not exist")
62 }
63
64 // Load metadata
65 metadata, err := fs.loadMetadata()
66 if err != nil {
67 return nil, fmt.Errorf("failed to load cache metadata: %w", err)
68 }
69
70 // Verify cache version compatibility
71 if metadata.Version != cacheVersion {
72 return nil, fmt.Errorf("cache version mismatch: got %d, expected %d", metadata.Version, cacheVersion)
73 }
74
75 // Load cache data
76 data, err := os.ReadFile(fs.cacheFile)
77 if err != nil {
78 return nil, fmt.Errorf("failed to read cache file: %w", err)
79 }
80
81 // Verify data integrity
82 dataHash := fmt.Sprintf("%x", sha256.Sum256(data))
83 if dataHash != metadata.DataHash {
84 return nil, fmt.Errorf("cache data corrupted: hash mismatch")
85 }
86
87 // Deserialize compiled filters
88 var compiled CompiledFilters
89 if err := json.Unmarshal(data, &compiled); err != nil {
90 return nil, fmt.Errorf("failed to deserialize cache data: %w", err)
91 }
92
93 // Update last used time
94 metadata.LastUsed = time.Now()
95 if err := fs.saveMetadata(metadata); err != nil {
96 logging.Error(fmt.Sprintf("Failed to update cache metadata: %v", err))
97 }
98
99 logging.Info(fmt.Sprintf("Loaded filter cache from %s", fs.cacheFile))
100 return &compiled, nil
101}
102
103// SaveCache saves compiled filters to cache
104func (fs *FileFilterStore) SaveCache(filters *CompiledFilters) error {
105 // Serialize filters
106 data, err := json.Marshal(filters)
107 if err != nil {
108 return fmt.Errorf("failed to serialize filters: %w", err)
109 }
110
111 // Create metadata
112 metadata := &CacheMetadata{
113 Version: cacheVersion,
114 CreatedAt: time.Now(),
115 LastUsed: time.Now(),
116 DataHash: fmt.Sprintf("%x", sha256.Sum256(data)),
117 // FilterHashes would be populated with source list hashes in a full implementation
118 FilterHashes: []string{},
119 }
120
121 // Write cache data atomically
122 tempFile := fs.cacheFile + ".tmp"
123 if err := os.WriteFile(tempFile, data, filePermissions); err != nil {
124 return fmt.Errorf("failed to write cache data: %w", err)
125 }
126
127 if err := os.Rename(tempFile, fs.cacheFile); err != nil {
128 if removeErr := os.Remove(tempFile); removeErr != nil {
129 logging.Error(fmt.Sprintf("Failed to cleanup temp file %s: %v", tempFile, removeErr))
130 }
131 return fmt.Errorf("failed to move cache file: %w", err)
132 }
133
134 // Save metadata
135 if err := fs.saveMetadata(metadata); err != nil {
136 return fmt.Errorf("failed to save cache metadata: %w", err)
137 }
138
139 logging.Info(fmt.Sprintf("Saved filter cache to %s", fs.cacheFile))
140 return nil
141}
142
143// GetCacheInfo returns cache existence and modification time
144func (fs *FileFilterStore) GetCacheInfo() (bool, time.Time, error) {
145 info, err := os.Stat(fs.cacheFile)
146 if os.IsNotExist(err) {
147 return false, time.Time{}, nil
148 }
149 if err != nil {
150 return false, time.Time{}, err
151 }
152
153 return true, info.ModTime(), nil
154}
155
156// InvalidateCache removes the cache file
157func (fs *FileFilterStore) InvalidateCache() error {
158 // Remove cache file
159 if err := os.Remove(fs.cacheFile); err != nil && !os.IsNotExist(err) {
160 return fmt.Errorf("failed to remove cache file: %w", err)
161 }
162
163 // Remove metadata file
164 if err := os.Remove(fs.metaFile); err != nil && !os.IsNotExist(err) {
165 return fmt.Errorf("failed to remove metadata file: %w", err)
166 }
167
168 logging.Info("Filter cache invalidated")
169 return nil
170}
171
172// loadMetadata loads cache metadata from file
173func (fs *FileFilterStore) loadMetadata() (*CacheMetadata, error) {
174 data, err := os.ReadFile(fs.metaFile)
175 if err != nil {
176 return nil, err
177 }
178
179 var metadata CacheMetadata
180 if err := json.Unmarshal(data, &metadata); err != nil {
181 return nil, err
182 }
183
184 return &metadata, nil
185}
186
187// saveMetadata saves cache metadata to file
188func (fs *FileFilterStore) saveMetadata(metadata *CacheMetadata) error {
189 data, err := json.Marshal(metadata)
190 if err != nil {
191 return err
192 }
193
194 // Write metadata atomically
195 tempFile := fs.metaFile + ".tmp"
196 if err := os.WriteFile(tempFile, data, filePermissions); err != nil {
197 return err
198 }
199
200 if err := os.Rename(tempFile, fs.metaFile); err != nil {
201 if removeErr := os.Remove(tempFile); removeErr != nil {
202 logging.Error(fmt.Sprintf("Failed to cleanup temp metadata file %s: %v", tempFile, removeErr))
203 }
204 return err
205 }
206
207 return nil
208}