Add ability to load templates live from the filesystem

See config.go.example.
This commit is contained in:
Ben Visness 2023-04-08 11:14:44 -05:00
parent 3a66b7a77d
commit 524cf8e27b
6 changed files with 115 additions and 18 deletions

View File

@ -82,4 +82,7 @@ var Config = HMNConfig{
CineraOutputPath: "./annotations/",
Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"},
},
DevConfig: DevConfig{
LiveTemplates: true,
},
}

View File

@ -29,6 +29,7 @@ type HMNConfig struct {
Discord DiscordConfig
Twitch TwitchConfig
EpisodeGuide EpisodeGuide
DevConfig DevConfig
}
type PostgresConfig struct {
@ -101,6 +102,10 @@ type AdminConfig struct {
AtomPassword string
}
type DevConfig struct {
LiveTemplates bool // load templates live from the filesystem instead of embedding them
}
func init() {
if Config.EpisodeGuide.Projects == nil {
Config.EpisodeGuide.Projects = make(map[string]string)

View File

@ -99,10 +99,7 @@ func IsEmail(address string) bool {
func renderTemplate(name string, data interface{}) (string, error) {
var buffer bytes.Buffer
template, hasTemplate := templates.Templates[name]
if !hasTemplate {
return "", oops.New(nil, "Template not found: %s", name)
}
template := templates.GetTemplate(name)
err := template.Execute(&buffer, data)
if err != nil {
return "", oops.New(err, "Failed to render template for email")

View File

@ -4,13 +4,16 @@ import (
"embed"
"fmt"
"html/template"
"io/fs"
"regexp"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/Masterminds/sprig"
"github.com/google/uuid"
@ -25,22 +28,22 @@ const (
)
//go:embed src
var templateFs embed.FS
var Templates map[string]*template.Template
var embeddedTemplateFs embed.FS
var embeddedTemplates map[string]*template.Template
//go:embed src/fishbowls
var FishbowlFS embed.FS
func Init() {
Templates = make(map[string]*template.Template)
func getTemplatesFromFS(templateFS fs.ReadDirFS) map[string]*template.Template {
templates := make(map[string]*template.Template)
files := utils.Must1(templateFs.ReadDir("src"))
files := utils.Must1(templateFS.ReadDir("src"))
for _, f := range files {
if hasSuffix(f.Name(), ".html") {
t := template.New(f.Name())
t = t.Funcs(sprig.FuncMap())
t = t.Funcs(HMNTemplateFuncs)
t, err := t.ParseFS(templateFs,
t, err := t.ParseFS(templateFS,
"src/layouts/*",
"src/include/*",
"src/"+f.Name(),
@ -49,19 +52,40 @@ func Init() {
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
}
Templates[f.Name()] = t
templates[f.Name()] = t
} else if hasSuffix(f.Name(), ".css", ".js", ".xml") {
t := template.New(f.Name())
t = t.Funcs(sprig.FuncMap())
t = t.Funcs(HMNTemplateFuncs)
t, err := t.ParseFS(templateFs, "src/"+f.Name())
t, err := t.ParseFS(templateFS, "src/"+f.Name())
if err != nil {
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
}
Templates[f.Name()] = t
templates[f.Name()] = t
}
}
return templates
}
func Init() {
embeddedTemplates = getTemplatesFromFS(embeddedTemplateFs)
}
func GetTemplate(name string) *template.Template {
var templates map[string]*template.Template
if config.Config.DevConfig.LiveTemplates {
templates = getTemplatesFromFS(utils.DirFS("src/templates").(fs.ReadDirFS))
} else {
templates = embeddedTemplates
}
template, hasTemplate := templates[name]
if !hasTemplate {
panic(oops.New(nil, "Template not found: %s", name))
}
return template
}
func hasSuffix(s string, suffixes ...string) bool {

72
src/utils/dirfs.go Normal file
View File

@ -0,0 +1,72 @@
package utils
import (
"io/fs"
"os"
"runtime"
)
// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir.
//
// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the
// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the
// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside
// the /prefix tree, then using DirFS does not stop the access any more than using
// os.Open does. Additionally, the root of the fs.FS returned for a relative path,
// DirFS("prefix"), will be affected by later calls to Chdir. DirFS is therefore not
// a general substitute for a chroot-style security mechanism when the directory tree
// contains arbitrary content.
//
// The result implements fs.StatFS AND fs.ReadDirFS because god dammit why not.
func DirFS(dir string) fs.FS {
return dirFS(dir)
}
func containsAny(s, chars string) bool {
for i := 0; i < len(s); i++ {
for j := 0; j < len(chars); j++ {
if s[i] == chars[j] {
return true
}
}
}
return false
}
type dirFS string
var _ fs.StatFS = dirFS("")
var _ fs.ReadDirFS = dirFS("")
func (dir dirFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrInvalid}
}
f, err := os.Open(string(dir) + "/" + name)
if err != nil {
return nil, err // nil fs.File
}
return f, nil
}
func (dir dirFS) Stat(name string) (fs.FileInfo, error) {
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
return nil, &os.PathError{Op: "stat", Path: name, Err: os.ErrInvalid}
}
f, err := os.Stat(string(dir) + "/" + name)
if err != nil {
return nil, err
}
return f, nil
}
func (dir dirFS) ReadDir(name string) ([]fs.DirEntry, error) {
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrInvalid}
}
d, err := os.ReadDir(string(dir) + "/" + name)
if err != nil {
return nil, err
}
return d, nil
}

View File

@ -444,11 +444,7 @@ func (rd *ResponseData) WriteTemplate(name string, data interface{}, rp *perf.Re
rp.StartBlock("TEMPLATE", name)
defer rp.EndBlock()
}
template, hasTemplate := templates.Templates[name]
if !hasTemplate {
panic(oops.New(nil, "Template not found: %s", name))
}
return template.Execute(rd, data)
return templates.GetTemplate(name).Execute(rd, data)
}
func (rd *ResponseData) MustWriteTemplate(name string, data interface{}, rp *perf.RequestPerf) {