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