Files
ipasso/resources/resources.go
2023-10-26 17:46:58 +02:00

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)
}