LocalhostBunny/webassets/website.go

225 lines
3.8 KiB
Go

package webassets
import (
"embed"
"fmt"
"github.com/rs/zerolog/log"
"io"
bunny "locbunny"
"os"
"path/filepath"
"sync"
"time"
)
//go:embed *.html
//go:embed *.ico
//go:embed scripts/*.js
//go:embed css/*.css
//go:embed fonts/*.woff
//go:embed fonts/*.woff2
//go:embed icons/*.svg
//go:embed icons/*.png
var _assets embed.FS
type templateCacheEntry struct {
MDate time.Time
Value ITemplate
}
type fileCacheEntry struct {
MDate time.Time
Value []byte
}
type Assets struct {
templateCache map[string]templateCacheEntry
fileCache map[string]fileCacheEntry
lock sync.RWMutex
}
func NewAssets() *Assets {
return &Assets{
templateCache: make(map[string]templateCacheEntry, 128),
fileCache: make(map[string]fileCacheEntry, 128),
lock: sync.RWMutex{},
}
}
type ITemplate interface {
Execute(wr io.Writer, data any) error
}
func (a *Assets) ListAssets() []string {
result := make([]string, 0)
entries, err := _assets.ReadDir(".")
if err != nil {
panic(err)
}
for _, entry := range entries {
if entry.IsDir() {
panic("TODO implement recursion")
}
if entry.Name() == "index.html" {
continue
}
result = append(result, "/"+entry.Name())
}
return result
}
func (a *Assets) Read(fp string) ([]byte, error) {
if bunny.Conf.LiveReload == nil {
// no live-reload: use embedded data
bin, err := _assets.ReadFile(fp)
if err != nil {
return nil, err
}
return bin, nil
} else {
liveFP := filepath.Join(*bunny.Conf.LiveReload, fp)
stat, err := os.Stat(liveFP)
if err != nil {
return nil, err
}
a.lock.RLock()
v, ok := a.fileCache[fp]
a.lock.RUnlock()
if !ok {
// initial load
bin, err := os.ReadFile(liveFP)
if err != nil {
return nil, err
}
a.lock.Lock()
a.fileCache[fp] = fileCacheEntry{MDate: stat.ModTime(), Value: bin}
a.lock.Unlock()
return bin, nil
} else if v.MDate != stat.ModTime() {
// live reload
log.Info().Msg(fmt.Sprintf("[>>] Live reload file '%s' from filesystem (file changed)", fp))
bin, err := os.ReadFile(liveFP)
if err != nil {
return nil, err
}
a.lock.Lock()
a.fileCache[fp] = fileCacheEntry{MDate: stat.ModTime(), Value: bin}
a.lock.Unlock()
return bin, nil
} else {
// return from cache
return v.Value, nil
}
}
}
func (a *Assets) Template(fp string, builder func([]byte) (ITemplate, error)) (ITemplate, error) {
if bunny.Conf.LiveReload == nil {
// no live-reload: use embedded data, and permanently cache template
a.lock.RLock()
v, ok := a.templateCache[fp]
a.lock.RUnlock()
if ok {
return v.Value, nil
}
bin, err := _assets.ReadFile(fp)
if err != nil {
return nil, err
}
t, err := builder(bin)
if err != nil {
panic(err)
}
a.lock.Lock()
a.templateCache[fp] = templateCacheEntry{MDate: time.Now(), Value: t}
a.lock.Unlock()
return t, nil
} else {
a.lock.RLock()
v, ok := a.templateCache[fp]
a.lock.RUnlock()
liveFP := filepath.Join(*bunny.Conf.LiveReload, fp)
stat, err := os.Stat(liveFP)
if err != nil {
return nil, err
}
if !ok {
// initial load
bin, err := os.ReadFile(liveFP)
if err != nil {
return nil, err
}
t, err := builder(bin)
if err != nil {
return nil, err
}
a.lock.Lock()
a.templateCache[fp] = templateCacheEntry{MDate: stat.ModTime(), Value: t}
a.lock.Unlock()
return t, nil
} else if v.MDate != stat.ModTime() {
// live reload
log.Info().Msg(fmt.Sprintf("[>>] Live reload file '%s' from filesystem (file changed)", fp))
bin, err := os.ReadFile(liveFP)
if err != nil {
return nil, err
}
t, err := builder(bin)
if err != nil {
return nil, err
}
a.lock.Lock()
a.templateCache[fp] = templateCacheEntry{MDate: stat.ModTime(), Value: t}
a.lock.Unlock()
return t, nil
} else {
// return from cache
return v.Value, nil
}
}
}