wip
This commit is contained in:
5
webutil/README.md
Normal file
5
webutil/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# webutil
|
||||
|
||||
## Roadmap
|
||||
|
||||
* logging middleware
|
||||
10
webutil/go.mod
Normal file
10
webutil/go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
module git.crumpington.com/lib/webutil
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require golang.org/x/crypto v0.28.0
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
)
|
||||
6
webutil/go.sum
Normal file
6
webutil/go.sum
Normal file
@@ -0,0 +1,6 @@
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
24
webutil/listenandserve.go
Normal file
24
webutil/listenandserve.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package webutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// Serve requests using the given http.Server. If srv.Addr has the format
|
||||
// `hostname:https`, then use autocert to manage certificates for the domain.
|
||||
//
|
||||
// For http on port 80, you can use :http.
|
||||
func ListenAndServe(srv *http.Server) error {
|
||||
if strings.HasSuffix(srv.Addr, ":https") {
|
||||
hostname := strings.TrimSuffix(srv.Addr, ":https")
|
||||
if len(hostname) == 0 {
|
||||
return errors.New("https requires a hostname")
|
||||
}
|
||||
return srv.Serve(autocert.NewListener(hostname))
|
||||
}
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
47
webutil/middleware-logging.go
Normal file
47
webutil/middleware-logging.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package webutil
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _log = log.New(os.Stderr, "", 0)
|
||||
|
||||
type responseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
httpStatus int
|
||||
responseSize int
|
||||
}
|
||||
|
||||
func (w *responseWriterWrapper) WriteHeader(status int) {
|
||||
w.httpStatus = status
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (w *responseWriterWrapper) Write(b []byte) (int, error) {
|
||||
if w.httpStatus == 0 {
|
||||
w.httpStatus = 200
|
||||
}
|
||||
w.responseSize += len(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func WithLogging(inner http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
t := time.Now()
|
||||
wrapper := responseWriterWrapper{w, 0, 0}
|
||||
|
||||
inner(&wrapper, r)
|
||||
_log.Printf("%s \"%s %s %s\" %d %d %v\n",
|
||||
r.RemoteAddr,
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
r.Proto,
|
||||
wrapper.httpStatus,
|
||||
wrapper.responseSize,
|
||||
time.Since(t),
|
||||
)
|
||||
}
|
||||
}
|
||||
100
webutil/template.go
Normal file
100
webutil/template.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package webutil
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseTemplateSet parses sets of templates from an embed.FS.
|
||||
//
|
||||
// Each directory constitutes a set of templates that are parsed together.
|
||||
//
|
||||
// Structure (within a directory):
|
||||
// - share/* are always parsed.
|
||||
// - base.html will be parsed with each other file in same dir
|
||||
//
|
||||
// Call a template with m[path].Execute(w, data) (root dir name is excluded).
|
||||
//
|
||||
// For example, if you have
|
||||
// - /user/share/*
|
||||
// - /user/base.html
|
||||
// - /user/home.html
|
||||
//
|
||||
// Then you call m["/user/home.html"].Execute(w, data).
|
||||
func ParseTemplateSet(funcs template.FuncMap, fs embed.FS) map[string]*template.Template {
|
||||
m := map[string]*template.Template{}
|
||||
rootDir := readDir(fs, ".")[0].Name()
|
||||
loadTemplateDir(fs, funcs, m, rootDir, rootDir)
|
||||
return m
|
||||
}
|
||||
|
||||
func loadTemplateDir(
|
||||
fs embed.FS,
|
||||
funcs template.FuncMap,
|
||||
m map[string]*template.Template,
|
||||
dirPath string,
|
||||
rootDir string,
|
||||
) map[string]*template.Template {
|
||||
t := template.New("")
|
||||
if funcs != nil {
|
||||
t = t.Funcs(funcs)
|
||||
}
|
||||
|
||||
shareDir := path.Join(dirPath, "share")
|
||||
if _, err := fs.ReadDir(shareDir); err == nil {
|
||||
log.Printf("Parsing %s...", path.Join(shareDir, "*"))
|
||||
t = template.Must(t.ParseFS(fs, path.Join(shareDir, "*")))
|
||||
}
|
||||
|
||||
if data, _ := fs.ReadFile(path.Join(dirPath, "base.html")); data != nil {
|
||||
log.Printf("Parsing %s...", path.Join(dirPath, "base.html"))
|
||||
t = template.Must(t.Parse(string(data)))
|
||||
}
|
||||
|
||||
for _, ent := range readDir(fs, dirPath) {
|
||||
if ent.Type().IsDir() {
|
||||
if ent.Name() != "share" {
|
||||
m = loadTemplateDir(fs, funcs, m, path.Join(dirPath, ent.Name()), rootDir)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !ent.Type().IsRegular() {
|
||||
continue
|
||||
}
|
||||
|
||||
if ent.Name() == "base.html" {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := path.Join(dirPath, ent.Name())
|
||||
log.Printf("Parsing %s...", filePath)
|
||||
|
||||
key := strings.TrimPrefix(path.Join(dirPath, ent.Name()), rootDir)
|
||||
tt := template.Must(t.Clone())
|
||||
tt = template.Must(tt.Parse(readFile(fs, filePath)))
|
||||
m[key] = tt
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func readDir(fs embed.FS, dirPath string) []fs.DirEntry {
|
||||
ents, err := fs.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ents
|
||||
}
|
||||
|
||||
func readFile(fs embed.FS, path string) string {
|
||||
data, err := fs.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
49
webutil/template_test.go
Normal file
49
webutil/template_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package webutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed all:test-templates
|
||||
var testFS embed.FS
|
||||
|
||||
func TestParseTemplateSet(t *testing.T) {
|
||||
funcs := template.FuncMap{"join": strings.Join}
|
||||
m := ParseTemplateSet(funcs, testFS)
|
||||
|
||||
type TestCase struct {
|
||||
Key string
|
||||
Data any
|
||||
Out string
|
||||
}
|
||||
|
||||
cases := []TestCase{
|
||||
{
|
||||
Key: "/home.html",
|
||||
Data: "DATA",
|
||||
Out: "<p>HOME!</p>",
|
||||
}, {
|
||||
Key: "/about.html",
|
||||
Data: "DATA",
|
||||
Out: "<p><b>DATA</b></p>",
|
||||
}, {
|
||||
Key: "/contact.html",
|
||||
Data: []string{"a", "b", "c"},
|
||||
Out: "<p>a,b,c</p>",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
b := &bytes.Buffer{}
|
||||
m[tc.Key].Execute(b, tc.Data)
|
||||
out := strings.TrimSpace(b.String())
|
||||
if out != tc.Out {
|
||||
t.Fatalf("%s != %s", out, tc.Out)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1
webutil/test-templates/about.html
Normal file
1
webutil/test-templates/about.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "body"}}{{template "bold" .}}{{end}}
|
||||
1
webutil/test-templates/base.html
Normal file
1
webutil/test-templates/base.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>{{block "body" .}}default{{end}}</p>
|
||||
1
webutil/test-templates/contact.html
Normal file
1
webutil/test-templates/contact.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "body"}}{{join . ","}}{{end}}
|
||||
1
webutil/test-templates/home.html
Normal file
1
webutil/test-templates/home.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "body"}}HOME!{{end}}
|
||||
1
webutil/test-templates/share/bold.html
Normal file
1
webutil/test-templates/share/bold.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "bold"}}<b>{{.}}</b>{{end}}
|
||||
1
webutil/test-templates/share/italic.html
Normal file
1
webutil/test-templates/share/italic.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "italic"}}<i>{{.}}</i>{{end}}
|
||||
Reference in New Issue
Block a user