router and tpl packages

Signed-off-by: Michael <michael.lindman@gmail.com>
This commit is contained in:
Michael 2021-03-09 01:32:31 +00:00
parent cb2095dfac
commit 15e52bd393
10 changed files with 450 additions and 1 deletions

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
doc:
godoc -http=:6090 -index
debs:
go get ./...
update-debs:
go get -u ./...
fmt:
gofmt -l -s .
.PHONY: doc debs update-debs fmt

5
go.mod
View File

@ -3,7 +3,10 @@ module git.0cd.xyz/michael/gtools
go 1.15
require (
github.com/gorilla/csrf v1.7.0
github.com/gorilla/mux v1.8.0
github.com/magefile/mage v1.11.0 // indirect
github.com/sirupsen/logrus v1.8.0
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b // indirect
golang.org/x/sys v0.0.0-20210308170721-88b6017d0656 // indirect
google.golang.org/protobuf v1.25.0
)

34
router/debug.go Normal file
View File

@ -0,0 +1,34 @@
package router
import (
"net/http"
"net/http/pprof"
"github.com/gorilla/mux"
)
// HandlePprof pprof index
func (re *Router) HandlePprof() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
pprof.Index(w, r)
}
}
// HandlePprofProfile pprof profile
func (re *Router) HandlePprofProfile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
switch vars["pprof"] {
case "cmdline":
pprof.Cmdline(w, r)
case "profile":
pprof.Profile(w, r)
case "symbol":
pprof.Symbol(w, r)
case "trace":
pprof.Trace(w, r)
default:
re.HandlePprof().ServeHTTP(w, r)
}
}
}

87
router/errors.go Normal file
View File

@ -0,0 +1,87 @@
package router
import (
"net/http"
)
// ErrorResponder responds with error
type ErrorResponder interface {
RespondError(w http.ResponseWriter, r *http.Request, re *Router) bool
}
// HTTPStatusError http statuscode errors
type HTTPStatusError struct {
statuscode int
err error
body interface{}
}
// HTTPError handle HTTP error
func HTTPError(statuscode int, err error) *HTTPStatusError {
return &HTTPStatusError{statuscode: statuscode, err: err}
}
// HTTPErrorWithBody handle HTTP error with body
func HTTPErrorWithBody(body interface{}, statuscode int, err error) *HTTPStatusError {
return &HTTPStatusError{body: body, statuscode: statuscode, err: err}
}
// RespondError HTTP error response
func (e *HTTPStatusError) RespondError(w http.ResponseWriter, r *http.Request, re *Router) bool {
if e.err != nil {
if e.statuscode >= 200 && e.statuscode <= 499 {
re.logger.Error.Warn(e.err)
} else {
re.logger.Error.Error(e.err)
}
}
if e.body == nil {
re.handleError(e.statuscode).ServeHTTP(w, r)
} else {
re.Respond(e.body, e.statuscode).ServeHTTP(w, r)
}
return true
}
// Error returns HTTP error
func (e *HTTPStatusError) Error() string {
return e.err.Error()
}
// WithError error handler
func (re *Router) WithError(h HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
if er, ok := err.(ErrorResponder); ok {
if er.RespondError(w, r, re) {
return
}
}
re.logger.Error.Error(err)
re.handleError(http.StatusInternalServerError).ServeHTTP(w, r)
}
}
}
func (re *Router) handleError(statuscode int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
re.logger.AccessMessage(r, statuscode)
w.WriteHeader(statuscode)
re.RenderTemplate(struct {
Title string
Status struct {
Status string
StatusCode int
}
}{
Title: http.StatusText(statuscode),
Status: struct {
Status string
StatusCode int
}{
Status: http.StatusText(statuscode),
StatusCode: statuscode,
},
}).ServeHTTP(w, r)
}
}

37
router/files.go Normal file
View File

@ -0,0 +1,37 @@
package router
import (
"bytes"
"io/fs"
"net/http"
"path"
"time"
"github.com/gorilla/mux"
)
// FileHandler handles loading file assets
func (re *Router) FileHandler(ph string) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
filename := mux.Vars(r)["filename"]
file, err := fs.ReadFile(re.fsys, "assets/"+ph+"/"+filename)
if err != nil {
return HTTPError(http.StatusNotFound, err)
}
switch p := path.Ext(filename); p {
case ".css":
w.Header().Set("Content-Type", "text/css")
case ".js":
w.Header().Set("Content-Type", "application/javascript")
case ".json":
w.Header().Set("Content-Type", "application/json")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
default:
w.Header().Set("Content-Type", http.DetectContentType(file))
}
w.Header().Set("Cache-Control", "max-age=2592000")
http.ServeContent(w, r, filename, time.Now(), bytes.NewReader(file))
return nil
}
}

45
router/pages.go Normal file
View File

