package cacheutil import ( "bytes" gzip2 "compress/gzip" "fmt" "io/ioutil" "os" "strings" "time" ) const ( DefaultCacheTimeInMinutes = 15 ) var ( UseCache = true ) type ( lru map[string]*LruEntry Lru struct { entries *lru limit int64 currentSize int64 } LruEntry struct { Content []byte CompressedContent []byte created time.Time expires time.Time mostRecentAccess time.Time totalSize int64 MimeType string } SingleFileCache struct { Content *[]byte CompressedContent *[]byte timeCreated time.Time timeExpires time.Time Mimetype string } ) func NewLRU(size int64) *Lru { cLru := make(lru) return &Lru{&cLru, size, 0} } func NewSingleFileCache(pathlike string) *SingleFileCache { bits, exists := GetDiskContent(pathlike) if !exists { panic("Could not load single file for single file path: " + pathlike) } compressedBits := GZipBytes(bits) cache := SingleFileCache{ Content: bits, CompressedContent: compressedBits, timeCreated: time.Now(), timeExpires: time.Now().Add(DefaultCacheTimeInMinutes), Mimetype: GuessMimetype(pathlike), } return &cache } func (l *Lru) Put(key string, content *[]byte) { entry, exists := (*l.entries)[key] zippedContent := *GZipBytes(content) size := int64(len(*content)) + int64(len(zippedContent)) l.ensureVacancy(size) if exists { 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 { mimetype := GuessMimetype(key) entry = &LruEntry{ Content: *content, CompressedContent: zippedContent, created: time.Now(), expires: time.Now().Add(DefaultCacheTimeInMinutes * time.Minute), mostRecentAccess: time.Now(), totalSize: size, MimeType: mimetype, } } (*l.entries)[key] = entry l.currentSize = l.currentSize + size } func (l *Lru) Get(key string) (*LruEntry, bool) { if !UseCache { return nil, false } 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() } else { fmt.Printf("cacheutil miss for '%s'\n", key) } return entry, exists } } 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 GZipBytes(uncompressed *[]byte) *[]byte { buff := bytes.Buffer{} gzip := gzip2.NewWriter(&buff) _, err := gzip.Write(*uncompressed) if err != nil { panic(err) } err = gzip.Flush() if err != nil { panic(err) } compressed := buff.Bytes() return &compressed } 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" } func GetDiskContent(filePath string) (*[]byte, bool) { _, ferr := os.Stat(filePath) if os.IsNotExist(ferr) { return nil, false } else { f, err := os.OpenFile(filePath, os.O_RDONLY, 0755) if err != nil { panic(err) } defer f.Close() content, err := ioutil.ReadAll(f) if err != nil { panic(err) } return &content, true } }