204 lines
5.4 KiB
Go
204 lines
5.4 KiB
Go
package resources
|
|
|
|
import (
|
|
"embed"
|
|
"flag"
|
|
"git.thequux.com/thequux/ipasso/util/startup"
|
|
"github.com/CloudyKit/jet/v6"
|
|
"github.com/julienschmidt/httprouter"
|
|
"go.uber.org/zap"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
dumpResources = flag.Bool("extract-resources", false, "Extract resources to directory indicated by --external-resources")
|
|
externalResources = flag.String("external-resources", "", "Directory in which to find external resources")
|
|
)
|
|
|
|
//go:embed static templates
|
|
var embedded embed.FS
|
|
|
|
var Resources fs.FS = &embedded
|
|
|
|
var l *zap.Logger
|
|
var Templates TemplateSource
|
|
var StaticFiles fs.FS
|
|
|
|
func init() {
|
|
|
|
startup.Logger.Add(func() {
|
|
l = zap.L().Named("resources")
|
|
})
|
|
startup.PostFlags.Add(func() {
|
|
var err error
|
|
if *dumpResources {
|
|
if *externalResources == "" {
|
|
l.Fatal("Cannot extract resources without --external-resources")
|
|
}
|
|
extractResources(*externalResources)
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *externalResources != "" {
|
|
Resources = os.DirFS(*externalResources)
|
|
}
|
|
|
|
fs.WalkDir(Resources, ".", func(path string, d fs.DirEntry, err error) error {
|
|
l.Debug("found static file", zap.String("path", path))
|
|
return nil
|
|
})
|
|
|
|
templateFS, err := fs.Sub(Resources, "templates")
|
|
if err != nil {
|
|
l.Fatal("Unable to find templates")
|
|
}
|
|
|
|
Templates = loadTemplates(templateFS, *externalResources != "")
|
|
StaticFiles, err = fs.Sub(Resources, "static")
|
|
if err != nil {
|
|
l.Fatal("Cannot find static files", zap.Error(err))
|
|
}
|
|
})
|
|
|
|
startup.Routes.Add(func(router *httprouter.Router) {
|
|
router.NotFound = http.FileServer(http.FS(StaticFiles))
|
|
})
|
|
}
|
|
|
|
func extractResources(dst string) {
|
|
if err := os.MkdirAll(dst, 0755); err != nil {
|
|
l.Fatal("Failed to create external resource directory", zap.Error(err))
|
|
}
|
|
_ = fs.WalkDir(embedded, "/", func(filePath string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
l.Warn("Failed to walk file", zap.String("path", filePath), zap.Error(err))
|
|
return nil
|
|
}
|
|
fullPath := path.Join(dst, filePath)
|
|
if d.IsDir() {
|
|
if err = os.Mkdir(fullPath, 0755); err != nil {
|
|
l.Warn("Failed to extract directory", zap.String("dst", fullPath), zap.Error(err))
|
|
return fs.SkipDir
|
|
}
|
|
} else {
|
|
data, err := fs.ReadFile(embedded, filePath)
|
|
if err == nil {
|
|
err = os.WriteFile(fullPath, data, 0644)
|
|
}
|
|
if err != nil {
|
|
l.Warn("Failed to copy file", zap.String("dst", fullPath), zap.Error(err))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
type AvailableTemplates int
|
|
|
|
const (
|
|
ATHtml AvailableTemplates = 1 << iota
|
|
ATText
|
|
ATComboCount
|
|
)
|
|
|
|
type TemplateSource struct {
|
|
htmlCache *jet.Set
|
|
textCache *jet.Set
|
|
isDevMode bool
|
|
availableContentTypes map[string]AvailableTemplates
|
|
}
|
|
|
|
func loadTemplates(src fs.FS, devMode bool) TemplateSource {
|
|
var devModeOpt jet.Option
|
|
if devMode {
|
|
devModeOpt = jet.InDevelopmentMode()
|
|
} else {
|
|
devModeOpt = func(set *jet.Set) {}
|
|
}
|
|
|
|
loader := genFsLoader{fs: src}
|
|
res := TemplateSource{
|
|
isDevMode: devMode,
|
|
htmlCache: jet.NewSet(loader, devModeOpt, jet.WithTemplateNameExtensions([]string{".html", ".html.jet"})),
|
|
textCache: jet.NewSet(loader, devModeOpt, jet.WithTemplateNameExtensions([]string{".txt", ".txt.jet"}), jet.WithSafeWriter(nil)),
|
|
availableContentTypes: make(map[string]AvailableTemplates),
|
|
}
|
|
|
|
res.htmlCache.AddGlobal("TemplateMode", "html")
|
|
res.textCache.AddGlobal("TemplateMode", "text")
|
|
|
|
return res
|
|
}
|
|
|
|
// Add a global variable available to all templates
|
|
func (ts *TemplateSource) AddGlobalVar(name string, value interface{}) {
|
|
ts.textCache.AddGlobal(name, value)
|
|
ts.htmlCache.AddGlobal(name, value)
|
|
}
|
|
|
|
// Add a global function available to HTML templates
|
|
func (ts *TemplateSource) AddHtmlFunction(name string, value func(arguments jet.Arguments) reflect.Value) {
|
|
ts.htmlCache.AddGlobalFunc(name, value)
|
|
}
|
|
|
|
// Adds a global function available to text templates
|
|
func (ts *TemplateSource) AddTextFunction(name string, value func(arguments jet.Arguments) reflect.Value) {
|
|
ts.htmlCache.AddGlobalFunc(name, value)
|
|
}
|
|
|
|
// Adds a global function available to all templates. Equivalent to calling AddHtmlFunction and AddTextFunction
|
|
func (ts *TemplateSource) AddFunction(name string, value func(arguments jet.Arguments) reflect.Value) {
|
|
ts.AddHtmlFunction(name, value)
|
|
ts.AddTextFunction(name, value)
|
|
}
|
|
|
|
func (ts *TemplateSource) GetContentTypes(name string) AvailableTemplates {
|
|
if result, ok := ts.availableContentTypes[name]; ok {
|
|
return result
|
|
}
|
|
var result AvailableTemplates
|
|
if _, err := ts.HtmlTemplate(name); err == nil {
|
|
result = result | ATHtml
|
|
}
|
|
if _, err := ts.TextTemplate(name); err == nil {
|
|
result = result | ATText
|
|
}
|
|
if !ts.isDevMode {
|
|
ts.availableContentTypes[name] = result
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (ts *TemplateSource) HtmlTemplate(name string) (*jet.Template, error) {
|
|
return ts.htmlCache.GetTemplate(name)
|
|
}
|
|
|
|
func (ts *TemplateSource) TextTemplate(name string) (*jet.Template, error) {
|
|
return ts.textCache.GetTemplate(name)
|
|
}
|
|
|
|
type genFsLoader struct {
|
|
fs fs.FS
|
|
}
|
|
|
|
func (g genFsLoader) Exists(templatePath string) bool {
|
|
templatePath = strings.TrimPrefix(templatePath, "/")
|
|
l.Debug("Probe", zap.String("path", templatePath))
|
|
v, err := g.fs.Open(templatePath)
|
|
if err == nil {
|
|
_ = v.Close()
|
|
}
|
|
return err == nil
|
|
}
|
|
|
|
func (g genFsLoader) Open(templatePath string) (io.ReadCloser, error) {
|
|
templatePath = strings.TrimPrefix(templatePath, "/")
|
|
return g.fs.Open(templatePath)
|
|
}
|