~repos /gromer
git clone https://pyrossh.dev/repos/gromer.git
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
fc24079a
—
Peter John 3 years ago
use handlers
_example/main.go
CHANGED
|
@@ -32,6 +32,7 @@ func main() {
|
|
|
32
32
|
|
|
33
33
|
staticRouter := baseRouter.NewRoute().Subrouter()
|
|
34
34
|
staticRouter.Use(gromer.CacheMiddleware)
|
|
35
|
+
staticRouter.Use(gromer.CompressMiddleware)
|
|
35
36
|
gromer.StaticRoute(staticRouter, "/gromer/", gromer_assets.FS)
|
|
36
37
|
gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
|
|
37
38
|
gromer.StylesRoute(staticRouter, "/styles.css")
|
go.mod
CHANGED
|
@@ -24,6 +24,7 @@ require (
|
|
|
24
24
|
github.com/aymerick/raymond v2.0.2+incompatible // indirect
|
|
25
25
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
|
26
26
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
27
|
+
github.com/felixge/httpsnoop v1.0.1 // indirect
|
|
27
28
|
github.com/go-playground/locales v0.14.0 // indirect
|
|
28
29
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
|
29
30
|
github.com/gobuffalo/envy v1.6.5 // indirect
|
|
@@ -33,6 +34,7 @@ require (
|
|
|
33
34
|
github.com/google/wire v0.5.0 // indirect
|
|
34
35
|
github.com/googleapis/gax-go/v2 v2.1.0 // indirect
|
|
35
36
|
github.com/gorilla/css v1.0.0 // indirect
|
|
37
|
+
github.com/gorilla/handlers v1.5.1 // indirect
|
|
36
38
|
github.com/joho/godotenv v1.3.0 // indirect
|
|
37
39
|
github.com/leodido/go-urn v1.2.1 // indirect
|
|
38
40
|
github.com/markbates/inflect v1.0.4 // indirect
|
go.sum
CHANGED
|
@@ -161,6 +161,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
|
|
|
161
161
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
|
162
162
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
|
163
163
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
164
|
+
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
|
165
|
+
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
164
166
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
|
165
167
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
|
166
168
|
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
|
@@ -284,6 +286,8 @@ github.com/googleapis/gax-go/v2 v2.1.0 h1:6DWmvNpomjL1+3liNSZbVns3zsYzzCjm6pRBO1
|
|
|
284
286
|
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
|
285
287
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
|
286
288
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
|
289
|
+
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
|
290
|
+
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
|
287
291
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|
288
292
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
|
289
293
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
http.go
CHANGED
|
@@ -19,8 +19,10 @@ import (
|
|
|
19
19
|
"sync"
|
|
20
20
|
"time"
|
|
21
21
|
|
|
22
|
+
"github.com/felixge/httpsnoop"
|
|
22
23
|
"github.com/go-playground/validator/v10"
|
|
23
24
|
"github.com/google/uuid"
|
|
25
|
+
"github.com/gorilla/handlers"
|
|
24
26
|
"github.com/gorilla/mux"
|
|
25
27
|
"github.com/pyros2097/gromer/gsx"
|
|
26
28
|
"github.com/rs/zerolog"
|
|
@@ -29,6 +31,12 @@ import (
|
|
|
29
31
|
"xojoc.pw/useragent"
|
|
30
32
|
)
|
|
31
33
|
|
|
34
|
+
const (
|
|
35
|
+
gzipEncoding = "gzip"
|
|
36
|
+
flateEncoding = "deflate"
|
|
37
|
+
acceptEncoding = "Accept-Encoding"
|
|
38
|
+
)
|
|
39
|
+
|
|
32
40
|
var (
|
|
33
41
|
info *debug.BuildInfo
|
|
34
42
|
IsCloundRun bool
|
|
@@ -73,12 +81,14 @@ func getFunctionName(temp interface{}) string {
|
|
|
73
81
|
func RespondError(w http.ResponseWriter, status int, err error) {
|
|
74
82
|
w.Header().Set("Content-Type", "application/json")
|
|
75
83
|
w.WriteHeader(status) // always write status last
|
|
76
|
-
w.(*LogResponseWriter).SetError(err)
|
|
77
84
|
merror := map[string]interface{}{
|
|
78
85
|
"error": err.Error(),
|
|
79
86
|
}
|
|
80
87
|
if status >= 500 {
|
|
81
88
|
merror["error"] = "Internal Server Error"
|
|
89
|
+
stack := string(debug.Stack())
|
|
90
|
+
println(stack)
|
|
91
|
+
log.WithLevel(zerolog.ErrorLevel).Err(err).Bool("panic", status == 599).Str("stack", stack).Stack()
|
|
82
92
|
}
|
|
83
93
|
validationErrors, ok := err.(validator.ValidationErrors)
|
|
84
94
|
if ok {
|
|
@@ -97,26 +107,6 @@ func GetRouteParams(route string) []string {
|
|
|
97
107
|
return params
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
func addRouteDef(method, route string, h interface{}) {
|
|
101
|
-
pathParams := GetRouteParams(route)
|
|
102
|
-
var body any = nil
|
|
103
|
-
funcType := reflect.TypeOf(h)
|
|
104
|
-
if funcType.NumIn() > len(pathParams)+2 {
|
|
105
|
-
structType := funcType.In(funcType.NumIn() - 1)
|
|
106
|
-
instance := reflect.New(structType)
|
|
107
|
-
if structType.Kind() != reflect.Struct {
|
|
108
|
-
log.Fatal().Msgf("router '%s' '%s' func final param should be a struct", method, route)
|
|
109
|
-
}
|
|
110
|
-
body = instance.Interface()
|
|
111
|
-
}
|
|
112
|
-
routeDefs = append(routeDefs, RouteDefinition{
|
|
113
|
-
Method: method,
|
|
114
|
-
Path: route,
|
|
115
|
-
PathParams: pathParams,
|
|
116
|
-
Params: body,
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
|
|
120
110
|
func PerformRequest(route string, h interface{}, ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
121
111
|
params := GetRouteParams(route)
|
|
122
112
|
renderContext := gsx.NewContext(ctx, r.Header.Get("HX-Request") == "true")
|
|
@@ -224,107 +214,30 @@ func PerformRequest(route string, h interface{}, ctx context.Context, w http.Res
|
|
|
224
214
|
w.Write(data)
|
|
225
215
|
}
|
|
226
216
|
|
|
227
|
-
type writeCounter int64
|
|
228
|
-
|
|
229
|
-
func (wc *writeCounter) Write(p []byte) (n int, err error) {
|
|
230
|
-
*wc += writeCounter(len(p))
|
|
231
|
-
return len(p), nil
|
|
232
|
-
}
|
|
233
|
-
func headerSize(h http.Header) int64 {
|
|
234
|
-
var wc writeCounter
|
|
235
|
-
h.Write(&wc)
|
|
236
|
-
return int64(wc) + 2 // for CRLF
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
type LogResponseWriter struct {
|
|
240
|
-
http.ResponseWriter
|
|
241
|
-
startTime time.Time
|
|
242
|
-
responseStatusCode int
|
|
243
|
-
responseContentLength int
|
|
244
|
-
responseHeaderSize int
|
|
245
|
-
err error
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
func NewLogResponseWriter(w http.ResponseWriter) *LogResponseWriter {
|
|
249
|
-
return &LogResponseWriter{ResponseWriter: w, startTime: time.Now()}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
func (w *LogResponseWriter) WriteHeader(code int) {
|
|
253
|
-
w.ResponseWriter.WriteHeader(code)
|
|
254
|
-
w.responseStatusCode = code
|
|
255
|
-
w.responseHeaderSize = int(headerSize(w.Header()))
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
func (w *LogResponseWriter) Write(body []byte) (int, error) {
|
|
259
|
-
w.responseContentLength += len(body)
|
|
260
|
-
return w.ResponseWriter.Write(body)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
func (w *LogResponseWriter) SetError(err error) {
|
|
264
|
-
w.err = err
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
func (w *LogResponseWriter) LogRequest(r *http.Request) {
|
|
268
|
-
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
269
|
-
if len(ip) > 0 && ip[0] == '[' {
|
|
270
|
-
ip = ip[1 : len(ip)-1]
|
|
271
|
-
}
|
|
272
|
-
logger := log.WithLevel(zerolog.InfoLevel)
|
|
273
|
-
if w.err != nil {
|
|
274
|
-
stack := string(debug.Stack())
|
|
275
|
-
println(stack)
|
|
276
|
-
logger = log.WithLevel(zerolog.ErrorLevel).Err(w.err).Str("stack", stack).Stack()
|
|
277
|
-
}
|
|
278
|
-
ua := useragent.Parse(r.UserAgent())
|
|
279
|
-
logger.Msgf("%s %d %.2f KB %s %s %s", r.Method,
|
|
280
|
-
w.responseStatusCode,
|
|
281
|
-
float32(w.responseContentLength)/1024.0,
|
|
282
|
-
time.Since(w.startTime).Round(time.Millisecond).String(), ua.Name, r.URL.Path)
|
|
283
|
-
// logger.
|
|
284
|
-
// Str("method", r.Method).
|
|
285
|
-
// Str("url", r.URL.String()).
|
|
286
|
-
// Int("header_size", int(headerSize(r.Header))).
|
|
287
|
-
// Int64("body_size", r.ContentLength).
|
|
288
|
-
// Str("host", r.Host).
|
|
289
|
-
// // Str("agent", r.UserAgent()).
|
|
290
|
-
// Str("referer", r.Referer()).
|
|
291
|
-
// Str("proto", r.Proto).
|
|
292
|
-
// Str("remote_ip", ip).
|
|
293
|
-
// Int("status", logRespWriter.responseStatusCode).
|
|
294
|
-
// Int("resp_header_size", logRespWriter.responseHeaderSize).
|
|
295
|
-
// Int("resp_body_size", logRespWriter.responseContentLength).
|
|
296
|
-
// Str("latency", time.Since(startTime).String()).
|
|
297
|
-
// Msgf("")
|
|
298
|
-
}
|
|
299
|
-
|
|
300
217
|
func LogMiddleware(next http.Handler) http.Handler {
|
|
301
218
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
302
|
-
logRespWriter := NewLogResponseWriter(w)
|
|
303
219
|
defer func() {
|
|
304
220
|
if err := recover(); err != nil {
|
|
305
|
-
RespondError(
|
|
221
|
+
RespondError(w, 599, fmt.Errorf("%+v", err))
|
|
306
|
-
logRespWriter.LogRequest(r)
|
|
307
222
|
}
|
|
308
223
|
}()
|
|
309
|
-
|
|
224
|
+
m := httpsnoop.CaptureMetrics(next, w, r)
|
|
225
|
+
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
226
|
+
if len(ip) > 0 && ip[0] == '[' {
|
|
310
|
-
|
|
227
|
+
ip = ip[1 : len(ip)-1]
|
|
311
|
-
return
|
|
312
228
|
}
|
|
229
|
+
log.WithLevel(zerolog.InfoLevel).Msgf("%s %d %.2fkb %s %s %s", r.Method,
|
|
230
|
+
m.Code,
|
|
313
|
-
|
|
231
|
+
float64(m.Written)/1024.0,
|
|
232
|
+
m.Duration.Round(time.Millisecond).String(),
|
|
233
|
+
useragent.Parse(r.UserAgent()).Name,
|
|
234
|
+
r.URL.Path,
|
|
235
|
+
)
|
|
314
236
|
})
|
|
315
237
|
}
|
|
316
238
|
|
|
317
|
-
func
|
|
239
|
+
func CompressMiddleware(next http.Handler) http.Handler {
|
|
318
|
-
return
|
|
240
|
+
return handlers.CompressHandler(next)
|
|
319
|
-
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
320
|
-
w.Header().Set("Access-Control-Allow-Methods", "*")
|
|
321
|
-
w.Header().Set("Access-Control-Allow-Headers", "*")
|
|
322
|
-
if r.Method == "OPTIONS" {
|
|
323
|
-
w.WriteHeader(200)
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
next.ServeHTTP(w, r)
|
|
327
|
-
})
|
|
328
241
|
}
|
|
329
242
|
|
|
330
243
|
func CacheMiddleware(next http.Handler) http.Handler {
|
|
@@ -367,7 +280,6 @@ func StylesRoute(router *mux.Router, path string) {
|
|
|
367
280
|
}
|
|
368
281
|
|
|
369
282
|
func Handle(router *mux.Router, method, route string, h interface{}) {
|
|
370
|
-
addRouteDef(method, route, h)
|
|
371
283
|
router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
|
|
372
284
|
ctx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
373
285
|
PerformRequest(route, h, ctx, w, r)
|