Use static assets for HTML templates
authorJan Dittberner <jandd@cacert.org>
Thu, 29 Mar 2018 19:26:12 +0000 (21:26 +0200)
committerJan Dittberner <jandd@cacert.org>
Thu, 29 Mar 2018 19:26:12 +0000 (21:26 +0200)
- implement custom http.Filesystem boardvoting.AssetFS
- replace "footer" and "header" with "footer.html" and "header.html"
- change renderTemplate to use Assets
- use boardvoting.GetAssetFS() with http.Fileserver

13 files changed:
boardvoting.go
boardvoting/main.go
boardvoting/templates/create_motion_form.html
boardvoting/templates/denied.html
boardvoting/templates/direct_vote_form.html
boardvoting/templates/edit_motion_form.html
boardvoting/templates/footer.html
boardvoting/templates/header.html
boardvoting/templates/motion.html
boardvoting/templates/motion_fragments.html
boardvoting/templates/motions.html
boardvoting/templates/proxy_vote_form.html
boardvoting/templates/withdraw_motion_form.html

index c1fa28f..a0d6359 100644 (file)
@@ -10,7 +10,7 @@ import (
        "encoding/pem"
        "flag"
        "fmt"
-
+       "git.cacert.org/cacert-boardvoting/boardvoting"
        "github.com/Masterminds/sprig"
        "github.com/gorilla/sessions"
        _ "github.com/mattn/go-sqlite3"
@@ -35,21 +35,31 @@ var log *logging.Logger
 
 const sessionCookieName = "votesession"
 
-func getTemplateFilenames(templates []string) (result []string) {
-       result = make([]string, len(templates))
-       for i := range templates {
-               result[i] = fmt.Sprintf("templates/%s", templates[i])
-       }
-       return result
-}
-
 func renderTemplate(w http.ResponseWriter, templates []string, context interface{}) {
        funcMaps := sprig.FuncMap()
        funcMaps["nl2br"] = func(text string) template.HTML {
                return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
        }
-       t := template.Must(template.New(templates[0]).Funcs(funcMaps).ParseFiles(getTemplateFilenames(templates)...))
-       if err := t.Execute(w, context); err != nil {
+
+       var baseTemplate *template.Template
+
+       for count, t := range templates {
+               if assetBytes, err := boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+               } else {
+                       if count == 0 {
+                               if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
+                                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                               }
+                       } else {
+                               if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
+                                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                               }
+                       }
+               }
+       }
+
+       if err := baseTemplate.Execute(w, context); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
        }
 }
