gohttp/vendor/github.com/nanmu42/gzip/writerwrapper.go

286 lines
6.7 KiB
Go
Raw Normal View History

2023-12-21 18:36:51 +08:00
package gzip
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/klauspost/compress/gzip"
)
// writerWrapper wraps the originalHandler
// to test whether to gzip and gzip the body if applicable.
type writerWrapper struct {
// header filter are applied by its sequence
Filters []ResponseHeaderFilter
// min content length to enable compress
MinContentLength int64
OriginWriter http.ResponseWriter
// use initGzipWriter() to init gzipWriter when in need
GetGzipWriter func() *gzip.Writer
// must close gzip writer and put it back to pool
PutGzipWriter func(*gzip.Writer)
// internal below
// *** WARNING ***
// *writerWrapper.Reset() method must be updated
// upon following field changing
// compress or not
// default to true
shouldCompress bool
// whether body is large enough
bodyBigEnough bool
// is header already flushed?
headerFlushed bool
responseHeaderChecked bool
statusCode int
// how many raw bytes has been written
size int
gzipWriter *gzip.Writer
bodyBuffer []byte
}
// interface guard
var _ http.ResponseWriter = (*writerWrapper)(nil)
var _ http.Flusher = (*writerWrapper)(nil)
func newWriterWrapper(filters []ResponseHeaderFilter, minContentLength int64, originWriter http.ResponseWriter, getGzipWriter func() *gzip.Writer, putGzipWriter func(*gzip.Writer)) *writerWrapper {
return &writerWrapper{
shouldCompress: true,
bodyBuffer: make([]byte, 0, minContentLength),
Filters: filters,
MinContentLength: minContentLength,
OriginWriter: originWriter,
GetGzipWriter: getGzipWriter,
PutGzipWriter: putGzipWriter,
}
}
// Reset the wrapper into a fresh one,
// writing to originWriter
func (w *writerWrapper) Reset(originWriter http.ResponseWriter) {
w.OriginWriter = originWriter
// internal below
// reset status with caution
// all internal fields should be taken good care
w.shouldCompress = true
w.headerFlushed = false
w.responseHeaderChecked = false
w.bodyBigEnough = false
w.statusCode = 0
w.size = 0
if w.gzipWriter != nil {
w.PutGzipWriter(w.gzipWriter)
w.gzipWriter = nil
}
if w.bodyBuffer != nil {
w.bodyBuffer = w.bodyBuffer[:0]
}
}
func (w *writerWrapper) Status() int {
return w.statusCode
}
func (w *writerWrapper) Size() int {
return w.size
}
func (w *writerWrapper) Written() bool {
return w.headerFlushed || len(w.bodyBuffer) > 0
}
func (w *writerWrapper) WriteHeaderCalled() bool {
return w.statusCode != 0
}
func (w *writerWrapper) initGzipWriter() {
w.gzipWriter = w.GetGzipWriter()
w.gzipWriter.Reset(w.OriginWriter)
}
// Header implements http.ResponseWriter
func (w *writerWrapper) Header() http.Header {
return w.OriginWriter.Header()
}
// Write implements http.ResponseWriter
func (w *writerWrapper) Write(data []byte) (int, error) {
w.size += len(data)
if !w.WriteHeaderCalled() {
w.WriteHeader(http.StatusOK)
}
if !w.shouldCompress {
w.WriteHeaderNow()
return w.OriginWriter.Write(data)
}
if w.bodyBigEnough {
return w.gzipWriter.Write(data)
}
// fast check
if !w.responseHeaderChecked {
w.responseHeaderChecked = true
header := w.Header()
for _, filter := range w.Filters {
w.shouldCompress = filter.ShouldCompress(header)
if !w.shouldCompress {
w.WriteHeaderNow()
return w.OriginWriter.Write(data)
}
}
if w.enoughContentLength() {
w.bodyBigEnough = true
w.WriteHeaderNow()
w.initGzipWriter()
return w.gzipWriter.Write(data)
}
}
if !w.writeBuffer(data) {
w.bodyBigEnough = true
// detect Content-Type if there's none
if header := w.Header(); header.Get("Content-Type") == "" {
header.Set("Content-Type", http.DetectContentType(w.bodyBuffer))
}
w.WriteHeaderNow()
w.initGzipWriter()
if len(w.bodyBuffer) > 0 {
written, err := w.gzipWriter.Write(w.bodyBuffer)
if err != nil {
err = fmt.Errorf("w.gzipWriter.Write: %w", err)
return written, err
}
}
return w.gzipWriter.Write(data)
}
return len(data), nil
}
func (w *writerWrapper) writeBuffer(data []byte) (fit bool) {
if int64(len(data)+len(w.bodyBuffer)) > w.MinContentLength {
return false
}
w.bodyBuffer = append(w.bodyBuffer, data...)
return true
}
func (w *writerWrapper) enoughContentLength() bool {
contentLength, err := strconv.ParseInt(w.Header().Get("Content-Length"), 10, 64)
if err != nil {
return false
}
if contentLength != 0 && contentLength >= w.MinContentLength {
return true
}
return false
}
// WriteHeader implements http.ResponseWriter
//
// WriteHeader does not really calls originalHandler's WriteHeader,
// and the calling will actually be handler by WriteHeaderNow().
//
// http.ResponseWriter does not specify clearly whether permitting
// updating status code on second call to WriteHeader(), and it's
// conflicting between http and gin's implementation.
// Here, gzip consider second(and furthermore) calls to WriteHeader()
// valid. WriteHeader() is disabled after flushing header.
// Do note setting status code to 204 or 304 marks content uncompressable,
// and a later status code change does not revert this.
func (w *writerWrapper) WriteHeader(statusCode int) {
if w.headerFlushed {
return
}
w.statusCode = statusCode
if !w.shouldCompress {
return
}
if statusCode == http.StatusNoContent ||
statusCode == http.StatusNotModified {
w.shouldCompress = false
return
}
}
// WriteHeaderNow Forces to write the http header (status code + headers).
//
// WriteHeaderNow must always be called and called after
// WriteHeader() is called and
// w.shouldCompress is decided.
//
// This method is usually called by gin's AbortWithStatus()
func (w *writerWrapper) WriteHeaderNow() {
if w.headerFlushed {
return
}
// if neither WriteHeader() or Write() are called,
// do nothing
if !w.WriteHeaderCalled() {
return
}
if w.shouldCompress {
header := w.Header()
header.Del("Content-Length")
header.Set("Content-Encoding", "gzip")
header.Add("Vary", "Accept-Encoding")
originalEtag := w.Header().Get("ETag")
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
w.Header().Set("ETag", "W/"+originalEtag)
}
}
w.OriginWriter.WriteHeader(w.statusCode)
w.headerFlushed = true
}
// FinishWriting flushes header and closed gzip writer
//
// Write() and WriteHeader() should not be called
// after FinishWriting()
func (w *writerWrapper) FinishWriting() {
// still buffering
if w.shouldCompress && !w.bodyBigEnough {
w.shouldCompress = false
w.WriteHeaderNow()
if len(w.bodyBuffer) > 0 {
_, _ = w.OriginWriter.Write(w.bodyBuffer)
}
}
w.WriteHeaderNow()
if w.gzipWriter != nil {
w.PutGzipWriter(w.gzipWriter)
w.gzipWriter = nil
}
}
// Flush implements http.Flusher
func (w *writerWrapper) Flush() {
w.FinishWriting()
if flusher, ok := w.OriginWriter.(http.Flusher); ok {
flusher.Flush()
}
}