From c5b1e0363ec336d5ea9ac670183d139628002811 Mon Sep 17 00:00:00 2001 From: Naman Sood Date: Fri, 26 Feb 2021 00:12:21 -0500 Subject: [PATCH] server: watch for changes in templates and stylesheets Signed-off-by: Naman Sood --- go.mod | 2 ++ go.sum | 6 ++++ server.go | 106 +++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 93 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 43db5bf..7fea4a1 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.15 require ( github.com/aymerick/raymond v2.0.2+incompatible + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/mitchellh/mapstructure v1.4.1 github.com/wellington/go-libsass v0.9.2 github.com/yuin/goldmark v1.3.1 github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 github.com/yuin/goldmark-meta v1.0.0 golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect + golang.org/x/sys v0.0.0-20210219172841-57ea560cfca1 // indirect ) diff --git a/go.sum b/go.sum index 2de3205..9080052 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -71,7 +73,11 @@ gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9X golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210219172841-57ea560cfca1 h1:mDSj8NPponP6fRpRDblAGl5bpSHjPulHtk5lGl0gLSY= +golang.org/x/sys v0.0.0-20210219172841-57ea560cfca1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/server.go b/server.go index 2a755fa..bf9ebd0 100644 --- a/server.go +++ b/server.go @@ -7,19 +7,19 @@ import ( "net/http" "os" "strings" + "sync" "github.com/aymerick/raymond" + "github.com/fsnotify/fsnotify" "github.com/wellington/go-libsass" ) type server struct { pages []*Page staticHandler http.Handler - pageTpl *raymond.Template - fullPostTpl *raymond.Template - summaryTpl *raymond.Template - errorTpl *raymond.Template - notFoundTpl *raymond.Template + + tplMutex sync.RWMutex + templates map[string]*raymond.Template } func newServer() (*server, error) { @@ -42,6 +42,28 @@ func newServer() (*server, error) { return nil, err } + go listenForChanges("templates/", func(file string) error { + s.tplMutex.Lock() + defer s.tplMutex.Unlock() + tplName := strings.TrimSuffix(file, ".html") + newTpl, err := loadTemplate(tplName) + if err != nil { + return err + } + s.templates[tplName] = newTpl + return nil + }) + + go listenForChanges("styles/", func(file string) error { + var err error + if strings.HasSuffix(file, ".scss") { + err = loadSassStylesheet(file) + } else if strings.HasSuffix(file, ".css") { + err = loadRegularStylesheet(file) + } + return err + }) + return s, nil } @@ -68,33 +90,40 @@ func (s *server) refreshPages() error { return nil } +func loadTemplate(file string) (*raymond.Template, error) { + tpl, err := raymond.ParseFile("templates/" + file + ".html") + if err != nil { + return nil, fmt.Errorf("Could not parse %s template: %w", file, err) + } + log.Printf("Loaded template: %s", file) + return tpl, nil +} + // loadTemplates, for each f in files, loads `templates/$f.html` // as a handlebars HTML template. If any single template fails to // load, only an error is returned. Conversely, if there is no error, // every template name passed is guaranteed to have loaded successfully. -func loadTemplates(files []string) ([]*raymond.Template, error) { - templates := make([]*raymond.Template, 0, len(files)) +func loadTemplates(files []string) (map[string]*raymond.Template, error) { + templates := make(map[string]*raymond.Template) for _, f := range files { - tpl, err := raymond.ParseFile("templates/" + f + ".html") + tpl, err := loadTemplate(f) if err != nil { - return nil, fmt.Errorf("Could not parse %s template: %w", f, err) + return nil, err } - templates = append(templates, tpl) + templates[f] = tpl } log.Printf("Loaded templates: %s", files) return templates, nil } func (s *server) refreshTemplates() error { - templates, err := loadTemplates([]string{"page", "fullpost", "summary", "notfound", "error"}) + s.tplMutex.Lock() + defer s.tplMutex.Unlock() + tpls, err := loadTemplates([]string{"page", "fullpost", "summary", "notfound", "error"}) if err != nil { return err } - s.pageTpl = templates[0] - s.fullPostTpl = templates[1] - s.summaryTpl = templates[2] - s.notFoundTpl = templates[3] - s.errorTpl = templates[4] + s.templates = tpls return nil } @@ -161,17 +190,52 @@ func (s *server) refreshStyles() error { return nil } +func listenForChanges(folder string, action func(string) error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatalf("could not start fsnotify watcher for folder %s", folder) + } + defer watcher.Close() + + done := make(chan bool) + go func() { + defer close(done) + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + log.Printf("Modified file: %s", event.Name) + err := action(strings.TrimPrefix(event.Name, folder)) + if err != nil { + log.Printf("watcher action on %s failed: %v", event.Name, err) + } + } + case err := <-watcher.Errors: + log.Printf("Watcher error: %s", err) + } + } + }() + + err = watcher.Add(folder) + if err != nil { + log.Fatal(err) + } + <-done +} + func (s *server) logRequest(req *http.Request) { log.Printf("%s %s from %s", req.Method, req.URL.Path, req.RemoteAddr) } func (s *server) router(res http.ResponseWriter, req *http.Request) { + s.tplMutex.RLock() + defer s.tplMutex.RUnlock() s.logRequest(req) res = &errorCatcher{ res: res, req: req, - errorTpl: s.errorTpl, - notFoundTpl: s.notFoundTpl, + errorTpl: s.templates["error"], + notFoundTpl: s.templates["notfound"], handledError: false, } slug := req.URL.Path[1:] @@ -202,12 +266,12 @@ func (s *server) createPage(title, contents string) (string, error) { "title": title, "contents": contents, } - return s.pageTpl.Exec(ctx) + return s.templates["page"].Exec(ctx) } func (s *server) renderPage(p *Page, res http.ResponseWriter, req *http.Request) { res.Header().Add("content-type", "text/html") - contents, err := p.render(s.fullPostTpl) + contents, err := p.render(s.templates["fullpost"]) if err != nil { s.errorInRequest(res, req, err) } @@ -224,7 +288,7 @@ func (s *server) homePage(res http.ResponseWriter, req *http.Request) { var posts string for _, p := range s.pages { - summary, err := p.render(s.summaryTpl) + summary, err := p.render(s.templates["summary"]) if err != nil { log.Printf("could not render page summary for %s", p.Slug) }