From 599712fab9127013e2d89dadabd839c847730637 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sun, 1 Jul 2018 13:20:01 -0700 Subject: rip out mage --- cmd/site/gopreload.go | 9 +++ cmd/site/gops.go | 13 ++++ cmd/site/hash.go | 14 ++++ cmd/site/html.go | 73 ++++++++++++++++++ cmd/site/main.go | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/site/rss.go | 64 ++++++++++++++++ 6 files changed, 378 insertions(+) create mode 100644 cmd/site/gopreload.go create mode 100644 cmd/site/gops.go create mode 100644 cmd/site/hash.go create mode 100644 cmd/site/html.go create mode 100644 cmd/site/main.go create mode 100644 cmd/site/rss.go (limited to 'cmd') diff --git a/cmd/site/gopreload.go b/cmd/site/gopreload.go new file mode 100644 index 0000000..6829ae5 --- /dev/null +++ b/cmd/site/gopreload.go @@ -0,0 +1,9 @@ +// gopreload.go +package main + +/* + This file is separate to make it very easy to both add into an application, but + also very easy to remove. +*/ + +import _ "github.com/Xe/gopreload" diff --git a/cmd/site/gops.go b/cmd/site/gops.go new file mode 100644 index 0000000..184b656 --- /dev/null +++ b/cmd/site/gops.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/google/gops/agent" +) + +func init() { + if err := agent.Listen(nil); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/site/hash.go b/cmd/site/hash.go new file mode 100644 index 0000000..ed6112c --- /dev/null +++ b/cmd/site/hash.go @@ -0,0 +1,14 @@ +package main + +import ( + "crypto/md5" + "fmt" +) + +// Hash is a simple wrapper around the MD5 algorithm implementation in the +// Go standard library. It takes in data and a salt and returns the hashed +// representation. +func Hash(data string, salt string) string { + output := md5.Sum([]byte(data + salt)) + return fmt.Sprintf("%x", output) +} diff --git a/cmd/site/html.go b/cmd/site/html.go new file mode 100644 index 0000000..ba304c5 --- /dev/null +++ b/cmd/site/html.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "fmt" + "html/template" + "net/http" + "time" + + "github.com/Xe/ln" +) + +func logTemplateTime(name string, from time.Time) { + now := time.Now() + ln.Log(context.Background(), ln.F{"action": "template_rendered", "dur": now.Sub(from).String(), "name": name}) +} + +func (s *Site) renderTemplatePage(templateFname string, data interface{}) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer logTemplateTime(templateFname, time.Now()) + s.tlock.RLock() + defer s.tlock.RUnlock() + + var t *template.Template + var err error + + if s.templates[templateFname] == nil { + t, err = template.ParseFiles("templates/base.html", "templates/"+templateFname) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + ln.Error(context.Background(), err, ln.F{"action": "renderTemplatePage", "page": templateFname}) + fmt.Fprintf(w, "error: %v", err) + } + + ln.Log(context.Background(), ln.F{"action": "loaded_new_template", "fname": templateFname}) + + s.tlock.RUnlock() + s.tlock.Lock() + s.templates[templateFname] = t + s.tlock.Unlock() + s.tlock.RLock() + } else { + t = s.templates[templateFname] + } + + err = t.Execute(w, data) + if err != nil { + panic(err) + } + }) +} + +func (s *Site) showPost(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/blog/" { + http.Redirect(w, r, "/blog", http.StatusSeeOther) + return + } + + var p *Post + for _, pst := range s.Posts { + if pst.Link == r.RequestURI[1:] { + p = pst + } + } + + if p == nil { + w.WriteHeader(http.StatusNotFound) + s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r) + return + } + + s.renderTemplatePage("blogpost.html", p).ServeHTTP(w, r) +} diff --git a/cmd/site/main.go b/cmd/site/main.go new file mode 100644 index 0000000..e0cc20d --- /dev/null +++ b/cmd/site/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "context" + "html/template" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + "github.com/GeertJohan/go.rice" + "github.com/Xe/jsonfeed" + "github.com/Xe/ln" + "github.com/gorilla/feeds" + blackfriday "github.com/russross/blackfriday" + "github.com/tj/front" +) + +var port = os.Getenv("PORT") + +func main() { + if port == "" { + port = "29384" + } + + s, err := Build() + if err != nil { + ln.FatalErr(context.Background(), err, ln.Action("Build")) + } + + ln.Log(context.Background(), ln.F{"action": "http_listening", "port": port}) + http.ListenAndServe(":"+port, s) +} + +// Site is the parent object for https://christine.website's backend. +type Site struct { + Posts Posts + Resume template.HTML + + rssFeed *feeds.Feed + jsonFeed *jsonfeed.Feed + + mux *http.ServeMux + + templates map[string]*template.Template + tlock sync.RWMutex +} + +func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ln.Log(r.Context(), ln.F{"action": "Site.ServeHTTP", "user_ip_address": r.RemoteAddr, "path": r.RequestURI}) + s.mux.ServeHTTP(w, r) +} + +// Build creates a new Site instance or fails. +func Build() (*Site, error) { + type postFM struct { + Title string + Date string + } + + s := &Site{ + rssFeed: &feeds.Feed{ + Title: "Christine Dodrill's Blog", + Link: &feeds.Link{Href: "https://christine.website/blog"}, + Description: "My blog posts and rants about various technology things.", + Author: &feeds.Author{Name: "Christine Dodrill", Email: "me@christine.website"}, + Created: bootTime, + Copyright: "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.", + }, + jsonFeed: &jsonfeed.Feed{ + Version: jsonfeed.CurrentVersion, + Title: "Christine Dodrill's Blog", + HomePageURL: "https://christine.website", + FeedURL: "https://christine.website/blog.json", + Description: "My blog posts and rants about various technology things.", + UserComment: "This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1", + Icon: icon, + Favicon: icon, + Author: jsonfeed.Author{ + Name: "Christine Dodrill", + Avatar: icon, + }, + }, + mux: http.NewServeMux(), + templates: map[string]*template.Template{}, + } + + err := filepath.Walk("./blog/", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + fin, err := os.Open(path) + if err != nil { + return err + } + defer fin.Close() + + content, err := ioutil.ReadAll(fin) + if err != nil { + return err + } + + var fm postFM + remaining, err := front.Unmarshal(content, &fm) + if err != nil { + return err + } + + output := blackfriday.Run(remaining) + + p := &Post{ + Title: fm.Title, + Date: fm.Date, + Link: strings.Split(path, ".")[0], + Body: string(remaining), + BodyHTML: template.HTML(output), + } + + s.Posts = append(s.Posts, p) + + return nil + }) + if err != nil { + return nil, err + } + + sort.Sort(sort.Reverse(s.Posts)) + + cb, err := rice.FindBox("css") + if err != nil { + return nil, err + } + + sb, err := rice.FindBox("static") + if err != nil { + return nil, err + } + + s.Resume = template.HTML(blackfriday.Run(sb.MustBytes("resume/resume.md"))) + + for _, item := range s.Posts { + itime, _ := time.Parse("2006-01-02", item.Date) + s.rssFeed.Items = append(s.rssFeed.Items, &feeds.Item{ + Title: item.Title, + Link: &feeds.Link{Href: "https://christine.website/" + item.Link}, + Description: item.Summary, + Created: itime, + }) + + s.jsonFeed.Items = append(s.jsonFeed.Items, jsonfeed.Item{ + ID: "https://christine.website/" + item.Link, + URL: "https://christine.website/" + item.Link, + Title: item.Title, + DatePublished: itime, + ContentHTML: string(item.BodyHTML), + }) + } + + // Add HTTP routes here + s.mux.Handle("/", s.renderTemplatePage("index.html", nil)) + s.mux.Handle("/resume", s.renderTemplatePage("resume.html", s.Resume)) + s.mux.Handle("/blog", s.renderTemplatePage("blogindex.html", s.Posts)) + s.mux.Handle("/contact", s.renderTemplatePage("contact.html", nil)) + s.mux.HandleFunc("/blog.rss", s.createFeed) + s.mux.HandleFunc("/blog.atom", s.createAtom) + s.mux.HandleFunc("/blog.json", s.createJsonFeed) + s.mux.HandleFunc("/blog/", s.showPost) + s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(sb.HTTPBox()))) + s.mux.Handle("/css/", http.StripPrefix("/css/", http.FileServer(cb.HTTPBox()))) + + return s, nil +} + +const icon = "https://christine.website/static/img/avatar.png" + +// Post is a single blogpost. +type Post struct { + Title string `json:"title"` + Link string `json:"link"` + Summary string `json:"summary,omitifempty"` + Body string `json:"-"` + BodyHTML template.HTML `json:"body"` + Date string `json:"date"` +} + +// Posts implements sort.Interface for a slice of Post objects. +type Posts []*Post + +func (p Posts) Len() int { return len(p) } +func (p Posts) Less(i, j int) bool { + iDate, _ := time.Parse("2006-01-02", p[i].Date) + jDate, _ := time.Parse("2006-01-02", p[j].Date) + + return iDate.Unix() < jDate.Unix() +} +func (p Posts) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/cmd/site/rss.go b/cmd/site/rss.go new file mode 100644 index 0000000..15e9163 --- /dev/null +++ b/cmd/site/rss.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/Xe/ln" +) + +var bootTime = time.Now() + +// IncrediblySecureSalt ******* +const IncrediblySecureSalt = "hunter2" + +func (s *Site) createFeed(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/rss+xml") + w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt)) + + err := s.rssFeed.WriteRss(w) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + ln.Error(r.Context(), err, ln.F{ + "remote_addr": r.RemoteAddr, + "action": "generating_rss", + "uri": r.RequestURI, + "host": r.Host, + }) + } +} + +func (s *Site) createAtom(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/atom+xml") + w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt)) + + err := s.rssFeed.WriteAtom(w) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + ln.Error(r.Context(), err, ln.F{ + "remote_addr": r.RemoteAddr, + "action": "generating_atom", + "uri": r.RequestURI, + "host": r.Host, + }) + } +} + +func (s *Site) createJsonFeed(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt)) + + e := json.NewEncoder(w) + e.SetIndent("", "\t") + err := e.Encode(s.jsonFeed) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + ln.Error(r.Context(), err, ln.F{ + "remote_addr": r.RemoteAddr, + "action": "generating_jsonfeed", + "uri": r.RequestURI, + "host": r.Host, + }) + } +} -- cgit v1.2.3