From c7a8aa113a914e70e027fea93265c7232b865b5e Mon Sep 17 00:00:00 2001 From: "Gabriel A. Giovanini" Date: Fri, 7 Jun 2024 19:33:07 +0200 Subject: feat: Add compression --- pkg/ext/compression.go | 142 ++++++++++++++++++++++++++++++++++++++++++ pkg/ext/compression_test.go | 42 +++++++++++++ pkg/ext/mime.go | 24 +++++++ pkg/handler/git/handler.go | 7 +++ pkg/handler/router.go | 25 +++++--- pkg/handler/static/handler.go | 11 +++- pkg/u/list.go | 8 +++ pkg/u/list_test.go | 35 +++++++++++ 8 files changed, 282 insertions(+), 12 deletions(-) create mode 100644 pkg/ext/compression.go create mode 100644 pkg/ext/compression_test.go create mode 100644 pkg/ext/mime.go (limited to 'pkg') diff --git a/pkg/ext/compression.go b/pkg/ext/compression.go new file mode 100644 index 0000000..92144b8 --- /dev/null +++ b/pkg/ext/compression.go @@ -0,0 +1,142 @@ +package ext + +import ( + "compress/gzip" + "compress/lzw" + "errors" + "io" + "log/slog" + "net/http" + "strconv" + "strings" + + "git.gabrielgio.me/cerrado/pkg/u" + "github.com/andybalholm/brotli" + "github.com/klauspost/compress/zstd" +) + +var ( + invalidParamErr = errors.New("Invalid weighted param") +) + +type CompressionResponseWriter struct { + innerWriter http.ResponseWriter + compressWriter io.Writer +} + +func Compress(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if accept, ok := r.Header["Accept-Encoding"]; ok { + if compress, algo := GetCompressionWriter(u.FirstOrZero(accept), w); algo != "" { + defer compress.Close() + w.Header().Add("Content-Encoding", algo) + w = &CompressionResponseWriter{ + innerWriter: w, + compressWriter: compress, + } + } + } + next(w, r) + } +} + +func GetCompressionWriter(header string, inner io.Writer) (io.WriteCloser, string) { + c := GetCompression(header) + switch c { + case "br": + return GetBrotliWriter(inner), c + case "gzip": + return GetGZIPWriter(inner), c + case "compress": + return GetLZWWriter(inner), c + case "zstd": + return GetZSTDWriter(inner), c + default: + return nil, "" + } + +} + +func (c *CompressionResponseWriter) Header() http.Header { + return c.innerWriter.Header() +} +func (c *CompressionResponseWriter) Write(b []byte) (int, error) { + return c.compressWriter.Write(b) +} + +func (c *CompressionResponseWriter) WriteHeader(statusCode int) { + c.innerWriter.WriteHeader(statusCode) +} + +func GetCompression(header string) string { + c := "*" + q := 0.0 + + if header == "" { + return c + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding + for _, e := range strings.Split(header, ",") { + ps := strings.Split(e, ";") + if len(ps) == 2 { + w, err := getWeighedValue(ps[1]) + if err != nil { + slog.Error( + "Error parsing weighting from Accept-Encoding", + "error", err, + ) + continue + } + // gettting weighting value + if w > q { + q = w + c = strings.Trim(ps[0], " ") + } + } else { + if 1 > q { + q = 1 + c = strings.Trim(ps[0], " ") + } + } + } + + return c +} + +func GetGZIPWriter(w io.Writer) io.WriteCloser { + // error can be ignored here since it will only err when compression level + // is not valid + r, _ := gzip.NewWriterLevel(w, gzip.BestCompression) + return r +} + +func GetBrotliWriter(w io.Writer) io.WriteCloser { + return brotli.NewWriterLevel(w, brotli.BestCompression) +} + +func GetZSTDWriter(w io.Writer) io.WriteCloser { + // error can be ignored here since it will only opts are given + r, _ := zstd.NewWriter(w) + return r +} + +func GetLZWWriter(w io.Writer) io.WriteCloser { + return lzw.NewWriter(w, lzw.LSB, 8) +} + +func getWeighedValue(part string) (float64, error) { + ps := strings.SplitN(part, "=", 2) + if len(ps) != 2 { + return 0, invalidParamErr + } + if name := strings.TrimSpace(ps[0]); name == "q" { + w, err := strconv.ParseFloat(ps[1], 64) + if err != nil { + return 0, err + } + return w, nil + } + + return 0, invalidParamErr +} diff --git a/pkg/ext/compression_test.go b/pkg/ext/compression_test.go new file mode 100644 index 0000000..6424378 --- /dev/null +++ b/pkg/ext/compression_test.go @@ -0,0 +1,42 @@ +// go:build unit +package ext + +import "testing" + +func TestGetCompression(t *testing.T) { + testCases := []struct { + name string + header string + compression string + }{ + { + name: "Empty", + header: "", + compression: "*", + }, + { + name: "Weighted", + header: "gzip;q=1.0, *;q=0.5", + compression: "gzip", + }, + { + name: "Mixed", + header: "deflate, gzip;q=1.0, *;q=0.5", + compression: "deflate", + }, + { + name: "Not weighted", + header: "zstd, deflate, gzip", + compression: "zstd", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := GetCompression(tc.header) + if got != tc.compression { + t.Errorf("Wrong compression returned: got %s want %s", got, tc.compression) + } + }) + } +} diff --git a/pkg/ext/mime.go b/pkg/ext/mime.go new file mode 100644 index 0000000..6da66e3 --- /dev/null +++ b/pkg/ext/mime.go @@ -0,0 +1,24 @@ +package ext + +import "net/http" + +type ContentType = string + +const ( + TextHTML ContentType = "text/html" +) + +func Html(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + next(w, r) + } +} + +func SetHTML(w http.ResponseWriter) { + SetMIME(w, TextHTML) + +} + +func SetMIME(w http.ResponseWriter, mime ContentType) { + w.Header().Add("Content-Type", mime) +} diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go index f3e74c7..28cc99e 100644 --- a/pkg/handler/git/handler.go +++ b/pkg/handler/git/handler.go @@ -6,6 +6,7 @@ import ( "net/http" "path/filepath" + "git.gabrielgio.me/cerrado/pkg/ext" "git.gabrielgio.me/cerrado/pkg/service" "git.gabrielgio.me/cerrado/templates" "github.com/alecthomas/chroma/v2" @@ -50,6 +51,7 @@ func (g *GitHandler) List(w http.ResponseWriter, _ *http.Request) { } func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) { + ext.SetHTML(w) name := r.PathValue("name") ref, err := g.gitService.GetHead(name) if err != nil { @@ -66,6 +68,7 @@ func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) { } func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) { + ext.SetHTML(w) name := r.PathValue("name") ref, err := g.gitService.GetHead(name) if err != nil { @@ -81,6 +84,7 @@ func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) { } func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) { + ext.SetHTML(w) name := r.PathValue("name") tags, err := g.gitService.ListTags(name) @@ -113,6 +117,7 @@ func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) { } func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) { + ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") rest := r.PathValue("rest") @@ -137,6 +142,7 @@ func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) { } func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) { + ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") rest := r.PathValue("rest") @@ -178,6 +184,7 @@ func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) { } func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) { + ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") diff --git a/pkg/handler/router.go b/pkg/handler/router.go index ed782f7..de5117c 100644 --- a/pkg/handler/router.go +++ b/pkg/handler/router.go @@ -4,6 +4,7 @@ import ( "net/http" serverconfig "git.gabrielgio.me/cerrado/pkg/config" + "git.gabrielgio.me/cerrado/pkg/ext" "git.gabrielgio.me/cerrado/pkg/handler/about" "git.gabrielgio.me/cerrado/pkg/handler/config" "git.gabrielgio.me/cerrado/pkg/handler/git" @@ -31,15 +32,19 @@ func MountHandler( mux := http.NewServeMux() - mux.HandleFunc("/static/{file}", staticHandler) - mux.HandleFunc("/{name}/about/{$}", gitHandler.About) - mux.HandleFunc("/{name}", gitHandler.Summary) - mux.HandleFunc("/{name}/refs/{$}", gitHandler.Refs) - mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree) - mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob) - mux.HandleFunc("/{name}/log/{ref}", gitHandler.Log) - mux.HandleFunc("/config", configHander) - mux.HandleFunc("/about", aboutHandler.About) - mux.HandleFunc("/", gitHandler.List) + mux.HandleFunc("/static/{file}", m(staticHandler)) + mux.HandleFunc("/{name}/about/{$}", m(gitHandler.About)) + mux.HandleFunc("/{name}", m(gitHandler.Summary)) + mux.HandleFunc("/{name}/refs/{$}", m(gitHandler.Refs)) + mux.HandleFunc("/{name}/tree/{ref}/{rest...}", m(gitHandler.Tree)) + mux.HandleFunc("/{name}/blob/{ref}/{rest...}", m(gitHandler.Blob)) + mux.HandleFunc("/{name}/log/{ref}", m(gitHandler.Log)) + mux.HandleFunc("/config", m(configHander)) + mux.HandleFunc("/about", m(aboutHandler.About)) + mux.HandleFunc("/", m(gitHandler.List)) return mux, nil } + +func m(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return ext.Compress(next) +} diff --git a/pkg/handler/static/handler.go b/pkg/handler/static/handler.go index a8b4583..5155068 100644 --- a/pkg/handler/static/handler.go +++ b/pkg/handler/static/handler.go @@ -2,8 +2,11 @@ package static import ( "io/fs" + "mime" "net/http" + "path/filepath" + "git.gabrielgio.me/cerrado/pkg/ext" "git.gabrielgio.me/cerrado/static" ) @@ -14,8 +17,12 @@ func ServeStaticHandler() (func(w http.ResponseWriter, r *http.Request), error) } return func(w http.ResponseWriter, r *http.Request) { - f := r.PathValue("file") - + var ( + f = r.PathValue("file") + e = filepath.Ext(f) + m = mime.TypeByExtension(e) + ) + ext.SetMIME(w, m) http.ServeFileFS(w, r, staticFs, f) }, nil } diff --git a/pkg/u/list.go b/pkg/u/list.go index 34eafd1..cf71909 100644 --- a/pkg/u/list.go +++ b/pkg/u/list.go @@ -8,6 +8,14 @@ func First[T any](v []T) (T, bool) { return v[0], true } +func FirstOrZero[T any](v []T) T { + if len(v) == 0 { + var zero T + return zero + } + return v[0] +} + func ChunkBy[T any](items []T, chunkSize int) [][]T { var chunks = make([][]T, 0, (len(items)/chunkSize)+1) for chunkSize < len(items) { diff --git a/pkg/u/list_test.go b/pkg/u/list_test.go index a6d84c7..805a209 100644 --- a/pkg/u/list_test.go +++ b/pkg/u/list_test.go @@ -94,3 +94,38 @@ func TestSubList(t *testing.T) { }) } } + +func TestFirstOrZero(t *testing.T) { + testCases := []struct { + name string + slice []int + first int + }{ + { + name: "multiple items slice", + slice: []int{1, 2, 3}, + first: 1, + }, + { + name: "single item slice", + slice: []int{1}, + first: 1, + }, + { + name: "empty slice", + slice: []int{}, + first: 0, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + first := FirstOrZero(tc.slice) + + if first != tc.first { + t.Errorf("Error first, want %d got %d", tc.first, first) + } + + }) + } +} -- cgit v1.2.3