212 lines
4.3 KiB
Go
212 lines
4.3 KiB
Go
package core
|
|
|
|
import (
|
|
"bytes"
|
|
gzip2 "compress/gzip"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultCacheTimeInMinutes = 15
|
|
ContentTypeHeaderKey = "Content-Type"
|
|
ContentEncodingHeaderKey = "Content-Encoding"
|
|
)
|
|
|
|
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) {
|
|
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("cache 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"
|
|
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
|
|
}
|
|
}
|