lru.go

  1package config
  2
  3import (
  4	"container/list"
  5	"encoding/json"
  6	"fmt"
  7	"log"
  8	"os"
  9	"path/filepath"
 10	"sort"
 11	"sync"
 12	"time"
 13)
 14
 15type Node struct {
 16	Key    string // key = folder:uid:accountID
 17	Folder string
 18	Body   *CachedEmailBody
 19}
 20
 21type LRU struct {
 22	threshold   int
 23	currentSize int
 24	cache       map[string]*list.Element
 25	ll          *list.List
 26	mu          sync.Mutex
 27}
 28
 29var lru *LRU
 30var once sync.Once
 31
 32func GetLRUInstance(threshold int) *LRU {
 33	once.Do(
 34		func() {
 35			lru = &LRU{
 36				threshold: threshold,
 37				cache:     make(map[string]*list.Element),
 38				ll:        list.New(),
 39			}
 40
 41			if err := lru.LoadFromDisk(); err != nil {
 42				log.Printf("Failed to load LRU from disk: %v\n", err)
 43			}
 44		})
 45
 46	lru.mu.Lock()
 47	defer lru.mu.Unlock()
 48
 49	if lru.threshold != threshold {
 50		lru.threshold = threshold
 51		if lru.currentSize > lru.threshold {
 52			lru.evict()
 53		}
 54	}
 55
 56	return lru
 57}
 58
 59func (lru *LRU) makeKey(folder string, uid uint32, accountID string) string {
 60	return fmt.Sprintf("%s:%d:%s", folder, uid, accountID)
 61}
 62
 63func removeBodyFromDisk(folder string, uid uint32, accountID string) error {
 64	cache, err := LoadEmailBodyCache(folder)
 65
 66	if err != nil {
 67		return err
 68	}
 69
 70	kept := cache.Bodies[:0]
 71	for _, b := range cache.Bodies {
 72		if b.UID != uid || b.AccountID != accountID {
 73			kept = append(kept, b)
 74		}
 75	}
 76
 77	if len(kept) == len(cache.Bodies) {
 78		return nil
 79	}
 80
 81	cache.Bodies = kept
 82	return saveEmailBodyCache(cache)
 83}
 84
 85func (lru *LRU) evict() {
 86	for lru.currentSize > lru.threshold {
 87		back := lru.ll.Back()
 88
 89		if back == nil {
 90			break
 91		}
 92
 93		node := back.Value.(*Node)
 94
 95		lru.ll.Remove(back)
 96		delete(lru.cache, node.Key)
 97		lru.currentSize -= node.Body.SizeBytes
 98
 99		_ = removeBodyFromDisk(node.Folder, node.Body.UID, node.Body.AccountID)
100	}
101}
102
103func (lru *LRU) LoadFromDisk() error {
104	dir, err := bodyCacheDir()
105
106	if err != nil {
107		return err
108	}
109
110	entries, err := os.ReadDir(dir)
111	if err != nil {
112		if os.IsNotExist(err) {
113			return nil
114		}
115		return err
116	}
117
118	var caches []EmailBodyCache
119
120	for _, entry := range entries {
121		if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
122			continue
123		}
124
125		path := filepath.Join(dir, entry.Name())
126		data, err := SecureReadFile(path)
127		if err != nil {
128			continue
129		}
130
131		var cache EmailBodyCache
132		if err := json.Unmarshal(data, &cache); err != nil {
133			continue
134		}
135
136		for i := range cache.Bodies {
137			if cache.Bodies[i].SizeBytes <= 0 {
138				cache.Bodies[i].SizeBytes = calculateEmailBodySize(&cache.Bodies[i])
139			}
140		}
141
142		caches = append(caches, cache)
143	}
144
145	type bodyWithFolder struct {
146		folder string
147		body   CachedEmailBody
148	}
149
150	var allBodies []bodyWithFolder
151
152	for _, cache := range caches {
153		for _, body := range cache.Bodies {
154			allBodies = append(allBodies, bodyWithFolder{
155				folder: cache.FolderName,
156				body:   body,
157			})
158		}
159	}
160
161	sort.Slice(allBodies, func(i, j int) bool {
162		ti := allBodies[i].body.LastAccessedAt
163		tj := allBodies[j].body.LastAccessedAt
164		return ti.After(tj)
165	})
166
167	for i := len(allBodies) - 1; i >= 0; i-- {
168		item := allBodies[i]
169
170		if item.body.SizeBytes > lru.threshold {
171			continue
172		}
173
174		key := lru.makeKey(item.folder, item.body.UID, item.body.AccountID)
175
176		bodyCopy := item.body
177		node := &Node{
178			Key:    key,
179			Folder: item.folder,
180			Body:   &bodyCopy,
181		}
182
183		e := lru.ll.PushFront(node)
184		lru.cache[key] = e
185		lru.currentSize += item.body.SizeBytes
186	}
187
188	if lru.currentSize > lru.threshold {
189		lru.evict()
190	}
191	return nil
192}
193
194func saveEmailBodyToDisk(folder string, body *CachedEmailBody) error {
195	cache, err := LoadEmailBodyCache(folder)
196
197	if err != nil {
198		cache = &EmailBodyCache{FolderName: folder}
199	}
200
201	found := false
202	for i, b := range cache.Bodies {
203		if b.UID == body.UID && b.AccountID == body.AccountID {
204			cache.Bodies[i] = *body
205			found = true
206			break
207		}
208	}
209	if !found {
210		cache.Bodies = append(cache.Bodies, *body)
211	}
212
213	return saveEmailBodyCache(cache)
214}
215
216func (lru *LRU) Get(folder string, uid uint32, accountID string) *CachedEmailBody {
217	lru.mu.Lock()
218	defer lru.mu.Unlock()
219
220	key := lru.makeKey(folder, uid, accountID)
221
222	e, ok := lru.cache[key]
223
224	if !ok {
225		return nil
226	}
227
228	lru.ll.MoveToFront(e)
229
230	node := e.Value.(*Node)
231	node.Body.LastAccessedAt = time.Now()
232
233	_ = saveEmailBodyToDisk(folder, node.Body)
234
235	bodyCopy := *node.Body
236
237	return &bodyCopy
238}
239
240func (lru *LRU) removeKey(key string) {
241	if e, ok := lru.cache[key]; ok {
242		node := e.Value.(*Node)
243
244		lru.currentSize -= node.Body.SizeBytes
245		lru.ll.Remove(e)
246		delete(lru.cache, key)
247	}
248}
249
250func (lru *LRU) Put(folder string, uid uint32, accountID string, body *CachedEmailBody) {
251	lru.mu.Lock()
252	defer lru.mu.Unlock()
253
254	key := lru.makeKey(folder, uid, accountID)
255
256	if body.SizeBytes > lru.threshold {
257		lru.removeKey(key)
258		_ = removeBodyFromDisk(folder, uid, accountID)
259		return
260	}
261
262	body.LastAccessedAt = time.Now()
263
264	if e, ok := lru.cache[key]; ok {
265		node := e.Value.(*Node)
266		lru.currentSize -= node.Body.SizeBytes
267		lru.currentSize += body.SizeBytes
268		node.Body = body
269		lru.ll.MoveToFront(e)
270	} else {
271		node := &Node{
272			Key:    key,
273			Folder: folder,
274			Body:   body,
275		}
276		e := lru.ll.PushFront(node)
277		lru.cache[key] = e
278		lru.currentSize += body.SizeBytes
279	}
280
281	lru.evict()
282
283	_ = saveEmailBodyToDisk(folder, body)
284}
285
286func (lru *LRU) Delete(folder string, uid uint32, accountID string) {
287	lru.mu.Lock()
288	defer lru.mu.Unlock()
289
290	key := lru.makeKey(folder, uid, accountID)
291	lru.removeKey(key)
292	_ = removeBodyFromDisk(folder, uid, accountID)
293}
294
295func resetLRU() {
296	once = sync.Once{}
297	lru = nil
298}