vinegar/vinegarUtil/webLRU.go
2023-08-01 12:53:09 -04:00

273 lines
5.6 KiB
Go

package vinegarUtil
import (
"log"
"strings"
"time"
)
const (
DefaultCacheTimeInMinutes = 5
)
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, error) {
entry, err := newLRUEntry(pathlike)
if err != nil {
return nil, err
}
sfc := SingleFileCache{path: pathlike, entry: entry}
return &sfc, nil
}
func (l *Lru) Get(key string) (*LruEntry, bool) {
entry, exists := (*l.entries)[key]
if exists && entry.expires.Before(time.Now()) {
log.Println("Cache miss due to expired content")
err := entry.Reload()
if err != nil {
return nil, false
}
return entry, true
} else {
if exists {
entry.mostRecentAccess = time.Now()
}
return entry, exists
}
}
func (l *Lru) GetFresh(key string) (*LruEntry, bool) {
entry, exists := l.Get(key)
if exists {
err := entry.Reload()
if err != nil {
return nil, false
}
}
return l.Get(key)
}
func (l *Lru) Put(key string, pathlike string) error {
entry, exists := (*l.entries)[key]
var size int64
if exists {
content, err := GetDiskContent(pathlike)
if err != nil {
return err
}
zippedBytes, err := GZipBytes(content)
zippedContent := *zippedBytes
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 {
nEntry, err := newLRUEntry(pathlike)
if err != nil {
return err
}
entry = nEntry
}
(*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
}
}
return leastUsedKey
}
func (l *Lru) recalcSize() {
var total int64
for _, entry := range *l.entries {
total += entry.totalSize
}
l.currentSize = total
}
func (le *LruEntry) Reload() error {
content, err := GetDiskContent(le.path)
if err != nil {
return err
}
zippedBytes, err := GZipBytes(content)
if err != nil {
return err
}
le.Content = *content
le.CompressedContent = *zippedBytes
le.created = time.Now()
le.mostRecentAccess = le.created
le.expires = le.created.Add(time.Minute * DefaultCacheTimeInMinutes)
return nil
}
func (sfc *SingleFileCache) Get(key string) (*LruEntry, bool) {
return sfc.entry, true
}
func (sfc *SingleFileCache) GetFresh(key string) (*LruEntry, bool) {
err := sfc.entry.Reload()
if err != nil {
return nil, false
}
return sfc.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, err := newLRUEntry(pathlike)
if err != nil {
return err
}
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, error) {
bits, err := GetDiskContent(pathlike)
if err != nil {
return nil, err
}
compressedBits, err := GZipBytes(bits)
if err != nil {
return nil, err
}
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, nil
}
func GuessMimetype(filePath string) string {
dotIndex := strings.LastIndex(filePath, ".")
ext := filePath[dotIndex+1:]
switch ext {
case "css":
return "text/css"
case "htm":
case "html":
return "text/html"
case "ico":
return "image/vnd.microsoft.icon"
case "jpg":
fallthrough
case "jpeg":
return "image/jpeg"
case "js":
return "text/javascript"
case "json":
return "application/json"
case "png":
return "image/png"
case "svg":
return "image/svg+xml"
case "webp":
return "image/webp"
default:
log.Default().Printf("[WARN] '%s' is unrecognized MimeType. Returning as [application/octet-stream]. Please use VinegarServlet.AddMimeType() to define the appropriate response for this file extension.", ext)
return "application/octet-stream"
}
return "application/octet-stream"
}