diff --git a/config.go b/config.go index a3de203..f93ff77 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,9 @@ type Config struct { // Response headers to add to every static. Response map[string]string `mapstructure:"response"` + + // GzipEnabled determines if gzip compression is enabled for serving static files. + GzipEnabled bool `mapstructure:"gzip_enabled"` } // Valid returns nil if config is valid. diff --git a/go.mod b/go.mod index 9a1ea6a..33671a2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23 toolchain go1.23.4 require ( + github.com/klauspost/compress v1.17.11 github.com/roadrunner-server/context v1.0.2 github.com/roadrunner-server/errors v1.4.1 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 diff --git a/go.sum b/go.sum index 541ecb8..b4bc128 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/roadrunner-server/context v1.0.2 h1:MSK6mjG2KsFKS1IoieMrY9Ia7SGxDGh/tagpBQxi1SE= diff --git a/plugin.go b/plugin.go index 7b5b885..3895403 100644 --- a/plugin.go +++ b/plugin.go @@ -2,6 +2,8 @@ package static import ( "fmt" + "github.com/klauspost/compress/gzhttp" + "io" "net/http" "path" "strings" @@ -48,6 +50,9 @@ type Plugin struct { forbiddenExtensions map[string]struct{} // opentelemetry prop propagation.TextMapPropagator + + // gzipEnabled indicates if gzip compression is enabled for serving static files. + gzipEnabled bool } // Init must return configure service and return true if the service hasStatus enabled. Must return error in case of @@ -79,6 +84,7 @@ func (s *Plugin) Init(cfg Configurer, log Logger) error { s.log = log.NamedLogger(PluginName) s.root = http.Dir(s.cfg.Dir) + s.gzipEnabled = s.cfg.GzipEnabled // init forbidden for i := 0; i < len(s.cfg.Forbid); i++ { @@ -225,6 +231,26 @@ func (s *Plugin) Middleware(next http.Handler) http.Handler { //nolint:gocognit, } } + if s.gzipEnabled && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + s.log.Debug("gzip compression requested") + // Skip compression for already compressed formats + contentType := http.DetectContentType(make([]byte, 512)) + if strings.Contains(contentType, "image/") || + strings.Contains(contentType, "video/") || + strings.Contains(contentType, "audio/") { + s.log.Debug("skipping compression for already compressed content", + zap.String("type", contentType)) + } else { + gzhttp.GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.Copy(w, f) + if err != nil { + s.log.Error("failed to copy file to response: ", zap.Error(err)) + } + })).ServeHTTP(w, r) + return + } + } + // we passed all checks - serve the file http.ServeContent(w, r, finfo.Name(), finfo.ModTime(), f) })