templating.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package render
  2. import (
  3. "fmt"
  4. "html/template"
  5. "net/http"
  6. "path/filepath"
  7. "runtime/debug"
  8. "sync"
  9. "time"
  10. "github.com/go-kit/kit/log"
  11. "github.com/go-kit/kit/log/level"
  12. "github.com/oxtoacart/bpool"
  13. "github.com/shurcooL/httpfs/html/vfstemplate"
  14. "go.mindeco.de/logging"
  15. )
  16. type Renderer struct {
  17. assets http.FileSystem
  18. log log.Logger
  19. // files
  20. templateFiles []string
  21. baseTemplates []string
  22. errorTemplate string
  23. funcMap template.FuncMap
  24. tplFuncInjectors map[string]FuncInjector
  25. // bufpool is shared between all render() calls
  26. bufpool *bpool.BufferPool
  27. doReload bool // Reload is whether to reload templates on each request.
  28. mu sync.RWMutex // protect concurrent map access
  29. reloading bool
  30. templates map[string]*template.Template
  31. }
  32. // New creates a new Renderer
  33. func New(fs http.FileSystem, opts ...Option) (*Renderer, error) {
  34. r := &Renderer{
  35. assets: fs,
  36. bufpool: bpool.NewBufferPool(64),
  37. templates: make(map[string]*template.Template),
  38. tplFuncInjectors: make(map[string]FuncInjector),
  39. }
  40. for i, o := range opts {
  41. if err := o(r); err != nil {
  42. return nil, fmt.Errorf("render: option %d failed: %w", i, err)
  43. }
  44. }
  45. // todo defaults
  46. if r.log == nil {
  47. r.log = logging.Logger("render")
  48. }
  49. if len(r.baseTemplates) == 0 {
  50. r.baseTemplates = []string{"base.tmpl"}
  51. }
  52. if r.errorTemplate == "" {
  53. r.errorTemplate = "/error.tmpl"
  54. }
  55. return r, r.parseHTMLTemplates()
  56. }
  57. func (r *Renderer) GetReloader() func(http.Handler) http.Handler {
  58. r.doReload = true
  59. return func(next http.Handler) http.Handler {
  60. return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  61. if err := r.Reload(); err != nil {
  62. level.Error(r.log).Log("event", "reload failed", "err", err)
  63. err = fmt.Errorf("render: could not reload templates: %w", err)
  64. r.Error(rw, req, http.StatusInternalServerError, err)
  65. return
  66. }
  67. next.ServeHTTP(rw, req)
  68. })
  69. }
  70. }
  71. func (r *Renderer) Reload() error {
  72. if r.doReload {
  73. r.mu.RLock()
  74. if r.reloading {
  75. r.mu.RUnlock()
  76. return nil
  77. }
  78. r.mu.RUnlock()
  79. return r.parseHTMLTemplates()
  80. }
  81. return nil
  82. }
  83. type RenderFunc func(w http.ResponseWriter, req *http.Request) (interface{}, error)
  84. func (r *Renderer) HTML(name string, f RenderFunc) http.HandlerFunc {
  85. return func(w http.ResponseWriter, req *http.Request) {
  86. data, err := f(w, req)
  87. if err != nil {
  88. level.Error(r.log).Log("event", "handler failed", "err", err)
  89. r.Error(w, req, http.StatusInternalServerError, err)
  90. return
  91. }
  92. w.Header().Set("Content-Type", "text/html")
  93. if err := r.Render(w, req, name, http.StatusOK, data); err != nil {
  94. level.Error(r.log).Log("event", "HTML render failed", "err", err)
  95. r.Error(w, req, http.StatusInternalServerError, err)
  96. return
  97. }
  98. }
  99. }
  100. func (r *Renderer) StaticHTML(name string) http.Handler {
  101. return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
  102. err := r.Render(w, req, name, http.StatusOK, nil)
  103. if err != nil {
  104. level.Error(r.log).Log("msg", "static HTML failed", "err", err)
  105. r.Error(w, req, http.StatusInternalServerError, err)
  106. }
  107. })
  108. }
  109. func (r *Renderer) Render(w http.ResponseWriter, req *http.Request, name string, status int, data interface{}) error {
  110. r.mu.RLock()
  111. defer r.mu.RUnlock()
  112. t, ok := r.templates[name]
  113. if !ok {
  114. return fmt.Errorf("render: could not find template: %s", name)
  115. }
  116. // create request scoped functions
  117. var scopedFuncs = make(template.FuncMap, len(r.tplFuncInjectors))
  118. for name, fn := range r.tplFuncInjectors {
  119. scopedFuncs[name] = fn(req)
  120. }
  121. // need to clone the template to not bork it for future requests
  122. scopedTpl, err := t.Clone()
  123. if err != nil {
  124. return err
  125. }
  126. // assign the scoped functions
  127. scopedTpl = scopedTpl.Funcs(scopedFuncs)
  128. start := time.Now()
  129. buf := r.bufpool.Get()
  130. err = scopedTpl.ExecuteTemplate(buf, filepath.Base(r.baseTemplates[0]), data)
  131. if err != nil {
  132. return fmt.Errorf("render: template(%s) execution failed: %w", name, err)
  133. }
  134. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  135. w.WriteHeader(status)
  136. sz := buf.Len()
  137. _, err = buf.WriteTo(w)
  138. r.bufpool.Put(buf)
  139. level.Debug(r.log).Log("event", "rendered",
  140. "tpl", name,
  141. "status", status,
  142. "took", time.Since(start),
  143. "size", sz,
  144. )
  145. return err
  146. }
  147. func (r *Renderer) Error(w http.ResponseWriter, req *http.Request, status int, err error) {
  148. r.logError(req, err, nil)
  149. w.Header().Set("cache-control", "no-cache")
  150. err2 := r.Render(w, req, r.errorTemplate, status, map[string]interface{}{
  151. "StatusCode": status,
  152. "Status": http.StatusText(status),
  153. "Err": err,
  154. })
  155. if err2 != nil {
  156. err2 = fmt.Errorf("render: during execution of error template: %w", err2)
  157. err = fmt.Errorf("meant to return %s but ran into %w", err, err2)
  158. r.logError(req, err, nil)
  159. http.Error(w, err.Error(), http.StatusInternalServerError)
  160. }
  161. }
  162. func (r *Renderer) parseHTMLTemplates() error {
  163. r.mu.Lock()
  164. defer r.mu.Unlock()
  165. r.reloading = true
  166. parseFuncs := make(template.FuncMap, len(r.funcMap)+len(r.tplFuncInjectors))
  167. for k, v := range r.funcMap {
  168. parseFuncs[k] = v
  169. }
  170. // these are just placeholders so that the functions are not undefined.
  171. // they are repaced in Render() after the template is cloned.
  172. for k, _ := range r.tplFuncInjectors {
  173. parseFuncs[k] = func(...interface{}) string { return k }
  174. }
  175. funcTpl := template.New("").Funcs(parseFuncs)
  176. for _, tf := range r.templateFiles {
  177. ftc, err := funcTpl.Clone()
  178. if err != nil {
  179. return fmt.Errorf("render: could not clone func template: %w", err)
  180. }
  181. t, err := vfstemplate.ParseFiles(r.assets, ftc, append(r.baseTemplates, tf)...)
  182. if err != nil {
  183. return fmt.Errorf("render: failed to parse template %s: %w", tf, err)
  184. }
  185. r.templates[tf] = t
  186. }
  187. r.reloading = false
  188. return nil
  189. }
  190. func (r *Renderer) logError(req *http.Request, err error, rv interface{}) {
  191. if err != nil {
  192. buf := r.bufpool.Get()
  193. fmt.Fprintf(buf, "Error serving %s: %s", req.URL, err)
  194. if rv != nil {
  195. fmt.Fprintln(buf, rv)
  196. buf.Write(debug.Stack())
  197. }
  198. level.Error(r.log).Log("event", "logError", "err", err)
  199. r.bufpool.Put(buf)
  200. }
  201. }