package ui
import (
"bytes"
"fmt"
"io"
"io/fs"
"net/http"
"path"
"strings"
"sync"
"text/template"
"time"
"github.com/gorilla/mux"
"github.com/prometheus/common/server"
)
func RegisterRoutes(pathPrefix string, router *mux.Router) {
if !strings.HasSuffix(pathPrefix, "/") {
pathPrefix = pathPrefix + "/"
}
publicPath := path.Join(pathPrefix, "public")
renderer := &templateRenderer{
pathPrefix: strings.TrimSuffix(pathPrefix, "/"),
inner: Assets(),
contentCache: make(map[string]string),
contentCacheTime: make(map[string]time.Time),
}
router.PathPrefix(publicPath).Handler(http.StripPrefix(publicPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server.StaticFileServer(renderer).ServeHTTP(w, r)
})))
router.HandleFunc(strings.TrimSuffix(pathPrefix, "/"), func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, pathPrefix, http.StatusFound)
})
router.PathPrefix(pathPrefix).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/"
server.StaticFileServer(renderer).ServeHTTP(w, r)
})
}
type templateRenderer struct {
pathPrefix string
inner http.FileSystem
cacheMut sync.RWMutex
contentCache map[string]string
contentCacheTime map[string]time.Time
}
var _ http.FileSystem = (*templateRenderer)(nil)
func (tr *templateRenderer) Open(name string) (http.File, error) {
f, err := tr.inner.Open(name)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, err
}
if fi.IsDir() {
return f, nil
}
defer f.Close()
if ent, ok := tr.getCacheEntry(name, fi, true); ok {
return ent, nil
}
tr.cacheMut.Lock()
defer tr.cacheMut.Unlock()
if ent, ok := tr.getCacheEntry(name, fi, false); ok {
return ent, nil
}
rawBytes, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("reading file for template processing: %w", err)
}
tmpl, err := template.New(name).Delims("{{!", "!}}").Parse(string(rawBytes))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, struct{ PublicURL string }{PublicURL: tr.pathPrefix}); err != nil {
return nil, fmt.Errorf("rendering template: %w", err)
}
tr.contentCache[name] = buf.String()
tr.contentCacheTime[name] = fi.ModTime()
return &readerFile{
Reader: bytes.NewReader(buf.Bytes()),
fi: &infoWithSize{
FileInfo: fi,
size: int64(buf.Len()),
},
}, nil
}
func (tr *templateRenderer) getCacheEntry(name string, fi fs.FileInfo, lock bool) (f http.File, ok bool) {
if lock {
tr.cacheMut.RLock()
defer tr.cacheMut.RUnlock()
}
content, ok := tr.contentCache[name]
if !ok {
return nil, false
}
if fi.ModTime().After(tr.contentCacheTime[name]) {
return nil, false
}
return &readerFile{
Reader: strings.NewReader(content),
fi: &infoWithSize{
FileInfo: fi,
size: int64(len(content)),
},
}, true
}
type readerFile struct {
io.Reader
fi fs.FileInfo
}
var _ http.File = (*readerFile)(nil)
func (rf *readerFile) Stat() (fs.FileInfo, error) { return rf.fi, nil }
func (rf *readerFile) Close() error {
return nil
}
func (rf *readerFile) Seek(offset int64, whence int) (int64, error) {
return 0, fmt.Errorf("Seek not implemented")
}
func (rf *readerFile) Readdir(count int) ([]fs.FileInfo, error) {
return nil, fmt.Errorf("Readdir not implemented")
}
type infoWithSize struct {
fs.FileInfo
size int64
}
func (iws *infoWithSize) Size() int64 { return iws.size }