router and tpl packages
Signed-off-by: Michael <michael.lindman@gmail.com>
This commit is contained in:
parent
cb2095dfac
commit
15e52bd393
|
@ -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
5
go.mod
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
Reference in New Issue