ginzip/ginzip.go
Theta-Dev 32a105e462
All checks were successful
continuous-integration/drone/push Build is passing
use map for ignored extensions
2021-12-17 14:18:40 +01:00

224 lines
4.9 KiB
Go

package ginzip
import (
"compress/gzip"
"fmt"
"log"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/andybalholm/brotli"
"github.com/gin-gonic/gin"
)
// Options object is used to configure Ginzip.
type Options struct {
// Compression levels for gzip/brotli
// Default setting: ""/"d"/"default"
// Best speed: "min"/"speed"
// Best compression: "max"
// Compression disabled: "false"/"n"/"no"
// Manual setting: 1-9 (gzip), 0-11 (brotli)
GzipLevel string
BrotliLevel string
// Filepath extensions where compression should be skipped
SkipExtensions []string
}
type intOptions struct {
GzipEn bool
GzipLevel int
BrotliEn bool
BrotliLevel int
SkipExtensions map[string]bool
}
// New creates a new Ginzip middleware function.
// Attach to your Gin router like this:
// router.Use(ginzip.New(ginzip.DefaultOptions()))
//
// Or use groups:
// ui := r.Group("/", ginzip.New(ginzip.DefaultOptions()))
func New(options Options) gin.HandlerFunc {
opt := parseOptions(options)
return func(c *gin.Context) {
gWriter := getGinzipWriter(c, opt)
if gWriter == nil {
return
}
c.Writer = gWriter
c.Header("Content-Encoding", gWriter.Encoding())
c.Header("Vary", "Accept-Encoding")
defer func() {
gWriter.Close()
c.Header("Content-Length", fmt.Sprint(c.Writer.Size()))
}()
c.Next()
}
}
// DefaultOptions creates a new config object with default values:
//
// Default compression level, ignore most common binary filetypes
// .png, .gif, .jpeg, .jpg, .mp3, .mp4, .ogg, .zip, .exe
func DefaultOptions() Options {
return Options{
GzipLevel: "",
BrotliLevel: "",
SkipExtensions: []string{
".png", ".gif", ".jpeg", ".jpg", ".mp3", ".mp4", ".ogg",
".zip", ".exe",
},
}
}
// IgnoreExt adds more ignored file extension to the Options object.
func (o Options) IgnoreExt(exts ...string) Options {
o.SkipExtensions = append(o.SkipExtensions, exts...)
return o
}
// ginzipWriter is Ginzip's interface for the individual writer types
// handling the different compression algorithms.
type ginzipWriter interface {
gin.ResponseWriter
WriteString(s string) (int, error)
Write(data []byte) (int, error)
Close() error
Encoding() string
}
type gzipWriter struct {
gin.ResponseWriter
writer *gzip.Writer
}
type brotliWriter struct {
gin.ResponseWriter
writer *brotli.Writer
}
func (g *gzipWriter) WriteString(s string) (int, error) {
return g.writer.Write([]byte(s))
}
func (g *gzipWriter) Write(data []byte) (int, error) {
return g.writer.Write(data)
}
func (g *gzipWriter) Close() error {
return g.writer.Close()
}
func (g *gzipWriter) Encoding() string {
return "gzip"
}
func (b *brotliWriter) WriteString(s string) (int, error) {
return b.writer.Write([]byte(s))
}
func (b *brotliWriter) Write(data []byte) (int, error) {
return b.writer.Write(data)
}
func (b *brotliWriter) Close() error {
return b.writer.Close()
}
func (b *brotliWriter) Encoding() string {
return "br"
}
func shouldCompress(req *http.Request, skipExtensions map[string]bool) bool {
// Dont compress websocket connections
if strings.Contains(req.Header.Get("Connection"), "Upgrade") ||
strings.Contains(req.Header.Get("Content-Type"), "text/event-stream") {
return false
}
extension := filepath.Ext(req.URL.Path)
if extension == "" {
return true
}
_, doSkip := skipExtensions[extension]
return !doSkip
}
func parseLevel(level string, dfault int, min int, max int) (int, bool) {
level = strings.ToLower(level)
switch level {
case "", "default", "d":
return dfault, true
case "false", "no", "n":
return 0, false
case "max":
return max, true
case "min", "speed":
return min, true
}
i, err := strconv.Atoi(level)
if err == nil && i >= min && i <= max {
return i, true
}
log.Printf("Ginzip level %s is invalid, falling back to default", level)
return dfault, true
}
func parseOptions(options Options) intOptions {
gzipLvl, gzipEn := parseLevel(
options.GzipLevel,
gzip.DefaultCompression, gzip.BestSpeed, gzip.BestCompression,
)
brLvl, brEn := parseLevel(
options.BrotliLevel,
brotli.DefaultCompression, brotli.BestSpeed, brotli.BestCompression,
)
exts := map[string]bool{}
for _, ext := range options.SkipExtensions {
exts[ext] = true
}
return intOptions{
GzipEn: gzipEn,
GzipLevel: gzipLvl,
BrotliEn: brEn,
BrotliLevel: brLvl,
SkipExtensions: exts,
}
}
func getGinzipWriter(c *gin.Context, options intOptions) ginzipWriter {
if !shouldCompress(c.Request, options.SkipExtensions) {
return nil
}
if strings.Contains(c.Request.Header.Get("Accept-Encoding"), "br") &&
options.BrotliEn {
br := brotli.NewWriterLevel(c.Writer, options.BrotliLevel)
return &brotliWriter{c.Writer, br}
}
if strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") &&
options.GzipEn {
gz, err := gzip.NewWriterLevel(c.Writer, options.GzipLevel)
if err != nil {
return nil
}
return &gzipWriter{c.Writer, gz}
}
return nil
}