@ -0,0 +1,45 @@
package router
import (
"html/template"
"net/http"
"git.0cd.xyz/michael/gtools/tpl"
"github.com/gorilla/csrf"
)
// Page web page
type Page struct {
App interface{}
CSRFField template.HTML
Content interface{}
}
// HandlePage handles page requests with body
func (re *Router) HandlePage(title string, body interface{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
re.Response(&Response{
Title: title,
Body: body,
}, http.StatusOK).ServeHTTP(w, r)
}
}
// RenderTemplate renders html templates
func (re *Router) RenderTemplate(data interface{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := tpl.Load(re.tplopts)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := tmpl.Execute(w, &Page{
App: re.page,
CSRFField: csrf.TemplateField(r),
Content: data,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

87
router/router.go Normal file
View File

@ -0,0 +1,87 @@
package router
import (
"encoding/json"
"io/fs"
"net/http"
"net/url"
"git.0cd.xyz/michael/gtools/logger"
"git.0cd.xyz/michael/gtools/tpl"
"github.com/gorilla/mux"
)
// Router Configuration
type Router struct {
router *mux.Router
tplopts *tpl.Options
page interface{}
logger *logger.Logger
fsys fs.FS
}
// NewRouter instance of new router
func NewRouter(tplopts *tpl.Options, page interface{}, logger *logger.Logger, fsys fs.FS) *Router {
return &Router{
router: mux.NewRouter(),
tplopts: tplopts,
page: page,
logger: logger,
fsys: fsys,
}
}
// Router returns router
func (re *Router) Router() *mux.Router {
return re.router
}
// Response HTTP Response
type Response struct {
Title string
Vars map[string]string
Query url.Values
Body interface{}
Status Status
}
// Status HTTP Status Code
type Status struct {
Status string
StatusCode int
}
// HandlerFunc custom handler function that passes errors
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
// Response handles http respondes
func (re *Router) Response(resp *Response, statuscode int) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
re.logger.AccessMessage(r, statuscode)
if statuscode >= 200 && statuscode <= 299 {
re.RenderTemplate(resp).ServeHTTP(w, r)
return nil
}
return HTTPError(statuscode, nil)
}
}
// Respond responds with data json
func (re *Router) Respond(data interface{}, status int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
re.logger.AccessMessage(r, status)
w.WriteHeader(status)
w.Header().Set("Content-Type", "application/json")
if data != nil {
if err := json.NewEncoder(w).Encode(data); err != nil {
re.logger.Error.Errorf("faield to encode a response: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
}
}

22
tpl/cache.go Normal file
View File

@ -0,0 +1,22 @@
package tpl
import "html/template"
func setCache(name string, tpl template.Template) {
cacheMutex.Lock()
cache[name] = tpl
cacheMutex.Unlock()
}
func getCache(name string) (template.Template, bool) {
cacheMutex.RLock()
tpl, ok := cache[name]
cacheMutex.RUnlock()
return tpl, ok
}
func flushCache() {
cacheMutex.Lock()
cache = make(map[string]template.Template)
cacheMutex.Unlock()
}

51
tpl/funcmap.go Normal file
View File

@ -0,0 +1,51 @@
package tpl
import (
"html/template"
"strconv"
"strings"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
)
// FuncMap function map
func FuncMap() template.FuncMap {
return template.FuncMap{
"parseTime": parseTime,
"add": add,
"replace": strings.ReplaceAll,
"HTML": parseHTML,
}
}
func parseTime(format string, datetime *timestamppb.Timestamp) string {
dur := time.Since(datetime.AsTime())
switch {
case dur.Seconds() == 1:
return "1 second ago"
case dur.Seconds() < 60:
return strconv.FormatFloat(dur.Seconds(), 'f', 0, 64) + " seconds ago"
case dur.Minutes() == 1:
return "1 minute ago"
case dur.Minutes() < 60:
return strconv.FormatFloat(dur.Minutes(), 'f', 0, 64) + " minutes ago"
case dur.Hours() == 1:
return "1 hour ago"
case dur.Hours() < 24:
return strconv.FormatFloat(dur.Hours(), 'f', 0, 64) + " hours ago"
case dur.Hours() < 48:
return "1 day ago"
case dur.Hours() < 168:
return strconv.FormatFloat(dur.Hours()/24, 'f', 0, 64) + " days ago"
}
return datetime.AsTime().Format(format)
}
func add(x, y int32) int32 {
return x + y
}
func parseHTML(html string) template.HTML {
return template.HTML(html)
}

70
tpl/template.go Normal file
View File

@ -0,0 +1,70 @@
package tpl
import (
"html/template"
"io/fs"
"os"
"path/filepath"
"sync"
)
// Options for the template
type Options struct {
BaseDir string
BasePath string
DynamicReload bool
FuncMap template.FuncMap
Fsys fs.FS
}
var cache = make(map[string]template.Template)
var cacheMutex = new(sync.RWMutex)
// Load new template
func Load(opts *Options) (*template.Template, error) {
if !opts.DynamicReload {
if tpl, ok := getCache(opts.BasePath); ok {
return &tpl, nil
}
}
tpl, err := readFiles(opts.BasePath, opts)
if err != nil {
return nil, err
}
if !opts.DynamicReload {
setCache(opts.BasePath, *tpl)
}
return tpl, nil
}
func readFiles(basePath string, opts *Options) (*template.Template, error) {
var tpl *template.Template
var err error
if !opts.DynamicReload {
tpl, err = template.New(opts.BaseDir).
Funcs(FuncMap()).
Funcs(opts.FuncMap).
ParseFS(opts.Fsys, basePath+"/*.html", basePath+"/*/*.html", basePath+"/*/*/*.html")
if err != nil {
return nil, err
}
} else {
var files []string
if err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
files = append(files, path)
}
return nil
}); err != nil {
return nil, err
}
tpl, err = template.New(opts.BaseDir).
Funcs(FuncMap()).
Funcs(opts.FuncMap).
ParseFiles(files...)
if err != nil {
return nil, err
}
}
return tpl, nil
}