@@ -733,7 +743,7 @@ func readConfig() {
 }
 
 func setupDbConfig(ctx context.Context) {
-       database, err :=  sql.Open("sqlite3", config.DatabaseFile)
+       database, err := sql.Open("sqlite3", config.DatabaseFile)
        if err != nil {
                log.Panicf("Opening database failed: %v", err)
        }
@@ -777,7 +787,7 @@ func setupHandlers() {
        http.Handle("/newmotion/", motionsHandler{})
        http.Handle("/proxy/", &decisionVoteHandler{})
        http.Handle("/vote/", &decisionVoteHandler{})
-       http.Handle("/static/", http.FileServer(http.Dir(".")))
+       http.Handle("/static/", http.FileServer(boardvoting.GetAssetFS()))
        http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
 }
 
index 5c8e6eb..7cc6005 100644 (file)
@@ -1,3 +1,143 @@
 package boardvoting
 
 //go:generate go-bindata -pkg $GOPACKAGE -o assets.go ./migrations/... ./static/... ./templates/...
+
+import (
+       "bytes"
+       "errors"
+       "io"
+       "io/ioutil"
+       "net/http"
+       "os"
+       "path/filepath"
+       "strings"
+       "time"
+)
+
+var defaultFileTimestamp = time.Now()
+
+type AssetFS struct {
+       Asset     func(path string) ([]byte, error)
+       AssetDir  func(path string) ([]string, error)
+       AssetInfo func(path string) (os.FileInfo, error)
+}
+
+type FakeFile struct {
+       Path      string
+       Dir       bool
+       Len       int64
+       Timestamp time.Time
+}
+
+func (f *FakeFile) Name() string {
+       _, name := filepath.Split(f.Path)
+       return name
+}
+
+func (f *FakeFile) Size() int64 { return f.Len }
+
+func (f *FakeFile) Mode() os.FileMode {
+       mode := os.FileMode(0644)
+       if f.Dir {
+               return mode | os.ModeDir
+       }
+       return mode
+}
+
+func (f *FakeFile) ModTime() time.Time { return f.Timestamp }
+func (f *FakeFile) IsDir() bool        { return f.Dir }
+func (f *FakeFile) Sys() interface{}   { return nil }
+
+type AssetFile struct {
+       *bytes.Reader
+       io.Closer
+       FakeFile
+}
+
+func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
+       if timestamp.IsZero() {
+               timestamp = defaultFileTimestamp
+       }
+       return &AssetFile{
+               bytes.NewReader(content),
+               ioutil.NopCloser(nil),
+               FakeFile{name, false, int64(len(content)), timestamp},
+       }
+}
+
+func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) {
+       return nil, errors.New("not a directory")
+}
+
+func (f *AssetFile) Size() int64 {
+       return f.FakeFile.Size()
+}
+
+func (f *AssetFile) Stat() (os.FileInfo, error) {
+       return f, nil
+}
+
+type AssetDirectory struct {
+       AssetFile
+       ChildrenRead int
+       Children     []os.FileInfo
+}
+
+func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) {
+       if count <= 0 {
+               return f.Children, nil
+       }
+       if f.ChildrenRead+count > len(f.Children) {
+               count = len(f.Children) - f.ChildrenRead
+       }
+       rv := f.Children[f.ChildrenRead : f.ChildrenRead+count]
+       f.ChildrenRead += count
+       return rv, nil
+}
+
+func (f *AssetDirectory) Stat() (os.FileInfo, error) {
+       return f, nil
+}
+
+func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory {
+       fileInfos := make([]os.FileInfo, 0, len(children))
+       for _, child := range children {
+               _, err := fs.AssetDir(filepath.Join(name, child))
+               fileInfos = append(fileInfos, &FakeFile{child, err == nil, 0, time.Time{}})
+       }
+       return &AssetDirectory{
+               AssetFile{
+                       bytes.NewReader(nil),
+                       ioutil.NopCloser(nil),
+                       FakeFile{name, true, 0, time.Time{}},
+               },
+               0,
+               fileInfos}
+}
+
+func (f *AssetFS) Open(name string) (http.File, error) {
+       if len(name) > 0 && name[0] == '/' {
+               name = name[1:]
+       }
+       if b, err := f.Asset(name); err == nil {
+               timestamp := defaultFileTimestamp
+               if f.AssetInfo != nil {
+                       if info, err := f.AssetInfo(name); err == nil {
+                               timestamp = info.ModTime()
+                       }
+               }
+               return NewAssetFile(name, b, timestamp), nil
+       }
+       if children, err := f.AssetDir(name); err == nil {
+               return NewAssetDirectory(name, children, f), nil
+       } else {
+               if strings.Contains(err.Error(), "not found") {
+                       return nil, os.ErrNotExist
+               }
+               return nil, err
+       }
+}
+
+func GetAssetFS() *AssetFS {
+       return &AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo}
+}
index b3cbadb..5958aa7 100644 (file)
@@ -1,4 +1,4 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 <div class="column">
     <div class="ui basic segment">
         <div class="ui floated right secondary menu">
@@ -70,4 +70,4 @@
         </form>
     </div>
 </div>
-{{ template "footer" . }}
\ No newline at end of file
+{{ template "footer.html" . }}
\ No newline at end of file
index 398a36f..53bb364 100644 (file)
@@ -1,7 +1,7 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 <div class="column">
     <div class="ui negative message">
-        <div class="header">You are not authorized to act here!</div>
+        <div class="header.html">You are not authorized to act here!</div>
         <p>If you think this is in error, please contact the administrator.</p>
         <p>If you don't know who that is, it is definitely not an error ;)</p>
         {{ if .Emails }}
