243 lines
5.4 KiB
Go
243 lines
5.4 KiB
Go
package vinegarUtil
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"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")
|
|
entry.Reload()
|
|
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 {
|
|
entry.Reload()
|
|
}
|
|
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 (le *LruEntry) Reload() {
|
|
content, _ := GetDiskContent(le.path)
|
|
le.Content = *content
|
|
le.CompressedContent = *GZipBytes(content)
|
|
le.created = time.Now()
|
|
le.mostRecentAccess = le.created
|
|
le.expires = le.created.Add(time.Minute * DefaultCacheTimeInMinutes)
|
|
}
|
|
|
|
func (sfc *SingleFileCache) Get(key string) (*LruEntry, bool) {
|
|
return sfc.entry, true
|
|
}
|
|
|
|
func (sfc *SingleFileCache) GetFresh(key string) (*LruEntry, bool) {
|
|
sfc.entry.Reload()
|
|
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 := 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 "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"
|
|
}
|