Pooled and buffered gzip implementation (#5722)
* Pooled and buffered gzip implementation * Add test for gzip * Add integration test * Ensure lfs check within transaction The previous code made it possible for a race condition to occur whereby a LFSMetaObject could be checked into the database twice. We should check if the LFSMetaObject is within the database and insert it if not in one transaction. * Try to avoid primary key problem in postgres The integration tests are being affected by https://github.com/go-testfixtures/testfixtures/issues/39 if we set the primary key high enough, keep a count of this and remove at the end of each test we shouldn't be affected by this.
This commit is contained in:
		
							parent
							
								
									075649572d
								
							
						
					
					
						commit
						7d434376f1
					
				
					 6 changed files with 598 additions and 10 deletions
				
			
		
							
								
								
									
										129
									
								
								integrations/lfs_getobject_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								integrations/lfs_getobject_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,129 @@ | |||
| // Copyright 2019 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package integrations | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/zip" | ||||
| 	"bytes" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/hex" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/gzip" | ||||
| 	"code.gitea.io/gitea/modules/lfs" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 
 | ||||
| 	gzipp "github.com/klauspost/compress/gzip" | ||||
| ) | ||||
| 
 | ||||
| func GenerateLFSOid(content io.Reader) (string, error) { | ||||
| 	h := sha256.New() | ||||
| 	if _, err := io.Copy(h, content); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	sum := h.Sum(nil) | ||||
| 	return hex.EncodeToString(sum), nil | ||||
| } | ||||
| 
 | ||||
| var lfsID = int64(20000) | ||||
| 
 | ||||
| func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string { | ||||
| 	oid, err := GenerateLFSOid(bytes.NewReader(*content)) | ||||
| 	assert.NoError(t, err) | ||||
| 	var lfsMetaObject *models.LFSMetaObject | ||||
| 
 | ||||
| 	if setting.UsePostgreSQL { | ||||
| 		lfsMetaObject = &models.LFSMetaObject{ID: lfsID, Oid: oid, Size: int64(len(*content)), RepositoryID: repositoryID} | ||||
| 	} else { | ||||
| 		lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(*content)), RepositoryID: repositoryID} | ||||
| 	} | ||||
| 
 | ||||
| 	lfsID = lfsID + 1 | ||||
| 	lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject) | ||||
| 	assert.NoError(t, err) | ||||
| 	contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} | ||||
| 	if !contentStore.Exists(lfsMetaObject) { | ||||
| 		err := contentStore.Put(lfsMetaObject, bytes.NewReader(*content)) | ||||
| 		assert.NoError(t, err) | ||||
| 	} | ||||
| 	return oid | ||||
| } | ||||
| 
 | ||||
