vinegar/cacheutil/WebLRU.go

219 lines
4.3 KiB
Go

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
}
}