mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Add ETag header (#15370)
* Add ETag header. * Comply with RFC 7232. * Moved logic into httpcache.go * Changed name. * Lint * Implemented If-None-Match list. * Fixed missing header on * * Removed weak etag support. * Removed * support. * Added unit test. * Lint Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		@@ -10,6 +10,7 @@ import (
 | 
				
			|||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
@@ -26,11 +27,13 @@ func GetCacheControl() string {
 | 
				
			|||||||
// generateETag generates an ETag based on size, filename and file modification time
 | 
					// generateETag generates an ETag based on size, filename and file modification time
 | 
				
			||||||
func generateETag(fi os.FileInfo) string {
 | 
					func generateETag(fi os.FileInfo) string {
 | 
				
			||||||
	etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat)
 | 
						etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat)
 | 
				
			||||||
	return base64.StdEncoding.EncodeToString([]byte(etag))
 | 
						return `"` + base64.StdEncoding.EncodeToString([]byte(etag)) + `"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// HandleTimeCache handles time-based caching for a HTTP request
 | 
					// HandleTimeCache handles time-based caching for a HTTP request
 | 
				
			||||||
func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
 | 
					func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
 | 
				
			||||||
 | 
						w.Header().Set("Cache-Control", GetCacheControl())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ifModifiedSince := req.Header.Get("If-Modified-Since")
 | 
						ifModifiedSince := req.Header.Get("If-Modified-Since")
 | 
				
			||||||
	if ifModifiedSince != "" {
 | 
						if ifModifiedSince != "" {
 | 
				
			||||||
		t, err := time.Parse(http.TimeFormat, ifModifiedSince)
 | 
							t, err := time.Parse(http.TimeFormat, ifModifiedSince)
 | 
				
			||||||
@@ -40,20 +43,40 @@ func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	w.Header().Set("Cache-Control", GetCacheControl())
 | 
					 | 
				
			||||||
	w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
 | 
						w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
 | 
				
			||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// HandleEtagCache handles ETag-based caching for a HTTP request
 | 
					// HandleFileETagCache handles ETag-based caching for a HTTP request
 | 
				
			||||||
func HandleEtagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
 | 
					func HandleFileETagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
 | 
				
			||||||
	etag := generateETag(fi)
 | 
						etag := generateETag(fi)
 | 
				
			||||||
	if req.Header.Get("If-None-Match") == etag {
 | 
						return HandleGenericETagCache(req, w, etag)
 | 
				
			||||||
		w.WriteHeader(http.StatusNotModified)
 | 
					}
 | 
				
			||||||
		return true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HandleGenericETagCache handles ETag-based caching for a HTTP request.
 | 
				
			||||||
 | 
					// It returns true if the request was handled.
 | 
				
			||||||
 | 
					func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag string) (handled bool) {
 | 
				
			||||||
 | 
						if len(etag) > 0 {
 | 
				
			||||||
 | 
							w.Header().Set("Etag", etag)
 | 
				
			||||||
 | 
							if checkIfNoneMatchIsValid(req, etag) {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusNotModified)
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	w.Header().Set("Cache-Control", GetCacheControl())
 | 
						w.Header().Set("Cache-Control", GetCacheControl())
 | 
				
			||||||
	w.Header().Set("ETag", etag)
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checkIfNoneMatchIsValid tests if the header If-None-Match matches the ETag
 | 
				
			||||||
 | 
					func checkIfNoneMatchIsValid(req *http.Request, etag string) bool {
 | 
				
			||||||
 | 
						ifNoneMatch := req.Header.Get("If-None-Match")
 | 
				
			||||||
 | 
						if len(ifNoneMatch) > 0 {
 | 
				
			||||||
 | 
							for _, item := range strings.Split(ifNoneMatch, ",") {
 | 
				
			||||||
 | 
								item = strings.TrimSpace(item)
 | 
				
			||||||
 | 
								if item == etag {
 | 
				
			||||||
 | 
									return true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										144
									
								
								modules/httpcache/httpcache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								modules/httpcache/httpcache_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
				
			|||||||
 | 
					// Copyright 2021 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 httpcache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type mockFileInfo struct {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m mockFileInfo) Name() string       { return "gitea.test" }
 | 
				
			||||||
 | 
					func (m mockFileInfo) Size() int64        { return int64(10) }
 | 
				
			||||||
 | 
					func (m mockFileInfo) Mode() os.FileMode  { return os.ModePerm }
 | 
				
			||||||
 | 
					func (m mockFileInfo) ModTime() time.Time { return time.Time{} }
 | 
				
			||||||
 | 
					func (m mockFileInfo) IsDir() bool        { return false }
 | 
				
			||||||
 | 
					func (m mockFileInfo) Sys() interface{}   { return nil }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHandleFileETagCache(t *testing.T) {
 | 
				
			||||||
 | 
						fi := mockFileInfo{}
 | 
				
			||||||
 | 
						etag := `"MTBnaXRlYS50ZXN0TW9uLCAwMSBKYW4gMDAwMSAwMDowMDowMCBHTVQ="`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("No_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleFileETagCache(req, w, fi)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.False(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 2)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Cache-Control")
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Wrong_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req.Header.Set("If-None-Match", `"wrong etag"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleFileETagCache(req, w, fi)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.False(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 2)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Cache-Control")
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Correct_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req.Header.Set("If-None-Match", etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleFileETagCache(req, w, fi)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.True(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 1)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
							assert.Equal(t, http.StatusNotModified, w.Code)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestHandleGenericETagCache(t *testing.T) {
 | 
				
			||||||
 | 
						etag := `"test"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("No_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleGenericETagCache(req, w, etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.False(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 2)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Cache-Control")
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Wrong_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req.Header.Set("If-None-Match", `"wrong etag"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleGenericETagCache(req, w, etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.False(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 2)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Cache-Control")
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Correct_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req.Header.Set("If-None-Match", etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleGenericETagCache(req, w, etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.True(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 1)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
							assert.Equal(t, http.StatusNotModified, w.Code)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Multiple_Wrong_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req.Header.Set("If-None-Match", `"wrong etag", "wrong etag "`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleGenericETagCache(req, w, etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.False(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 2)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Cache-Control")
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Multiple_Correct_If-None-Match", func(t *testing.T) {
 | 
				
			||||||
 | 
							req := &http.Request{Header: make(http.Header)}
 | 
				
			||||||
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req.Header.Set("If-None-Match", `"wrong etag", `+etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handled := HandleGenericETagCache(req, w, etag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.True(t, handled)
 | 
				
			||||||
 | 
							assert.Len(t, w.Header(), 1)
 | 
				
			||||||
 | 
							assert.Contains(t, w.Header(), "Etag")
 | 
				
			||||||
 | 
							assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
				
			||||||
 | 
							assert.Equal(t, http.StatusNotModified, w.Code)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -165,7 +165,7 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, opt *Optio
 | 
				
			|||||||
		log.Println("[Static] Serving " + file)
 | 
							log.Println("[Static] Serving " + file)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if httpcache.HandleEtagCache(req, w, fi) {
 | 
						if httpcache.HandleFileETagCache(req, w, fi) {
 | 
				
			||||||
		return true
 | 
							return true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/httpcache"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/storage"
 | 
						"code.gitea.io/gitea/modules/storage"
 | 
				
			||||||
@@ -124,21 +125,25 @@ func GetAttachment(ctx *context.Context) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := attach.IncreaseDownloadCount(); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("IncreaseDownloadCount", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if setting.Attachment.ServeDirect {
 | 
						if setting.Attachment.ServeDirect {
 | 
				
			||||||
		//If we have a signed url (S3, object storage), redirect to this directly.
 | 
							//If we have a signed url (S3, object storage), redirect to this directly.
 | 
				
			||||||
		u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)
 | 
							u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if u != nil && err == nil {
 | 
							if u != nil && err == nil {
 | 
				
			||||||
			if err := attach.IncreaseDownloadCount(); err != nil {
 | 
					 | 
				
			||||||
				ctx.ServerError("Update", err)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			ctx.Redirect(u.String())
 | 
								ctx.Redirect(u.String())
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//If we have matched and access to release or issue
 | 
						//If we have matched and access to release or issue
 | 
				
			||||||
	fr, err := storage.Attachments.Open(attach.RelativePath())
 | 
						fr, err := storage.Attachments.Open(attach.RelativePath())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -147,11 +152,6 @@ func GetAttachment(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	defer fr.Close()
 | 
						defer fr.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := attach.IncreaseDownloadCount(); err != nil {
 | 
					 | 
				
			||||||
		ctx.ServerError("Update", err)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err = ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
 | 
						if err = ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
 | 
				
			||||||
		ctx.ServerError("ServeData", err)
 | 
							ctx.ServerError("ServeData", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/charset"
 | 
						"code.gitea.io/gitea/modules/charset"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/git"
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/httpcache"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/lfs"
 | 
						"code.gitea.io/gitea/modules/lfs"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -31,6 +32,7 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
 | 
						ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if size >= 0 {
 | 
						if size >= 0 {
 | 
				
			||||||
		ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
 | 
							ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@@ -71,6 +73,10 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// ServeBlob download a git.Blob
 | 
					// ServeBlob download a git.Blob
 | 
				
			||||||
func ServeBlob(ctx *context.Context, blob *git.Blob) error {
 | 
					func ServeBlob(ctx *context.Context, blob *git.Blob) error {
 | 
				
			||||||
 | 
						if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	dataRc, err := blob.DataAsync()
 | 
						dataRc, err := blob.DataAsync()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@@ -86,6 +92,10 @@ func ServeBlob(ctx *context.Context, blob *git.Blob) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
 | 
					// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
 | 
				
			||||||
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
 | 
					func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
 | 
				
			||||||
 | 
						if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	dataRc, err := blob.DataAsync()
 | 
						dataRc, err := blob.DataAsync()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@@ -102,6 +112,9 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
 | 
				
			|||||||
		if meta == nil {
 | 
							if meta == nil {
 | 
				
			||||||
			return ServeBlob(ctx, blob)
 | 
								return ServeBlob(ctx, blob)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
 | 
							lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user