prose/server.go
Naman Sood 048c06ac66 separate template logic into own file
Signed-off-by: Naman Sood <mail@nsood.in>
2021-03-15 23:36:00 -04:00

305 lines
7 KiB
Go

package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"runtime"
"strings"
"sync"
"github.com/aymerick/raymond"
"github.com/rjeczalik/notify"
"github.com/wellington/go-libsass"
)
type server struct {
staticHandler http.Handler
tplMutex sync.RWMutex
templates map[string]*raymond.Template
postsMutex sync.RWMutex
postList
}
func newServer() (*server, error) {
s := &server{
staticHandler: http.FileServer(http.Dir("static/")),
}
posts, err := newPostList()
if err != nil {
return nil, err
}
s.postsMutex.Lock()
s.postList = posts
s.postsMutex.Unlock()
tpls, err := loadTemplates([]string{"page", "fullpost", "summary", "notfound", "error"})
if err != nil {
return nil, err
}
s.tplMutex.Lock()
s.templates = tpls
s.tplMutex.Unlock()
err = s.refreshStyles()
if err != nil {
return nil, err
}
postsLn := newPostListener(func(updateFn func(postList) postList) {
s.postsMutex.Lock()
defer s.postsMutex.Unlock()
s.postList = updateFn(s.postList)
})
go postsLn.listen()
templatesLn := newTemplateListener(func(updateFn func(map[string]*raymond.Template)) {
s.tplMutex.Lock()
defer s.tplMutex.Unlock()
updateFn(s.templates)
})
go templatesLn.listen()
stylesLn := &listener{
folder: "styles/",
update: 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
},
clean: func(file string) error {
var err error
if strings.HasSuffix(file, ".scss") {
err = os.Remove("static/style/" + strings.TrimSuffix(file, ".scss") + ".css")
} else if strings.HasSuffix(file, ".css") {
err = os.Remove("static/style/" + file)
}
return err
},
}
go stylesLn.listen()
return s, nil
}
func loadSassStylesheet(filename string) error {
in, err := os.Open("styles/" + filename)
if err != nil {
return fmt.Errorf("Could not open style infile %s: %w", filename, err)
}
output := strings.TrimSuffix(filename, ".scss") + ".css"
out, err := os.Create("static/css/" + output)
if err != nil {
return fmt.Errorf("Could not open style outfile %s: %w", output, err)
}
comp, err := libsass.New(out, in)
if err != nil {
return fmt.Errorf("Could not start sass compiler for file %s: %w", filename, err)
}
if err = comp.Run(); err != nil {
return fmt.Errorf("Could not generate stylesheet %s: %w", filename, err)
}
return nil
}
func loadRegularStylesheet(filename string) error {
in, err := os.Open("styles/" + filename)
if err != nil {
return fmt.Errorf("Could not open style infile %s: %w", filename, err)
}
out, err := os.Create("static/css/" + filename)
if err != nil {
return fmt.Errorf("Could not open style outfile %s: %w", filename, err)
}
_, err = io.Copy(out, in)
if err != nil {
return fmt.Errorf("Could not copy stylesheet %s: %s", filename, err)
}
return nil
}
func (s *server) refreshStyles() error {
styles, err := os.ReadDir("styles/")
if err != nil {
return fmt.Errorf("Could not load styles directory: %s", err)
}
for _, s := range styles {
filename := s.Name()
if strings.HasSuffix(filename, ".scss") {
err := loadSassStylesheet(filename)
if err != nil {
return err
}
} else if strings.HasSuffix(filename, ".css") {
err := loadRegularStylesheet(filename)
if err != nil {
return err
}
} else {
log.Printf("Skipping stylesheet %s, don't know how to handle", filename)
continue
}
log.Printf("Loaded stylesheet %s", filename)
}
return nil
}
type listener struct {
folder string
update func(string) error
clean func(string) error
}
func (l *listener) listen() {
cwd, err := os.Getwd()
if err != nil {
log.Fatal("could not get current working directory for listener!")
}
cwd = cwd + "/"
c := make(chan notify.EventInfo, 1)
var events []notify.Event
// inotify events prevent double-firing of
// certain events in Linux.
if runtime.GOOS == "linux" {
events = []notify.Event{
notify.InCloseWrite,
notify.InMovedFrom,
notify.InMovedTo,
notify.InDelete,
}
} else {
events = []notify.Event{
notify.Create,
notify.Remove,
notify.Rename,
notify.Write,
}
}
err = notify.Watch(l.folder, c, events...)
if err != nil {
log.Fatalf("Could not setup watcher for folder %s: %s", l.folder, err)
}
defer notify.Stop(c)
for {
ei := <-c
log.Printf("event: %s", ei.Event())
switch ei.Event() {
case notify.InCloseWrite, notify.InMovedTo, notify.Create, notify.Rename, notify.Write:
filePath := strings.TrimPrefix(ei.Path(), cwd)
log.Printf("updating file %s", filePath)
err := l.update(strings.TrimPrefix(filePath, l.folder))
if err != nil {
log.Printf("watcher update action on %s failed: %v", filePath, err)
}
case notify.InMovedFrom, notify.InDelete, notify.Remove:
filePath := strings.TrimPrefix(ei.Path(), cwd)
log.Printf("cleaning file %s", filePath)
err := l.clean(strings.TrimPrefix(filePath, l.folder))
if err != nil {
log.Printf("watcher clean action on %s failed: %v", filePath, err)
}
}
}
}
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.templates["error"],
notFoundTpl: s.templates["notfound"],
handledError: false,
}
slug := req.URL.Path[1:]
if slug == "" {
s.homePage(res, req)
return
}
s.postsMutex.RLock()
defer s.postsMutex.RUnlock()
for _, p := range s.postList {
if p.Slug == slug {
s.postPage(p, res, req)
return
}
}
s.staticHandler.ServeHTTP(res, req)
}
func (s *server) errorInRequest(res http.ResponseWriter, req *http.Request, err error) {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte("oh no"))
log.Printf("ERR %s: %s", req.URL.Path, err)
}
func (s *server) createWebPage(title, contents string) (string, error) {
ctx := map[string]interface{}{
"title": title,
"contents": contents,
}
return s.templates["page"].Exec(ctx)
}
func (s *server) postPage(p *Post, res http.ResponseWriter, req *http.Request) {
res.Header().Add("content-type", "text/html")
contents, err := p.render(s.templates["fullpost"])
if err != nil {
s.errorInRequest(res, req, err)
}
page, err := s.createWebPage(p.Metadata.Title, contents)
if err != nil {
s.errorInRequest(res, req, err)
}
res.Write([]byte(page))
}
func (s *server) homePage(res http.ResponseWriter, req *http.Request) {
res.Header().Add("content-type", "text/html")
var posts string
s.postsMutex.RLock()
defer s.postsMutex.RUnlock()
for _, p := range s.postList {
summary, err := p.render(s.templates["summary"])
if err != nil {
log.Printf("could not render post summary for %s", p.Slug)
}
posts = posts + summary
}
page, err := s.createWebPage("Home", posts)
if err != nil {
s.errorInRequest(res, req, err)
}
res.Write([]byte(page))
}