2023-12-01 13:44:58 +01:00
|
|
|
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
|
2023-12-02 01:01:16 +01:00
|
|
|
//go:embed icons/*.svg
|
2023-12-02 01:25:05 +01:00
|
|
|
//go:embed icons/*.png
|
2023-12-01 13:44:58 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|