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" "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 TemplateSource struct { htmlCache *jet.Set textCache *jet.Set } 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} return TemplateSource{ htmlCache: jet.NewSet(loader, devModeOpt), textCache: jet.NewSet(loader, devModeOpt, jet.WithSafeWriter(nil)), } } 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) }