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) *Node {
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	return node
236}
237
238func (lru *LRU) removeKey(key string) {
239	if e, ok := lru.cache[key]; ok {
240		node := e.Value.(*Node)
241
242		lru.currentSize -= node.Body.SizeBytes
243		lru.ll.Remove(e)
244		delete(lru.cache, key)
245	}
246}
247
248func (lru *LRU) Put(folder string, uid uint32, accountID string, body *CachedEmailBody) {
249	lru.mu.Lock()
250	defer lru.mu.Unlock()
251
252	key := lru.makeKey(folder, uid, accountID)
253
254	if body.SizeBytes > lru.threshold {
255		lru.removeKey(key)
256		_ = removeBodyFromDisk(folder, uid, accountID)
257		return
258	}
259
260	body.LastAccessedAt = time.Now()
261
262	if e, ok := lru.cache[key]; ok {
263		node := e.Value.(*Node)
264		lru.currentSize -= node.Body.SizeBytes
265		lru.currentSize += body.SizeBytes
266		node.Body = body
267		lru.ll.MoveToFront(e)
268	} else {
269		node := &Node{
270			Key:    key,
271			Folder: folder,
272			Body:   body,
273		}
274		e := lru.ll.PushFront(node)
275		lru.cache[key] = e
276		lru.currentSize += body.SizeBytes
277	}
278
279	lru.evict()
280
281	_ = saveEmailBodyToDisk(folder, body)
282}
283
284func (lru *LRU) Delete(folder string, uid uint32, accountID string) {
285	lru.mu.Lock()
286	defer lru.mu.Unlock()
287
288	key := lru.makeKey(folder, uid, accountID)
289	lru.removeKey(key)
290	_ = removeBodyFromDisk(folder, uid, accountID)
291}
292
293func resetLRU() {
294	once = sync.Once{}
295	lru = nil
296}