| func doLfs(t *testing.T, content *[]byte, expectGzip bool) { | ||||
| 	prepareTestEnv(t) | ||||
| 	repo, err := models.GetRepositoryByOwnerAndName("user2", "repo1") | ||||
| 	assert.NoError(t, err) | ||||
| 	oid := storeObjectInRepo(t, repo.ID, content) | ||||
| 	defer repo.RemoveLFSMetaObjectByOid(oid) | ||||
| 
 | ||||
| 	session := loginUser(t, "user2") | ||||
| 
 | ||||
| 	// Request OID
 | ||||
| 	req := NewRequest(t, "GET", "/user2/repo1.git/info/lfs/objects/"+oid+"/test") | ||||
| 	req.Header.Set("Accept-Encoding", "gzip") | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 	contentEncoding := resp.Header().Get("Content-Encoding") | ||||
| 	if !expectGzip || !setting.EnableGzip { | ||||
| 		assert.NotContains(t, contentEncoding, "gzip") | ||||
| 
 | ||||
| 		result := resp.Body.Bytes() | ||||
| 		assert.Equal(t, *content, result) | ||||
| 	} else { | ||||
| 		assert.Contains(t, contentEncoding, "gzip") | ||||
| 		gzippReader, err := gzipp.NewReader(resp.Body) | ||||
| 		assert.NoError(t, err) | ||||
| 		result, err := ioutil.ReadAll(gzippReader) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, *content, result) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func TestGetLFSSmall(t *testing.T) { | ||||
| 	content := []byte("A very small file\n") | ||||
| 	doLfs(t, &content, false) | ||||
| } | ||||
| 
 | ||||
| func TestGetLFSLarge(t *testing.T) { | ||||
| 	content := make([]byte, gzip.MinSize*10) | ||||
| 	for i := range content { | ||||
| 		content[i] = byte(i % 256) | ||||
| 	} | ||||
| 	doLfs(t, &content, true) | ||||
| } | ||||
| 
 | ||||
| func TestGetLFSGzip(t *testing.T) { | ||||
| 	b := make([]byte, gzip.MinSize*10) | ||||
| 	for i := range b { | ||||
| 		b[i] = byte(i % 256) | ||||
| 	} | ||||
| 	outputBuffer := bytes.NewBuffer([]byte{}) | ||||
| 	gzippWriter := gzipp.NewWriter(outputBuffer) | ||||
| 	gzippWriter.Write(b) | ||||
| 	gzippWriter.Close() | ||||
| 	content := outputBuffer.Bytes() | ||||
| 	doLfs(t, &content, false) | ||||
| } | ||||
| 
 | ||||
| func TestGetLFSZip(t *testing.T) { | ||||
| 	b := make([]byte, gzip.MinSize*10) | ||||
| 	for i := range b { | ||||
| 		b[i] = byte(i % 256) | ||||
| 	} | ||||
| 	outputBuffer := bytes.NewBuffer([]byte{}) | ||||
| 	zipWriter := zip.NewWriter(outputBuffer) | ||||
| 	fileWriter, err := zipWriter.Create("default") | ||||
| 	assert.NoError(t, err) | ||||
| 	fileWriter.Write(b) | ||||
| 	zipWriter.Close() | ||||
| 	content := outputBuffer.Bytes() | ||||
| 	doLfs(t, &content, false) | ||||
| } | ||||
|  | @ -30,6 +30,7 @@ LFS_CONTENT_PATH = data/lfs-sqlite | |||
| OFFLINE_MODE     = false | ||||
| LFS_JWT_SECRET   = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w | ||||
| APP_DATA_PATH    = integrations/gitea-integration-sqlite/data | ||||
| ENABLE_GZIP      = true | ||||
| 
 | ||||
| [mailer] | ||||
| ENABLED = false | ||||
|  |  | |||
|  | @ -44,20 +44,20 @@ const ( | |||
| func NewLFSMetaObject(m *LFSMetaObject) (*LFSMetaObject, error) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	has, err := x.Get(m) | ||||
| 	sess := x.NewSession() | ||||
| 	defer sess.Close() | ||||
| 	if err = sess.Begin(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	has, err := sess.Get(m) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if has { | ||||
| 		m.Existing = true | ||||
| 		return m, nil | ||||
| 	} | ||||
| 
 | ||||
| 	sess := x.NewSession() | ||||
| 	defer sess.Close() | ||||
| 	if err = sess.Begin(); err != nil { | ||||
| 		return nil, err | ||||
| 		return m, sess.Commit() | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err = sess.Insert(m); err != nil { | ||||
|  |  | |||
							
								
								
									
										327
									
								
								modules/gzip/gzip.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								modules/gzip/gzip.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,327 @@ | |||
| // Copyright 2019 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package gzip | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/klauspost/compress/gzip" | ||||
| 	"gopkg.in/macaron.v1" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	acceptEncodingHeader  = "Accept-Encoding" | ||||
| 	contentEncodingHeader = "Content-Encoding" | ||||
| 	contentLengthHeader   = "Content-Length" | ||||
| 	contentTypeHeader     = "Content-Type" | ||||
| 	rangeHeader           = "Range" | ||||
| 	varyHeader            = "Vary" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// MinSize is the minimum size of content we will compress
 | ||||
| 	MinSize = 1400 | ||||
| ) | ||||
| 
 | ||||
| // noopClosers are io.Writers with a shim to prevent early closure
 | ||||
| type noopCloser struct { | ||||
| 	io.Writer | ||||
| } | ||||
| 
 | ||||
| func (noopCloser) Close() error { return nil } | ||||
| 
 | ||||
| // WriterPool is a gzip writer pool to reduce workload on creation of
 | ||||
| // gzip writers
 | ||||
| type WriterPool struct { | ||||
| 	pool             sync.Pool | ||||
| 	compressionLevel int | ||||
| } | ||||
| 
 | ||||
| // NewWriterPool creates a new pool
 | ||||
| func NewWriterPool(compressionLevel int) *WriterPool { | ||||
| 	return &WriterPool{pool: sync.Pool{ | ||||
| 		// New will return nil, we'll manage the creation of new
 | ||||
| 		// writers in the middleware
 | ||||
| 		New: func() interface{} { return nil }, | ||||
| 	}, | ||||
| 		compressionLevel: compressionLevel} | ||||
| } | ||||
| 
 | ||||
| // Get a writer from the pool - or create one if not available
 | ||||
| func (wp *WriterPool) Get(rw macaron.ResponseWriter) *gzip.Writer { | ||||
| 	ret := wp.pool.Get() | ||||
| 	if ret == nil { | ||||
| 		ret, _ = gzip.NewWriterLevel(rw, wp.compressionLevel) | ||||
| 	} else { | ||||
| 		ret.(*gzip.Writer).Reset(rw) | ||||
| 	} | ||||
| 	return ret.(*gzip.Writer) | ||||
| } | ||||
| 
 | ||||
| // Put returns a writer to the pool
 | ||||
| func (wp *WriterPool) Put(w *gzip.Writer) { | ||||
| 	wp.pool.Put(w) | ||||
| } | ||||
| 
 | ||||
| var writerPool WriterPool | ||||
| var regex regexp.Regexp | ||||
| 
 | ||||
| // Options represents the configuration for the gzip middleware
 | ||||
| type Options struct { | ||||
| 	CompressionLevel int | ||||
| } | ||||
| 
 | ||||
| func validateCompressionLevel(level int) bool { | ||||
| 	return level == gzip.DefaultCompression || | ||||
| 		level == gzip.ConstantCompression || | ||||
| 		(level >= gzip.BestSpeed && level <= gzip.BestCompression) | ||||
| } | ||||
| 
 | ||||
| func validate(options []Options) Options { | ||||
| 	// Default to level 4 compression (Best results seem to be between 4 and 6)
 | ||||
| 	opt := Options{CompressionLevel: 4} | ||||
| 	if len(options) > 0 { | ||||
| 		opt = options[0] | ||||
| 	} | ||||
| 	if !validateCompressionLevel(opt.CompressionLevel) { | ||||
| 		opt.CompressionLevel = 4 | ||||
| 	} | ||||
| 	return opt | ||||
| } | ||||
| 
 | ||||
| // Middleware creates a macaron.Handler to proxy the response
 | ||||
| func Middleware(options ...Options) macaron.Handler { | ||||
| 	opt := validate(options) | ||||
| 	writerPool = *NewWriterPool(opt.CompressionLevel) | ||||
| 	regex := regexp.MustCompile(`bytes=(\d+)\-.*`) | ||||
| 
 | ||||
| 	return func(ctx *macaron.Context) { | ||||
| 		// If the client won't accept gzip or x-gzip don't compress
 | ||||
| 		if !strings.Contains(ctx.Req.Header.Get(acceptEncodingHeader), "gzip") && | ||||
| 			!strings.Contains(ctx.Req.Header.Get(acceptEncodingHeader), "x-gzip") { | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// If the client is asking for a specific range of bytes - don't compress
 | ||||
| 		if rangeHdr := ctx.Req.Header.Get(rangeHeader); rangeHdr != "" { | ||||
| 
 | ||||
| 			match := regex.FindStringSubmatch(rangeHdr) | ||||
| 			if match != nil && len(match) > 1 { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// OK we should proxy the response writer
 | ||||
| 		// We are still not necessarily going to compress...
 | ||||
| 		proxyWriter := &ProxyResponseWriter{ | ||||
| 			ResponseWriter: ctx.Resp, | ||||
| 		} | ||||
| 		defer proxyWriter.Close() | ||||
| 
 | ||||
| 		ctx.Resp = proxyWriter | ||||
| 		ctx.MapTo(proxyWriter, (*http.ResponseWriter)(nil)) | ||||
| 
 | ||||
| 		// Check if render middleware has been registered,
 | ||||
| 		// if yes, we need to modify ResponseWriter for it as well.
 | ||||
| 		if _, ok := ctx.Render.(*macaron.DummyRender); !ok { | ||||
| 			ctx.Render.SetResponseWriter(proxyWriter) | ||||
| 		} | ||||
| 
 | ||||
| 		ctx.Next() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ProxyResponseWriter is a wrapped macaron ResponseWriter that may compress its contents
 | ||||
| type ProxyResponseWriter struct { | ||||
| 	writer io.WriteCloser | ||||
| 	macaron.ResponseWriter | ||||
| 	stopped bool | ||||
| 
 | ||||
| 	code int | ||||
| 	buf  []byte | ||||
| } | ||||
| 
 | ||||
| // Write appends data to the proxied gzip writer.
 | ||||
| func (proxy *ProxyResponseWriter) Write(b []byte) (int, error) { | ||||
| 	// if writer is initialized, use the writer
 | ||||
| 	if proxy.writer != nil { | ||||
| 		return proxy.writer.Write(b) | ||||
| 	} | ||||
| 
 | ||||
| 	proxy.buf = append(proxy.buf, b...) | ||||
| 
 | ||||
| 	var ( | ||||
| 		contentLength, _ = strconv.Atoi(proxy.Header().Get(contentLengthHeader)) | ||||
| 		contentType      = proxy.Header().Get(contentTypeHeader) | ||||
| 		contentEncoding  = proxy.Header().Get(contentEncodingHeader) | ||||
| 	) | ||||
| 
 | ||||
| 	// OK if an encoding hasn't been chosen, and content length > 1400
 | ||||
| 	// and content type isn't a compressed type
 | ||||
| 	if contentEncoding == "" && | ||||
| 		(contentLength == 0 || contentLength >= MinSize) && | ||||
| 		(contentType == "" || !compressedContentType(contentType)) { | ||||
| 		// If current buffer is less than the min size and a Content-Length isn't set, then wait
 | ||||
| 		if len(proxy.buf) < MinSize && contentLength == 0 { | ||||
| 			return len(b), nil | ||||
| 		} | ||||
| 
 | ||||
| 		// If the Content-Length is larger than minSize or the current buffer is larger than minSize, then continue.
 | ||||
| 		if contentLength >= MinSize || len(proxy.buf) >= MinSize { | ||||
| 			// if we don't know the content type, infer it
 | ||||
| 			if contentType == "" { | ||||
| 				contentType = http.DetectContentType(proxy.buf) | ||||
| 				proxy.Header().Set(contentTypeHeader, contentType) | ||||
| 			} | ||||
| 			// If the Content-Type is not compressed - Compress!
 | ||||
| 			if !compressedContentType(contentType) { | ||||
| 				if err := proxy.startGzip(); err != nil { | ||||
| 					return 0, err | ||||
| 				} | ||||
| 				return len(b), nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// If we got here, we should not GZIP this response.
 | ||||
| 	if err := proxy.startPlain(); err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return len(b), nil | ||||
| } | ||||
| 
 | ||||
| func (proxy *ProxyResponseWriter) startGzip() error { | ||||
| 	// Set the content-encoding and vary headers.
 | ||||
| 	proxy.Header().Set(contentEncodingHeader, "gzip") | ||||
| 	proxy.Header().Set(varyHeader, acceptEncodingHeader) | ||||
| 
 | ||||
| 	// if the Content-Length is already set, then calls to Write on gzip
 | ||||
| 	// will fail to set the Content-Length header since its already set
 | ||||
| 	// See: https://github.com/golang/go/issues/14975.
 | ||||
| 	proxy.Header().Del(contentLengthHeader) | ||||
| 
 | ||||
| 	// Write the header to gzip response.
 | ||||
| 	if proxy.code != 0 { | ||||
| 		proxy.ResponseWriter.WriteHeader(proxy.code) | ||||
| 		// Ensure that no other WriteHeader's happen
 | ||||
| 		proxy.code = 0 | ||||
| 	} | ||||
| 
 | ||||
| 	// Initialize and flush the buffer into the gzip response if there are any bytes.
 | ||||
| 	// If there aren't any, we shouldn't initialize it yet because on Close it will
 | ||||
| 	// write the gzip header even if nothing was ever written.
 | ||||
| 	if len(proxy.buf) > 0 { | ||||
| 		// Initialize the GZIP response.
 | ||||
| 		proxy.writer = writerPool.Get(proxy.ResponseWriter) | ||||
| 
 | ||||
| 		return proxy.writeBuf() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (proxy *ProxyResponseWriter) startPlain() error { | ||||
| 	if proxy.code != 0 { | ||||
| 		proxy.ResponseWriter.WriteHeader(proxy.code) | ||||
| 		proxy.code = 0 | ||||
| 	} | ||||
| 	proxy.stopped = true | ||||
| 	proxy.writer = noopCloser{proxy.ResponseWriter} | ||||
| 	return proxy.writeBuf() | ||||
| } | ||||
| 
 | ||||
| func (proxy *ProxyResponseWriter) writeBuf() error { | ||||
| 	if proxy.buf == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	n, err := proxy.writer.Write(proxy.buf) | ||||
| 
 | ||||
| 	// This should never happen (per io.Writer docs), but if the write didn't
 | ||||
| 	// accept the entire buffer but returned no specific error, we have no clue
 | ||||
| 	// what's going on, so abort just to be safe.
 | ||||
| 	if err == nil && n < len(proxy.buf) { | ||||
| 		err = io.ErrShortWrite | ||||
| 	} | ||||
| 	proxy.buf = nil | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // WriteHeader will ensure that we have setup the writer before we write the header
 | ||||
| func (proxy *ProxyResponseWriter) WriteHeader(code int) { | ||||
| 	if proxy.code == 0 { | ||||
| 		proxy.code = code | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Close the writer
 | ||||
| func (proxy *ProxyResponseWriter) Close() error { | ||||
| 	if proxy.stopped { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if proxy.writer == nil { | ||||
| 		err := proxy.startPlain() | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			err = fmt.Errorf("GzipMiddleware: write to regular responseWriter at close gets error: %q", err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	err := proxy.writer.Close() | ||||
| 
 | ||||
| 	if poolWriter, ok := proxy.writer.(*gzip.Writer); ok { | ||||
| 		writerPool.Put(poolWriter) | ||||
| 	} | ||||
| 
 | ||||
| 	proxy.writer = nil | ||||
| 	proxy.stopped = true | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // Flush the writer
 | ||||
| func (proxy *ProxyResponseWriter) Flush() { | ||||
| 	if proxy.writer == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if gw, ok := proxy.writer.(*gzip.Writer); ok { | ||||
| 		gw.Flush() | ||||
| 	} | ||||
| 
 | ||||
| 	proxy.ResponseWriter.Flush() | ||||
| } | ||||
| 
 | ||||
| // Hijack implements http.Hijacker. If the underlying ResponseWriter is a
 | ||||
| // Hijacker, its Hijack method is returned. Otherwise an error is returned.
 | ||||
| func (proxy *ProxyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||||
| 	hijacker, ok := proxy.ResponseWriter.(http.Hijacker) | ||||
| 	if !ok { | ||||
| 		return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") | ||||
| 	} | ||||
| 	return hijacker.Hijack() | ||||
| } | ||||
| 
 | ||||
| // verify Hijacker interface implementation
 | ||||
| var _ http.Hijacker = &ProxyResponseWriter{} | ||||
| 
 | ||||
| func compressedContentType(contentType string) bool { | ||||
| 	switch contentType { | ||||
| 	case "application/zip": | ||||
| 		return true | ||||
| 	case "application/x-gzip": | ||||
| 		return true | ||||
| 	case "application/gzip": | ||||
| 		return true | ||||
| 	default: | ||||
| 		return false | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										131
									
								
								modules/gzip/gzip_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								modules/gzip/gzip_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,131 @@ | |||
| // Copyright 2019 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package gzip | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/zip" | ||||
| 	"bytes" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	gzipp "github.com/klauspost/compress/gzip" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	macaron "gopkg.in/macaron.v1" | ||||
| ) | ||||
| 
 | ||||
| func setup(sampleResponse []byte) (*macaron.Macaron, *[]byte) { | ||||
| 	m := macaron.New() | ||||
| 	m.Use(Middleware()) | ||||
| 	m.Get("/", func() *[]byte { return &sampleResponse }) | ||||
| 	return m, &sampleResponse | ||||
| } | ||||
| 
 | ||||
| func reqNoAcceptGzip(t *testing.T, m *macaron.Macaron, sampleResponse *[]byte) { | ||||
| 	// Request without accept gzip: Should not gzip
 | ||||
| 	resp := httptest.NewRecorder() | ||||
| 	req, err := http.NewRequest("GET", "/", nil) | ||||
| 	assert.NoError(t, err) | ||||
| 	m.ServeHTTP(resp, req) | ||||
| 
 | ||||
| 	_, ok := resp.HeaderMap[contentEncodingHeader] | ||||
| 	assert.False(t, ok) | ||||
| 
 | ||||
| 	contentEncoding := resp.Header().Get(contentEncodingHeader) | ||||
| 	assert.NotContains(t, contentEncoding, "gzip") | ||||
| 
 | ||||
| 	result := resp.Body.Bytes() | ||||
| 	assert.Equal(t, *sampleResponse, result) | ||||
| } | ||||
| 
 | ||||
| func reqAcceptGzip(t *testing.T, m *macaron.Macaron, sampleResponse *[]byte, expectGzip bool) { | ||||
| 	// Request without accept gzip: Should not gzip
 | ||||
| 	resp := httptest.NewRecorder() | ||||
| 	req, err := http.NewRequest("GET", "/", nil) | ||||
| 	assert.NoError(t, err) | ||||
| 	req.Header.Set(acceptEncodingHeader, "gzip") | ||||
| 	m.ServeHTTP(resp, req) | ||||
| 
 | ||||
| 	_, ok := resp.HeaderMap[contentEncodingHeader] | ||||
| 	assert.Equal(t, ok, expectGzip) | ||||
| 
 | ||||
| 	contentEncoding := resp.Header().Get(contentEncodingHeader) | ||||
| 	if expectGzip { | ||||
| 		assert.Contains(t, contentEncoding, "gzip") | ||||
| 		gzippReader, err := gzipp.NewReader(resp.Body) | ||||
| 		assert.NoError(t, err) | ||||
| 		result, err := ioutil.ReadAll(gzippReader) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, *sampleResponse, result) | ||||
| 	} else { | ||||
| 		assert.NotContains(t, contentEncoding, "gzip") | ||||
| 		result := resp.Body.Bytes() | ||||
| 		assert.Equal(t, *sampleResponse, result) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMiddlewareSmall(t *testing.T) { | ||||
| 	m, sampleResponse := setup([]byte("Small response")) | ||||
| 
 | ||||
| 	reqNoAcceptGzip(t, m, sampleResponse) | ||||
| 
 | ||||
| 	reqAcceptGzip(t, m, sampleResponse, false) | ||||
| } | ||||
| 
 | ||||
| func TestMiddlewareLarge(t *testing.T) { | ||||
| 	b := make([]byte, MinSize+1) | ||||
| 	for i := range b { | ||||
| 		b[i] = byte(i % 256) | ||||
| 	} | ||||
| 	m, sampleResponse := setup(b) | ||||
| 
 | ||||
| 	reqNoAcceptGzip(t, m, sampleResponse) | ||||
| 
 | ||||
| 	// This should be gzipped as we accept gzip
 | ||||
| 	reqAcceptGzip(t, m, sampleResponse, true) | ||||
| } | ||||
| 
 | ||||
| func TestMiddlewareGzip(t *testing.T) { | ||||
| 	b := make([]byte, MinSize*10) | ||||
| 	for i := range b { | ||||
| 		b[i] = byte(i % 256) | ||||
| 	} | ||||
| 	outputBuffer := bytes.NewBuffer([]byte{}) | ||||
| 	gzippWriter := gzipp.NewWriter(outputBuffer) | ||||
| 	gzippWriter.Write(b) | ||||
| 	gzippWriter.Flush() | ||||
| 	gzippWriter.Close() | ||||
| 	output := outputBuffer.Bytes() | ||||
| 
 | ||||
| 	m, sampleResponse := setup(output) | ||||
| 
 | ||||
| 	reqNoAcceptGzip(t, m, sampleResponse) | ||||
| 
 | ||||
| 	// This should not be gzipped even though we accept gzip
 | ||||
| 	reqAcceptGzip(t, m, sampleResponse, false) | ||||
| } | ||||
| 
 | ||||
| func TestMiddlewareZip(t *testing.T) { | ||||
| 	b := make([]byte, MinSize*10) | ||||
| 	for i := range b { | ||||
| 		b[i] = byte(i % 256) | ||||
| 	} | ||||
| 	outputBuffer := bytes.NewBuffer([]byte{}) | ||||
| 	zipWriter := zip.NewWriter(outputBuffer) | ||||
| 	fileWriter, err := zipWriter.Create("default") | ||||
| 	assert.NoError(t, err) | ||||
| 	fileWriter.Write(b) | ||||
| 	//fileWriter.Close()
 | ||||
| 	zipWriter.Close() | ||||
| 	output := outputBuffer.Bytes() | ||||
| 
 | ||||
| 	m, sampleResponse := setup(output) | ||||
| 
 | ||||
| 	reqNoAcceptGzip(t, m, sampleResponse) | ||||
| 
 | ||||
| 	// This should not be gzipped even though we accept gzip
 | ||||
| 	reqAcceptGzip(t, m, sampleResponse, false) | ||||
| } | ||||
|  | @ -14,6 +14,7 @@ import ( | |||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/auth" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/gzip" | ||||
| 	"code.gitea.io/gitea/modules/lfs" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/metrics" | ||||
|  | @ -36,7 +37,6 @@ import ( | |||
| 	"github.com/go-macaron/cache" | ||||
| 	"github.com/go-macaron/captcha" | ||||
| 	"github.com/go-macaron/csrf" | ||||
| 	"github.com/go-macaron/gzip" | ||||
| 	"github.com/go-macaron/i18n" | ||||
| 	"github.com/go-macaron/session" | ||||
| 	"github.com/go-macaron/toolbox" | ||||
|  | @ -54,7 +54,7 @@ func NewMacaron() *macaron.Macaron { | |||
| 	} | ||||
| 	m.Use(macaron.Recovery()) | ||||
| 	if setting.EnableGzip { | ||||
| 		m.Use(gzip.Gziper()) | ||||
| 		m.Use(gzip.Middleware()) | ||||
| 	} | ||||
| 	if setting.Protocol == setting.FCGI { | ||||
| 		m.SetURLPrefix(setting.AppSubURL) | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue