diff options
| -rw-r--r-- | .golangci.yml | 74 | ||||
| -rw-r--r-- | cmd/server/app.go | 16 | ||||
| -rw-r--r-- | cmd/server/feed.go | 48 | ||||
| -rw-r--r-- | cmd/server/handlers.go | 17 | ||||
| -rw-r--r-- | cmd/server/main.go | 38 | ||||
| -rw-r--r-- | go.mod | 3 | ||||
| -rw-r--r-- | go.sum | 25 | ||||
| -rw-r--r-- | page/checkup.go | 8 | ||||
| -rw-r--r-- | page/index.go | 23 | ||||
| -rw-r--r-- | page/misc.go | 8 | ||||
| -rw-r--r-- | page/page.go | 59 | ||||
| -rw-r--r-- | page/pagelist.go | 12 | ||||
| -rw-r--r-- | page/render.go | 33 | ||||
| -rw-r--r-- | page/time.go | 14 | ||||
| -rw-r--r-- | rediscache/main.go | 118 |
15 files changed, 415 insertions, 81 deletions
diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..319e4af --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,74 @@ + +linters: + enable: + - deadcode + - dupl + - errcheck + - funlen + # - gochecknoglobals + - ineffassign + - structcheck + - typecheck + - varcheck + - asciicheck + - bodyclose + - depguard + - dogsled + - errorlint + - exhaustive + # - exhaustivestruct + - exportloopref + - gci + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - gofmt + - gofumpt + - goheader + # - goimports + - golint + - gomnd + - gomodguard + - goprintffuncname + - interfacer + - lll + - maligned + - misspell + - nakedret + - nestif + - nlreturn + - noctx + - nolintlint + - paralleltest + - prealloc + - rowserrcheck + - scopelint + - sqlclosecheck + - stylecheck + - testpackage + - tparallel + - unconvert + - unparam + - whitespace + - wrapcheck + - wsl + - gosec + - goerr113 + + +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently from this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + - .*and that stutters.* + + # It's a complex function--it's just one though + - Function 'Checkup' is too long + - Cognitive complexity .* of func ..\*Page..Checkup. is high + - 'if .info.IsDir.. .. strings.HasSuffix.info.Name.., Suffix.. is deeply nested .complexity: ... .nesti' diff --git a/cmd/server/app.go b/cmd/server/app.go index 743a389..290b44a 100644 --- a/cmd/server/app.go +++ b/cmd/server/app.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "os" + "github.com/gomodule/redigo/redis" "gopkg.in/yaml.v3" "riedstra.dev/mitch/go-website/page" ) @@ -10,6 +12,8 @@ import ( var FeedPrefixDefault = ".feeds" type App struct { + redisPool *redis.Pool + ReIndexPath string StaticDirectory string BaseTemplate string @@ -21,7 +25,7 @@ type App struct { Description string // aka, "subtitle" Author Author SiteURL string - FeedId string + FeedId string //nolint Updated page.PageTime FeedPrefix string } @@ -29,28 +33,34 @@ type App struct { func loadConf(fn string) (*App, error) { fh, err := os.Open(fn) if err != nil { - return nil, err + return nil, fmt.Errorf("loading config: %w", err) } + dec := yaml.NewDecoder(fh) app := &App{} + err = dec.Decode(app) if err != nil { - return nil, err + return nil, fmt.Errorf("decoding yaml: %w", err) } if app.StaticDirectory == "" { app.StaticDirectory = "static" } + if app.FeedPrefix == "" { app.FeedPrefix = FeedPrefixDefault } + if app.BaseTemplate != "" { page.BaseTemplate = app.BaseTemplate } + if app.DocumentSplit != "" { page.DocumentSplit = app.DocumentSplit } + if app.Suffix != "" { page.Suffix = app.Suffix } diff --git a/cmd/server/feed.go b/cmd/server/feed.go index c478365..880eeb4 100644 --- a/cmd/server/feed.go +++ b/cmd/server/feed.go @@ -16,8 +16,8 @@ import ( ) type Author struct { - Name string `xml:"name"` // Required - Uri string `xml:"uri,omitempty"` + Name string `xml:"name"` // Required + Uri string `xml:"uri,omitempty"` //nolint:golint,stylecheck Email string `xml:"email,omitempty"` } @@ -37,7 +37,7 @@ type Content struct { type Entry struct { // Spec requires this, autogenerated from Title and updated if otherwise // left empty - Id string `xml:"id"` + Id string `xml:"id"` //nolint:golint,stylecheck Title string `xml:"title"` // Required Updated *time.Time `xml:"updated"` // Required @@ -54,12 +54,13 @@ func (i Entry) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if i.Title == "" { errs = append(errs, "Title Cannot be empty") } + if i.Updated == nil { errs = append(errs, "Updated cannot be nil") } if len(errs) > 0 { - return errors.New(strings.Join(errs, ",")) + return errors.New(strings.Join(errs, ",")) //nolint:goerr113 } if i.Id == "" { @@ -71,6 +72,7 @@ func (i Entry) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(i2, start) } +//nolint:stylecheck,golint type Atom struct { Ns string `xml:"xmlns,attr"` Title string `xml:"title"` // Required @@ -87,18 +89,21 @@ func (a Atom) MarshalXML(e *xml.Encoder, start xml.StartElement) error { a.Ns = "http://www.w3.org/2005/Atom" errs := []string{} + if a.Id == "" { errs = append(errs, "ID Cannot be empty") } + if a.Author.Name == "" { errs = append(errs, "Author Name cannot be empty") } + if a.Updated == nil { errs = append(errs, "Updated cannot be empty") } if len(errs) > 0 { - return errors.New(strings.Join(errs, ",")) + return errors.New(strings.Join(errs, ",")) //nolint:goerr113 } start.Name = xml.Name{Local: "feed"} @@ -114,12 +119,15 @@ func (a Atom) MarshalXML(e *xml.Encoder, start xml.StartElement) error { // Relevant query parameters are: // // "content" if unset, or set to false content is omitted from the feed -// "limit=n" stop at "n" and return the feed +// "limit=n" stop at "n" and return the feed. // -func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { +func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { //nolint:funlen vars := mux.Vars(r) - var addContent bool - var limit int + + var ( + addContent bool + limit int + ) if _, ok := r.URL.Query()["content"]; ok { if r.URL.Query().Get("content") != "false" { @@ -137,20 +145,24 @@ func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { tag, ok := vars["tag"] if !ok { http.Error(w, "Tag not found or supplied", http.StatusNotFound) + return } p := page.NewPage("index") + index, err := p.Index() if err != nil { log.Println(err) http.Error(w, "Internal server error", http.StatusInternalServerError) + return } pages, ok := index[tag] if !ok { http.Error(w, "Invalid tag", http.StatusNotFound) + return } @@ -158,6 +170,7 @@ func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { for _, p := range dateless { log.Printf("Warning, page %s has no Date field. Skipping inclusion on feed", p) } + pages.SortDate() feed := &Atom{ @@ -180,22 +193,24 @@ func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { } content := &bytes.Buffer{} + err := p.Render(content) if err != nil { log.Println(err) http.Error(w, "Internal server error", http.StatusInternalServerError) + return } entry := Entry{ Title: p.Title, Updated: &p.Date.Time, - Links: []Link{Link{Href: strings.Join([]string{a.SiteURL, p.Path()}, "/")}}, + Links: []Link{{Href: strings.Join([]string{a.SiteURL, p.Path()}, "/")}}, } if p.AuthorName != "" { entry.Author = &Author{ - Name: p.AuthorName, + Name: p.AuthorName, } if p.AuthorEmail != "" { entry.Author.Email = p.AuthorEmail @@ -207,13 +222,18 @@ func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { } entries = append(entries, entry) - } feed.Entries = entries w.Header().Add("Content-type", "application/xml") - w.Write([]byte(xml.Header)) + + _, err = w.Write([]byte(xml.Header)) + if err != nil { + log.Println("Writing xml: ", err) + + return + } enc := xml.NewEncoder(w) enc.Indent("", " ") @@ -224,6 +244,4 @@ func (a *App) FeedHandler(w http.ResponseWriter, r *http.Request) { // Headers probably already sent, but we'll try anyway http.Error(w, "Internal server error", http.StatusInternalServerError) } - - return } diff --git a/cmd/server/handlers.go b/cmd/server/handlers.go index cb63774..869e5ba 100644 --- a/cmd/server/handlers.go +++ b/cmd/server/handlers.go @@ -7,14 +7,23 @@ import ( "github.com/gorilla/mux" "riedstra.dev/mitch/go-website/page" + "riedstra.dev/mitch/go-website/rediscache" ) func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { rtr := mux.NewRouter() rtr.HandleFunc(a.ReIndexPath, a.RebuildIndexHandler) rtr.PathPrefix("/static/").Handler(a.StaticHandler()) - rtr.PathPrefix(fmt.Sprintf("/%s/{tag}", a.FeedPrefix)).HandlerFunc(a.FeedHandler) - rtr.PathPrefix("/").HandlerFunc(a.PageHandler) + rtr.PathPrefix(fmt.Sprintf("/%s/{tag}", a.FeedPrefix)).HandlerFunc( + a.FeedHandler) + + if a.redisPool != nil { + rtr.PathPrefix("/").Handler(rediscache.Handle( + a.redisPool, http.HandlerFunc(a.PageHandler))) + } else { + rtr.PathPrefix("/").Handler(http.HandlerFunc(a.PageHandler)) + } + rtr.ServeHTTP(w, r) } @@ -23,6 +32,7 @@ func (a *App) PageHandler(w http.ResponseWriter, r *http.Request) { if u == "/" { u = "/index" } + u = filepath.Join(".", u) page.RenderForPath(w, r, u) @@ -33,6 +43,7 @@ func (a *App) RebuildIndexHandler(w http.ResponseWriter, r *http.Request) { if u == "/" { u = "/index" } + u = filepath.Join(".", u) p := page.NewPage("index") @@ -44,7 +55,7 @@ func (a *App) RebuildIndexHandler(w http.ResponseWriter, r *http.Request) { } // StaticHandler simply returns a HTTP handler that looks at the current -// directory and exposes `static` via HTTP `/static` +// directory and exposes `static` via HTTP `/static`. func (a *App) StaticHandler() http.Handler { return http.StripPrefix("/static/", http.FileServer(http.Dir(a.StaticDirectory))) } diff --git a/cmd/server/main.go b/cmd/server/main.go index 00aecbf..d76f938 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,12 +3,13 @@ package main import ( "flag" "fmt" - "gopkg.in/yaml.v3" "log" "net/http" "os" "time" + "github.com/gomodule/redigo/redis" + "gopkg.in/yaml.v3" "riedstra.dev/mitch/go-website/page" ) @@ -19,17 +20,28 @@ func VersionPrint() { os.Exit(0) } -func main() { +func main() { //nolint:funlen fl := flag.NewFlagSet("Website", flag.ExitOnError) listen := fl.String("l", "0.0.0.0:8001", "Listening address") directory := fl.String("d", ".", "Directory to serve.") version := fl.Bool("v", false, "Print the version then exit") confFn := fl.String("c", "conf.yml", "Location for the config file") verbose := fl.Bool("V", false, "Be more verbose ( dump config, etc ) ") - fl.StringVar(&page.TimeFormat, "T", page.TimeFormat, "Set the page time format, be careful with this") + fl.StringVar(&page.TimeFormat, "T", page.TimeFormat, + "Set the page time format, be careful with this") + defaultIndexPath := "/reIndex" + indexPath := fl.String("i", defaultIndexPath, "Path in which, when called will rebuild the index and clear the cache") + redisAddr := fl.String("r", "127.0.0.1:6379", + "Redis server set to \"\" to disable") + + pageTimeout := fl.Int("timeout", 15, "Seconds until page timeout for read and write") + + fl.BoolVar(&page.CacheIndex, "cache-index", true, + "If set to false do not cache index") + _ = fl.Parse(os.Args[1:]) if *version { @@ -43,6 +55,7 @@ func main() { app, err := loadConf(*confFn) if err != nil { log.Println(err) + app = &App{} } @@ -50,6 +63,21 @@ func main() { app.ReIndexPath = *indexPath } + if *redisAddr != "" { + app.redisPool = &redis.Pool{ + MaxIdle: 80, //nolint:gomnd + MaxActive: 12000, //nolint:gomnd + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", *redisAddr) + if err != nil { + log.Println("Redis dial error: ", err) + } + + return c, err //nolint + }, + } + } + if *verbose { b, _ := yaml.Marshal(app) os.Stderr.Write(b) @@ -58,8 +86,8 @@ func main() { srv := &http.Server{ Handler: app, Addr: *listen, - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, + WriteTimeout: time.Duration(*pageTimeout) * time.Second, + ReadTimeout: time.Duration(*pageTimeout) * time.Second, } log.Fatal(srv.ListenAndServe()) } @@ -3,10 +3,13 @@ module riedstra.dev/mitch/go-website go 1.13 require ( + github.com/gomodule/redigo v1.8.5 // indirect github.com/gorilla/mux v1.8.0 github.com/kr/pretty v0.1.0 // indirect github.com/russross/blackfriday v2.0.0+incompatible github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 ) @@ -1,3 +1,9 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc= +github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -6,12 +12,31 @@ github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/page/checkup.go b/page/checkup.go index c4501c2..9f721b7 100644 --- a/page/checkup.go +++ b/page/checkup.go @@ -9,25 +9,25 @@ import ( ) // Checkup will return a map[string]PageList of all the pages broken down by -// the status of their fields. For instance, whehter or not they have a date +// the status of their fields. For instance, whehter or not they have a date. func (p *Page) Checkup() (map[string]PageList, error) { Logger.Println("Checking up on all files...") out := make(map[string]PageList) - filepath.Walk(filepath.Dir("."), + _ = filepath.Walk(filepath.Dir("."), func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(info.Name(), Suffix) { - p2 := NewPage(strings.ReplaceAll(path, Suffix, "")) err = p2.Read() if err != nil { Logger.Println("Error encountered: ", err) + return err } @@ -73,9 +73,7 @@ func (p *Page) Checkup() (map[string]PageList, error) { } else { out[key] = append(out[key], p2) } - } - } return nil diff --git a/page/index.go b/page/index.go index f9f2df5..425bf04 100644 --- a/page/index.go +++ b/page/index.go @@ -8,27 +8,32 @@ import ( "time" ) -var index map[string]PageList -var indexMu sync.RWMutex +var ( + index map[string]PageList + indexMu sync.RWMutex +) // RebuildIndex can be called in order to rebuild the entire website -// index +// index. func (p *Page) RebuildIndex() error { indexMu.Lock() index = nil indexMu.Unlock() + _, err := p.Index() + return err } -// Index returns a map of all pages in the current directory seperated into +// Index returns a map of all pages in the current directory separated into // their respective tags If a Page has multiple tags it will be listed under // each. -// Pages are located by their Suffix, default being ".md" +// Pages are located by their Suffix, default being ".md". func (p *Page) Index() (map[string]PageList, error) { indexMu.RLock() if index != nil && CacheIndex { indexMu.RUnlock() + return index, nil } indexMu.RUnlock() @@ -36,30 +41,29 @@ func (p *Page) Index() (map[string]PageList, error) { out := make(map[string]PageList) - filepath.Walk(filepath.Dir("."), + _ = filepath.Walk(filepath.Dir("."), func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(info.Name(), Suffix) { - p2 := NewPage(strings.ReplaceAll(path, Suffix, "")) err = p2.Read() if err != nil { Logger.Println("Error encountered: ", err) + return err } - for tag, _ := range p2.Tags { + for tag := range p2.Tags { if _, ok := out[tag]; !ok { out[tag] = []*Page{p2} } else { out[tag] = append(out[tag], p2) } } - } return nil @@ -72,6 +76,7 @@ func (p *Page) Index() (map[string]PageList, error) { return out, nil } +// Time fetches the time.Time from the Date field. func (p *Page) Time() time.Time { return p.Date.Time } diff --git a/page/misc.go b/page/misc.go index 0dbd059..e8bdfb7 100644 --- a/page/misc.go +++ b/page/misc.go @@ -6,6 +6,8 @@ import ( "gopkg.in/yaml.v3" ) +// EncodeYaml is meant to be used in templating functions to encode +// arbitrary information as a yaml string. func (p Page) EncodeYaml(data interface{}) string { if data == nil { data = p @@ -15,10 +17,13 @@ func (p Page) EncodeYaml(data interface{}) string { if err != nil { Logger.Println("Encountered error in EncodeYaml: ", err) } + return string(b) } -func (p Page) EncodeJson(data interface{}) string { +// EncodeJSON is meant to be used in templating functions to encode +// arbitrary information as a JSON string. +func (p Page) EncodeJSON(data interface{}) string { if data == nil { data = p } @@ -27,5 +32,6 @@ func (p Page) EncodeJson(data interface{}) string { if err != nil { Logger.Println("Encountered error in EncodeJson: ", err) } + return string(b) } diff --git a/page/page.go b/page/page.go index c926bed..fa4ce6c 100644 --- a/page/page.go +++ b/page/page.go @@ -1,4 +1,4 @@ -// page implements the website backed by a local filesystem. +// Package page implements the website backed by a local filesystem. // // Reading the base template off the disk, then any markdown files which are // split into two sections by the DocumentSplit global variable. The first @@ -25,6 +25,7 @@ package page import ( "bufio" "bytes" + "errors" "fmt" "io" "log" @@ -56,27 +57,28 @@ type Page struct { } // Global is meant to be supplied by external users of this package to populate -// globally accessable information across all of the templates accessiable via -// .Global care must be taken when utilizing this functionality +// globally accessible information across all of the templates accessiable via +// .Global care must be taken when utilizing this functionality. var Global interface{} // CacheIndex determines whether or not the index will be cached in memory -// or rebuilt on each call +// or rebuilt on each call. var CacheIndex = true -// BaseTemplate can be adjusted to change the base template used in rendering +// BaseTemplate can be adjusted to change the base template used in rendering. var BaseTemplate = "inc/base.html" -// Suffix is applied to all pages for reading off of the disk +// Suffix is applied to all pages for reading off of the disk. var Suffix = ".md" -// DocumentSplit is used to split the .md files into yaml and markdown +// DocumentSplit is used to split the .md files into yaml and markdown. var DocumentSplit = "|---\n" -// Default logger -var Logger = log.New(os.Stderr, "", log.LstdFlags) +// Logger is the default logger used throughout this package, feel +// free to override. +var Logger = log.New(os.Stderr, "PAGE: ", log.LstdFlags) -// NewPage returns a page struct with the path populated +// NewPage returns a page struct with the path populated. func NewPage(pth string) *Page { return &Page{path: filepath.FromSlash(filepath.Clean(pth))} } @@ -95,12 +97,12 @@ func (p Page) Path() string { } // Global is specifically for use inside of a page markdown file or -// in a base template. This simply returns the package Global variable +// in a base template. This simply returns the package Global variable. func (p *Page) Global() interface{} { return Global } -// Renders a page +// Render a page. func (p *Page) Render(wr io.Writer) error { if err := p.Read(); err != nil { return err @@ -108,38 +110,46 @@ func (p *Page) Render(wr io.Writer) error { t, err := template.ParseFiles(BaseTemplate) if err != nil { - return err + return fmt.Errorf("rendering: %w", err) } return t.Execute(wr, p) } -// Reads in the special markdown file format for the website off of the disk +// Read in the special markdown file format for the website off of the disk. func (p *Page) Read() error { yamlBuf := bytes.NewBuffer(nil) markdownBuf := bytes.NewBuffer(nil) fh, err := os.Open(p.path + Suffix) if err != nil { - return err + return fmt.Errorf("opening markdown: %w", err) } - defer fh.Close() + + defer func() { + err := fh.Close() + if err != nil { + Logger.Println(err) + } + }() + rdr := bufio.NewReader(fh) // Read in the file and split between markdown and yaml buffers yamlDone := false - for { + for { bytes, err := rdr.ReadBytes('\n') - if err == io.EOF { + if errors.Is(err, io.EOF) { break } else if err != nil { - return err + return fmt.Errorf("reading markdown: %w", err) } // Is this the line where we stop reading the yaml and start reading markdown? if DocumentSplit == string(bytes) && !yamlDone { yamlDone = true + continue } @@ -152,23 +162,28 @@ func (p *Page) Read() error { err = yaml.Unmarshal(yamlBuf.Bytes(), p) if err != nil { - return err + return fmt.Errorf("reading yaml: %w", err) } p.markdown = markdownBuf.Bytes() + return nil } +// RenderBody renders and executes a template from the body of the +// markdown file, then runs it through the markdown parser. func (p *Page) RenderBody() (string, error) { buf := &bytes.Buffer{} + t, err := template.New("Body").Parse(string(p.markdown)) if err != nil { - return "", err + return "", fmt.Errorf("render body: %w", err) } err = t.Execute(buf, p) + if err != nil { - return "", err + return "", fmt.Errorf("template execute; %w", err) } return string(blackfriday.Run(buf.Bytes())), nil diff --git a/page/pagelist.go b/page/pagelist.go index f2140f9..298acf9 100644 --- a/page/pagelist.go +++ b/page/pagelist.go @@ -5,15 +5,16 @@ import ( ) // PageList is a slice of pages, providing a couple of methods to sort -// by the date, or date reversed +// by the date, or date reversed. type PageList []*Page // RemoveDateless returns two PageLists, the first with valid dates, // and the second without. This is useful if you need a PageList which -// will run SortDate and SortDateReverse without issue +// will run SortDate and SortDateReverse without issue. func (p PageList) RemoveDateless() (PageList, PageList) { with := PageList{} without := PageList{} + for _, p := range p { if p.Date != nil { with = append(with, p) @@ -21,19 +22,26 @@ func (p PageList) RemoveDateless() (PageList, PageList) { without = append(without, p) } } + return with, without } +// SortDate returns the pagelist sorted by date, may panic if pages do +// not all have dates. func (p PageList) SortDate() PageList { sort.Slice(p, func(i, j int) bool { return p[i].Time().After(p[j].Time()) }) + return p } +// SortDateReverse returns the pagelist sorted by date in reverse, may panic if +// pages do not all have dates. func (p PageList) SortDateReverse() PageList { sort.Slice(p, func(i, j int) bool { return p[i].Time().Before(p[j].Time()) }) + return p } diff --git a/page/render.go b/page/render.go index 07b1b88..6cbabf9 100644 --- a/page/render.go +++ b/page/render.go @@ -7,14 +7,14 @@ import ( ) // Render is a lower level option, allowing you to specify local -// variables and the status code in which to return +// variables and the status code in which to return. func Render(w http.ResponseWriter, r *http.Request, path string, vars map[string]interface{}, statusCode int) { - u := r.URL.Path if u == "/" { u = "/index" } + u = filepath.Join(".", u) // Sepcifically use the specified path for the page @@ -32,8 +32,11 @@ func Render(w http.ResponseWriter, r *http.Request, r.Method, http.StatusNotFound, u) + p = NewPage("404") + w.WriteHeader(http.StatusNotFound) + err := p.Render(w) if err != nil { Logger.Printf("%s %s path: %s while trying 404: %s", @@ -43,19 +46,22 @@ func Render(w http.ResponseWriter, r *http.Request, err) http.Error(w, "Internal server error", http.StatusInternalServerError) + return } - return - } else { - Logger.Printf("%s %s path: %s encountered: %s", - r.RemoteAddr, - r.Method, - u, - err) - http.Error(w, "Internal server error", - http.StatusInternalServerError) + return } + + Logger.Printf("%s %s path: %s encountered: %s", + r.RemoteAddr, + r.Method, + u, + err) + http.Error(w, "Internal server error", + http.StatusInternalServerError) + + return } Logger.Printf("%s %s %d %s", r.RemoteAddr, r.Method, statusCode, u) @@ -63,15 +69,14 @@ func Render(w http.ResponseWriter, r *http.Request, // RenderWithVars allows you to specify a specific page and whether or not // you wish to override vars. If left nil they will not be overridden. -// Also see RenderForPath if you don't need to override them +// Also see RenderForPath if you don't need to override them. func RenderWithVars(w http.ResponseWriter, r *http.Request, path string, vars map[string]interface{}) { - Render(w, r, path, vars, http.StatusOK) } // RenderForPath takes the path to a page and finish up the rendering -// Allowing you to place logic on what page is rendered by your handlers +// Allowing you to place logic on what page is rendered by your handlers. func RenderForPath(w http.ResponseWriter, r *http.Request, path string) { RenderWithVars(w, r, path, nil) } diff --git a/page/time.go b/page/time.go index 0374490..cd419c8 100644 --- a/page/time.go +++ b/page/time.go @@ -1,10 +1,14 @@ package page import ( - "gopkg.in/yaml.v3" + "fmt" "time" + + "gopkg.in/yaml.v3" ) +// PageTime allows us to slip in a different time format for loading/unloading +// the yaml from disk than the default. type PageTime struct { time.Time } @@ -13,16 +17,22 @@ type PageTime struct { // from the yaml information. var TimeFormat = "01.02.2006 15:04:05 MST" +// UnmarshalYAML override the default and parse our time with the global time +// format. func (pt *PageTime) UnmarshalYAML(n *yaml.Node) error { t, err := time.Parse(TimeFormat, n.Value) if err != nil { - return err + return fmt.Errorf("pagetime: %w", err) } + pt.Time = t + return nil } +// MarshalYAML override the default with our own time format. func (pt PageTime) MarshalYAML() (interface{}, error) { s := pt.Time.Format(TimeFormat) + return s, nil } diff --git a/rediscache/main.go b/rediscache/main.go new file mode 100644 index 0000000..e7564c8 --- /dev/null +++ b/rediscache/main.go @@ -0,0 +1,118 @@ +package rediscache + +import ( + "bytes" + "log" + "net/http" + "os" + + "github.com/gomodule/redigo/redis" + "github.com/vmihailenco/msgpack" +) + +// Logger is the default logger used for this package, feel free to +// override. +var Logger = log.New(os.Stderr, "REDIS: ", log.LstdFlags) + +// redisHTTPResponseWriter is essentially a fake http.ResponseWriter that +// is going to let us suck out information and chuck it into redis +// implements the interface as defined in net/http. +type redisHTTPResponseWriter struct { + Headers http.Header + StatusCode int + Data []byte + + buf *bytes.Buffer +} + +// Simply for satisfying the http.ResponseWriter interface. +func (rw *redisHTTPResponseWriter) Header() http.Header { + if rw.Headers == nil { + rw.Headers = http.Header{} + } + + return rw.Headers +} + +// Writes to the internal buffer. +func (rw *redisHTTPResponseWriter) Write(msg []byte) (int, error) { + if rw.buf == nil { + rw.buf = &bytes.Buffer{} + } + + return rw.buf.Write(msg) +} + +// Simply for satisfying the http.ResponseWriter interface. +func (rw *redisHTTPResponseWriter) WriteHeader(code int) { + rw.StatusCode = code +} + +// WriteData takes the internal buffer and writes out the entire +// contents to the 'Data' field for storage in Redis. +func (rw *redisHTTPResponseWriter) WriteData() { + rw.Data = rw.buf.Bytes() +} + +// Simple function that will cache the response for given handler in redis +// and instead of responding with the result from the handler it will +// simply dump the contents of the redis key if it exists. +func Handle(pool *redis.Pool, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + client := pool.Get() + defer client.Close() + + content: + data, err := client.Do("GET", r.URL.Path) + if err != nil { + // Assume something bad has happened with redis, we're + // just going to log this and then pass through the + // request as normal. + Logger.Println("ERROR: ", err) + next.ServeHTTP(w, r) + + return + } else if data == nil { + rw := &redisHTTPResponseWriter{} + next.ServeHTTP(rw, r) + + rw.WriteData() + b, err := msgpack.Marshal(rw) + if err != nil { + Logger.Println("ERROR: marshaling: ", err) + + return + } + _, err = client.Do("SET", r.URL.Path, b) + if err != nil { + Logger.Println("ERROR: during set: ", err) + + return + } + + // We got the content, let's go back around again and dump + // it out from redis + goto content + } + + rw := &redisHTTPResponseWriter{} + + err = msgpack.Unmarshal(data.([]byte), rw) + if err != nil { + Logger.Println("ERROR: unmarshaling: ", err) + + return + } + + if rw.Headers != nil { + for k, v := range rw.Headers { + w.Header()[k] = v + } + } + + if rw.StatusCode != 0 { + w.WriteHeader(rw.StatusCode) + } + _, _ = w.Write(rw.Data) + }) +} |
