package vinegarUtil import ( "errors" "fmt" "strings" "time" ) const ( DefaultCacheTimeInMinutes = 15 ) type ( lru map[string]*LruEntry Lru struct { entries *lru limit int64 currentSize int64 } LruEntry struct { path string Content []byte CompressedContent []byte created time.Time expires time.Time mostRecentAccess time.Time totalSize int64 MimeType string } SingleFileCache struct { path string entry *LruEntry } Cache interface { Get(key string) (*LruEntry, bool) GetFresh(key string) (*LruEntry, bool) Put(key string, pathlike string) error Remove(key string) } ) func NewLRU(size int64) *Lru { cLru := make(lru) return &Lru{&cLru, size, 0} } func NewSingleFileCache(pathlike string) *SingleFileCache { entry := newLRUEntry(pathlike) sfc := SingleFileCache{path: pathlike, entry: entry} return &sfc } func (l *Lru) Get(key string) (*LruEntry, bool) { entry, exists := (*l.entries)[key] if exists && entry.expires.Before(time.Now()) { fmt.Println("Cache miss due to expired content") l.Remove(key) return nil, false } else { if exists { entry.mostRecentAccess = time.Now() } return entry, exists } } func (l *Lru) GetFresh(key string) (*LruEntry, bool) { l.Remove(key) return l.Get(key) } func (l *Lru) Put(key string, pathlike string) error { entry, exists := (*l.entries)[key] var size int64 if exists { content, fExists := GetDiskContent(pathlike) if !fExists { return errors.New("attempted to refresh cache with file that no longer exists on disk") } zippedContent := *GZipBytes(content) size = int64(len(*content)) + int64(len(zippedContent)) l.ensureVacancy(size) entry.Content = *content entry.CompressedContent = zippedContent entry.created = time.Now() entry.expires = time.Now().Add(DefaultCacheTimeInMinutes * time.Minute) entry.mostRecentAccess = time.Now() entry.totalSize = size } else { entry = newLRUEntry(pathlike) } (*l.entries)[key] = entry l.recalcSize() return nil } func (l *Lru) Remove(key string) { entry, exists := (*l.entries)[key] if exists { size := entry.totalSize delete(*l.entries, key) l.currentSize = l.currentSize - size } } func (l *Lru) ensureVacancy(required int64) { if l.currentSize == 0 { return } for l.currentSize+required > l.limit { leastKey := l.getLeastRecentlyUsedKey() l.Remove(leastKey) } } func (l *Lru) getLeastRecentlyUsedKey() string { started := false var benchmark *LruEntry leastUsedKey := "" for key, entry := range *l.entries { if !started { started = true benchmark = entry leastUsedKey = key continue } if benchmark.mostRecentAccess.After(entry.mostRecentAccess) { benchmark = entry } } if leastUsedKey == "" { panic("Invalid key for LRU: [" + leastUsedKey + "]") } return leastUsedKey } func (l *Lru) recalcSize() { var total int64 for _, entry := range *l.entries { total += entry.totalSize } l.currentSize = total } func (sfc *SingleFileCache) Get(key string) (*LruEntry, bool) { return sfc.entry, true } func (sfc *SingleFileCache) GetFresh(key string) (*LruEntry, bool) { entry := newLRUEntry(sfc.entry.path) sfc.entry = entry return entry, true } // Put THIS WILL DELETE ANYTHING YOU HAVE STORED IN THIS CACHE. func (sfc *SingleFileCache) Put(key string, pathlike string) error { //there's a bug in this. we don't return an error from newLRUEntry, so if the file disappears, the server will die entry := newLRUEntry(pathlike) sfc.entry = entry return nil } func (sfc *SingleFileCache) Remove(key string) { //i'm actually not sure what to do here. why would you ever remove from a single-file cache? } func newLRUEntry(pathlike string) *LruEntry { bits, exists := GetDiskContent(pathlike) if !exists { panic("Could not load single file for single file path: " + pathlike) } compressedBits := GZipBytes(bits) size := int64(len(*bits)) + int64(len(*compressedBits)) entry := LruEntry{ path: pathlike, Content: *bits, CompressedContent: *compressedBits, created: time.Now(), expires: time.Now().Add(DefaultCacheTimeInMinutes * time.Minute), mostRecentAccess: time.Now(), totalSize: size, MimeType: GuessMimetype(pathlike), } return &entry } func GuessMimetype(filePath string) string { dotIndex := strings.LastIndex(filePath, ".") ext := filePath[dotIndex+1:] switch ext { case "htm": case "html": return "text/html" case "ico": return "image/vnd.microsoft.icon" case "jpg": case "jpeg": return "image/jpeg" case "png": return "image/png" case "svg": return "image/svg+xml" case "css": return "text/css" default: return "application/octet-stream" } return "application/octet-stream" }