aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2023-05-13 22:04:57 +0800
committerGitHub <noreply@github.com>2023-05-13 16:04:57 +0200
commita94a8d0ab1f6c9d63ae607dfec866c47ae3e4b5c (patch)
tree84c1e5e4f3423ed952b573ab1403844de2951cb7 /modules
parentf74501609246646a3ab62b37a1e7469d6fea63b7 (diff)
downloadgitea-a94a8d0ab1f6c9d63ae607dfec866c47ae3e4b5c.tar.gz
gitea-a94a8d0ab1f6c9d63ae607dfec866c47ae3e4b5c.zip
Use standard HTTP library to serve files (#24693)
`http.ServeFile/ServeContent` handles `If-xxx`, `Content-Length`, `Range` and `Etag` correctly After this PR, storage files (eg: avatar) could be responded with correct Content-Length.
Diffstat (limited to 'modules')
-rw-r--r--modules/context/context_test.go4
-rw-r--r--modules/httpcache/httpcache.go37
-rw-r--r--modules/httpcache/httpcache_test.go57
-rw-r--r--modules/httplib/mock.go35
-rw-r--r--modules/httplib/serve_test.go13
-rw-r--r--modules/public/public.go8
6 files changed, 14 insertions, 140 deletions
diff --git a/modules/context/context_test.go b/modules/context/context_test.go
index a6facc9788..033ce2ef0a 100644
--- a/modules/context/context_test.go
+++ b/modules/context/context_test.go
@@ -5,16 +5,16 @@ package context
import (
"net/http"
+ "net/http/httptest"
"testing"
- "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestRemoveSessionCookieHeader(t *testing.T) {
- w := httplib.NewMockResponseWriter()
+ w := httptest.NewRecorder()
w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go
index 46e0152ef4..001ac06415 100644
--- a/modules/httpcache/httpcache.go
+++ b/modules/httpcache/httpcache.go
@@ -4,10 +4,8 @@
package httpcache
import (
- "encoding/base64"
- "fmt"
+ "io"
"net/http"
- "os"
"strconv"
"strings"
"time"
@@ -37,38 +35,9 @@ func SetCacheControlInHeader(h http.Header, maxAge time.Duration, additionalDire
h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", "))
}
-// generateETag generates an ETag based on size, filename and file modification time
-func generateETag(fi os.FileInfo) string {
- etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat)
- return `"` + base64.StdEncoding.EncodeToString([]byte(etag)) + `"`
-}
-
-// HandleTimeCache handles time-based caching for a HTTP request
-func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
- return HandleGenericTimeCache(req, w, fi.ModTime())
-}
-
-// HandleGenericTimeCache handles time-based caching for a HTTP request
-func HandleGenericTimeCache(req *http.Request, w http.ResponseWriter, lastModified time.Time) (handled bool) {
+func ServeContentWithCacheControl(w http.ResponseWriter, req *http.Request, name string, modTime time.Time, content io.ReadSeeker) {
SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
-
- ifModifiedSince := req.Header.Get("If-Modified-Since")
- if ifModifiedSince != "" {
- t, err := time.Parse(http.TimeFormat, ifModifiedSince)
- if err == nil && lastModified.Unix() <= t.Unix() {
- w.WriteHeader(http.StatusNotModified)
- return true
- }
- }
-
- w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
- return false
-}
-
-// HandleFileETagCache handles ETag-based caching for a HTTP request
-func HandleFileETagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
- etag := generateETag(fi)
- return HandleGenericETagCache(req, w, etag)
+ http.ServeContent(w, req, name, modTime, content)
}
// HandleGenericETagCache handles ETag-based caching for a HTTP request.
diff --git a/modules/httpcache/httpcache_test.go b/modules/httpcache/httpcache_test.go
index 624d2f4d4b..d81f06097c 100644
--- a/modules/httpcache/httpcache_test.go
+++ b/modules/httpcache/httpcache_test.go
@@ -6,23 +6,12 @@ package httpcache
import (
"net/http"
"net/http/httptest"
- "os"
"strings"
"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 countFormalHeaders(h http.Header) (c int) {
for k := range h {
// ignore our headers for internal usage
@@ -34,52 +23,6 @@ func countFormalHeaders(h http.Header) (c int) {
return c
}
-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.Equal(t, 2, countFormalHeaders(w.Header()))
- 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.Equal(t, 2, countFormalHeaders(w.Header()))
- 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.Equal(t, 1, countFormalHeaders(w.Header()))
- 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"`
diff --git a/modules/httplib/mock.go b/modules/httplib/mock.go
deleted file mode 100644
index 7d284e86fb..0000000000
--- a/modules/httplib/mock.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package httplib
-
-import (
- "bytes"
- "net/http"
-)
-
-type MockResponseWriter struct {
- header http.Header
-
- StatusCode int
- BodyBuffer bytes.Buffer
-}
-
-func (m *MockResponseWriter) Header() http.Header {
- return m.header
-}
-
-func (m *MockResponseWriter) Write(bytes []byte) (int, error) {
- if m.StatusCode == 0 {
- m.StatusCode = http.StatusOK
- }
- return m.BodyBuffer.Write(bytes)
-}
-
-func (m *MockResponseWriter) WriteHeader(statusCode int) {
- m.StatusCode = statusCode
-}
-
-func NewMockResponseWriter() *MockResponseWriter {
- return &MockResponseWriter{header: http.Header{}}
-}
diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go
index 0768f1c713..fed4611d21 100644
--- a/modules/httplib/serve_test.go
+++ b/modules/httplib/serve_test.go
@@ -6,6 +6,7 @@ package httplib
import (
"fmt"
"net/http"
+ "net/http/httptest"
"net/url"
"os"
"strings"
@@ -25,12 +26,12 @@ func TestServeContentByReader(t *testing.T) {
r.Header.Set("Range", fmt.Sprintf("bytes=%s", rangeStr))
}
reader := strings.NewReader(data)
- w := NewMockResponseWriter()
+ w := httptest.NewRecorder()
ServeContentByReader(r, w, "test", int64(len(data)), reader)
- assert.Equal(t, expectedStatusCode, w.StatusCode)
+ assert.Equal(t, expectedStatusCode, w.Code)
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))
- assert.Equal(t, expectedContent, w.BodyBuffer.String())
+ assert.Equal(t, expectedContent, w.Body.String())
}
}
@@ -76,12 +77,12 @@ func TestServeContentByReadSeeker(t *testing.T) {
}
defer seekReader.Close()
- w := NewMockResponseWriter()
+ w := httptest.NewRecorder()
ServeContentByReadSeeker(r, w, "test", time.Time{}, seekReader)
- assert.Equal(t, expectedStatusCode, w.StatusCode)
+ assert.Equal(t, expectedStatusCode, w.Code)
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))
- assert.Equal(t, expectedContent, w.BodyBuffer.String())
+ assert.Equal(t, expectedContent, w.Body.String())
}
}
diff --git a/modules/public/public.go b/modules/public/public.go
index 0c0e6dc1cc..ed38d85cfa 100644
--- a/modules/public/public.go
+++ b/modules/public/public.go
@@ -97,10 +97,6 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem,
return true
}
- if httpcache.HandleFileETagCache(req, w, fi) {
- return true
- }
-
serveContent(w, req, fi, fi.ModTime(), f)
return true
}
@@ -124,11 +120,11 @@ func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modt
w.Header().Set("Content-Type", "application/octet-stream")
}
w.Header().Set("Content-Encoding", "gzip")
- http.ServeContent(w, req, fi.Name(), modtime, rdGzip)
+ httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, rdGzip)
return
}
}
- http.ServeContent(w, req, fi.Name(), modtime, content)
+ httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, content)
return
}