@@ -14,4 +14,4 @@
         {{ end }}
     </div>
 </div>
-{{ template "footer" . }}
\ No newline at end of file
+{{ template "footer.html" . }}
\ No newline at end of file
index 861c68a..649c059 100644 (file)
@@ -1,4 +1,4 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 <div class="column">
     <div class="ui basic segment">
         <div class="ui floated right secondary menu">
@@ -24,4 +24,4 @@
         {{ end }}
     </div>
 </form>
-{{ template "footer" . }}
\ No newline at end of file
+{{ template "footer.html" . }}
\ No newline at end of file
index f686183..845442d 100644 (file)
@@ -1,4 +1,4 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 <div class="column">
     <div class="ui floated right secondary menu">
         <a href="/motions/" class="item" title="Show all votes">Back to
@@ -69,4 +69,4 @@
         </form>
     </div>
 </div>
-{{ template "footer" . }}
\ No newline at end of file
+{{ template "footer.html" . }}
\ No newline at end of file
index f2bc089..fa873e5 100644 (file)
@@ -1,4 +1,4 @@
-{{ define "footer" }}
+{{ define "footer.html" }}
 </div>
 <script type="text/javascript">
     $(document).ready(function() {
index 8e0aedc..8215f71 100644 (file)
@@ -1,4 +1,4 @@
-{{ define "header" -}}
+{{ define "header.html" -}}
 <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/html">
 <head>
index 8fc7530..e78a955 100644 (file)
@@ -1,4 +1,4 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 {{ $voter := .Voter }}
 <div class="column">
     <div class="ui basic segment">
@@ -16,4 +16,4 @@
     </div>
 </div>
 {{ end}}
-{{ template "footer" . }}
\ No newline at end of file
+{{ template "footer.html" . }}
\ No newline at end of file
index dbe59c2..96a1938 100644 (file)
@@ -1,7 +1,7 @@
 {{ define "motion_fragment" }}
 <span class="ui {{ template "status_class" .Status }} ribbon label">{{ .Status|toString|title }}</span>
-<span class="header">{{ .Modified|date "2006-01-02 15:04:05 UTC" }}</span>
-<h3 class="header"><a href="/motions/{{ .Tag }}">{{ .Tag }}: {{ .Title }}</a></h3>
+<span class="header.html">{{ .Modified|date "2006-01-02 15:04:05 UTC" }}</span>
+<h3 class="header.html"><a href="/motions/{{ .Tag }}">{{ .Tag }}: {{ .Title }}</a></h3>
 <p>{{ wrap 76 .Content | nl2br }}</p>
 <table class="ui small definition table">
     <tbody>
index 3f82092..625dc57 100644 (file)
@@ -1,4 +1,4 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 {{ $voter := .Voter }}
 <div class="column">
     <div class="ui basic segment">
@@ -49,4 +49,4 @@
     <p>There are no motions in the system yet.</p>
     {{ end }}
 {{ end }}
-{{ template "footer" . }}
\ No newline at end of file
+{{ template "footer.html" . }}
\ No newline at end of file
index 3bfa9df..3a344c3 100644 (file)
@@ -1,4 +1,4 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 {{ $form := .Form }}
 <div class="column">
     <div class="ui basic segment">
@@ -51,4 +51,4 @@
         </form>
     </div>
 </div>
-{{ template "footer" . }}
\ No newline at end of file
+{{ template "footer.html" . }}
\ No newline at end of file
index c5f0846..87ebb57 100644 (file)
@@ -1,4 +1,4 @@
-{{ template "header" . }}
+{{ template "header.html" . }}
 <div class="column">
     <div class="ui basic segment">
         <div class="ui floated right secondary menu">
@@ -18,4 +18,4 @@
         <button class="ui primary left labeled icon button" type="submit"><i class="trash icon"></i> Withdraw</button>
     </div>
 </form>
-{{ template "footer" . }}
+{{ template "footer.html" . }}