226 lines
4.9 KiB
Go
226 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 []string
|
|
}
|
|
|
|
// 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 containsString(s []string, e string) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func shouldCompress(req *http.Request, skipExtensions []string) 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
|
|
}
|
|
|
|
return !containsString(skipExtensions, extension)
|
|
}
|
|
|
|
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,
|
|
)
|
|
|
|
return intOptions{
|
|
GzipEn: gzipEn,
|
|
GzipLevel: gzipLvl,
|
|
BrotliEn: brEn,
|
|
BrotliLevel: brLvl,
|
|
SkipExtensions: options.SkipExtensions,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|