aboutsummaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/ext/compression.go142
-rw-r--r--pkg/ext/compression_test.go42
-rw-r--r--pkg/ext/mime.go24
-rw-r--r--pkg/handler/git/handler.go7
-rw-r--r--pkg/handler/router.go25
-rw-r--r--pkg/handler/static/handler.go11
-rw-r--r--pkg/u/list.go8
-rw-r--r--pkg/u/list_test.go35
8 files changed, 282 insertions, 12 deletions
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)
+ }
+
+ })
+ }
+}