mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-12 16:13:56 +01:00
Make Golang correctly delete temp files during uploading (#36128)
Fix #36127
This commit is contained in:
parent
01351cc6c7
commit
f25409fab8
@ -370,6 +370,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files.
|
||||||
|
// So ideally we should set the TMPDIR environment variable to make them use our managed temp directory.
|
||||||
|
// But there is no clear place to set it currently, for example: when running "install" page, the AppDataPath is not ready yet, then AppDataTempDir won't work
|
||||||
|
|
||||||
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
||||||
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
||||||
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
||||||
|
|||||||
@ -46,11 +46,15 @@ func RouterMockPoint(pointName string) func(next http.Handler) http.Handler {
|
|||||||
//
|
//
|
||||||
// Then the mock function will be executed as a middleware at the mock point.
|
// Then the mock function will be executed as a middleware at the mock point.
|
||||||
// It only takes effect in testing mode (setting.IsInTesting == true).
|
// It only takes effect in testing mode (setting.IsInTesting == true).
|
||||||
func RouteMock(pointName string, h any) {
|
func RouteMock(pointName string, h any) func() {
|
||||||
if _, ok := routeMockPoints[pointName]; !ok {
|
if _, ok := routeMockPoints[pointName]; !ok {
|
||||||
panic("route mock point not found: " + pointName)
|
panic("route mock point not found: " + pointName)
|
||||||
}
|
}
|
||||||
|
old := routeMockPoints[pointName]
|
||||||
routeMockPoints[pointName] = toHandlerProvider(h)
|
routeMockPoints[pointName] = toHandlerProvider(h)
|
||||||
|
return func() {
|
||||||
|
routeMockPoints[pointName] = old
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteMockReset resets all mock points (no mock anymore)
|
// RouteMockReset resets all mock points (no mock anymore)
|
||||||
|
|||||||
@ -55,7 +55,7 @@ func NewRouter() *Router {
|
|||||||
// Use supports two middlewares
|
// Use supports two middlewares
|
||||||
func (r *Router) Use(middlewares ...any) {
|
func (r *Router) Use(middlewares ...any) {
|
||||||
for _, m := range middlewares {
|
for _, m := range middlewares {
|
||||||
if m != nil {
|
if !isNilOrFuncNil(m) {
|
||||||
r.chiRouter.Use(toHandlerProvider(m))
|
r.chiRouter.Use(toHandlerProvider(m))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,8 +72,13 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
|
|||||||
req = req.WithContext(cache.WithCacheContext(ctx))
|
req = req.WithContext(cache.WithCacheContext(ctx))
|
||||||
ds.SetContextValue(httplib.RequestContextKey, req)
|
ds.SetContextValue(httplib.RequestContextKey, req)
|
||||||
ds.AddCleanUp(func() {
|
ds.AddCleanUp(func() {
|
||||||
if req.MultipartForm != nil {
|
// TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded files to temp directory (TMPDIR) when parsing multipart-form.
|
||||||
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
// The "req" might have changed due to the new "req.WithContext" calls
|
||||||
|
// For example: in NewBaseContext, a new "req" with context is created, and the multipart-form is parsed there.
|
||||||
|
// So we always use the latest "req" from the data store.
|
||||||
|
ctxReq := ds.GetContextValue(httplib.RequestContextKey).(*http.Request)
|
||||||
|
if ctxReq.MultipartForm != nil {
|
||||||
|
_ = ctxReq.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
next.ServeHTTP(respWriter, req)
|
next.ServeHTTP(respWriter, req)
|
||||||
|
|||||||
@ -227,6 +227,8 @@ func ctxDataSet(args ...any) func(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RouterMockPointBeforeWebRoutes = "before-web-routes"
|
||||||
|
|
||||||
// Routes returns all web routes
|
// Routes returns all web routes
|
||||||
func Routes() *web.Router {
|
func Routes() *web.Router {
|
||||||
routes := web.NewRouter()
|
routes := web.NewRouter()
|
||||||
@ -285,7 +287,7 @@ func Routes() *web.Router {
|
|||||||
|
|
||||||
webRoutes := web.NewRouter()
|
webRoutes := web.NewRouter()
|
||||||
webRoutes.Use(mid...)
|
webRoutes.Use(mid...)
|
||||||
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS())
|
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
|
||||||
routes.Mount("", webRoutes)
|
routes.Mount("", webRoutes)
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,8 +43,10 @@ type Base struct {
|
|||||||
Locale translation.Locale
|
Locale translation.Locale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ParseMultipartFormMaxMemory = int64(32 << 20)
|
||||||
|
|
||||||
func (b *Base) ParseMultipartForm() bool {
|
func (b *Base) ParseMultipartForm() bool {
|
||||||
err := b.Req.ParseMultipartForm(32 << 20)
|
err := b.Req.ParseMultipartForm(ParseMultipartFormMaxMemory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: all errors caused by client side should be ignored (connection closed).
|
// TODO: all errors caused by client side should be ignored (connection closed).
|
||||||
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
|
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
|||||||
@ -7,17 +7,23 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
"io/fs"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
route_web "code.gitea.io/gitea/routers/web"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testGeneratePngBytes() []byte {
|
func testGeneratePngBytes() []byte {
|
||||||
@ -52,14 +58,38 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL
|
|||||||
return obj["uuid"]
|
return obj["uuid"]
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateAnonymousAttachment(t *testing.T) {
|
func TestAttachments(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
t.Run("CreateAnonymousAttachment", testCreateAnonymousAttachment)
|
||||||
|
t.Run("CreateUser2IssueAttachment", testCreateUser2IssueAttachment)
|
||||||
|
t.Run("UploadAttachmentDeleteTemp", testUploadAttachmentDeleteTemp)
|
||||||
|
t.Run("GetAttachment", testGetAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUploadAttachmentDeleteTemp(t *testing.T) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
countTmpFile := func() int {
|
||||||
|
// TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded file to os.TempDir() when it exceeds the max memory limit.
|
||||||
|
files, err := fs.Glob(os.DirFS(os.TempDir()), "multipart-*") //nolint:usetesting // Golang's "http" package's behavior
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(files)
|
||||||
|
}
|
||||||
|
var tmpFileCountDuringUpload int
|
||||||
|
defer test.MockVariableValue(&context.ParseMultipartFormMaxMemory, 1)()
|
||||||
|
defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
tmpFileCountDuringUpload = countTmpFile()
|
||||||
|
})()
|
||||||
|
_ = testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||||
|
assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
|
||||||
|
assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateAnonymousAttachment(t *testing.T) {
|
||||||
session := emptyTestSession(t)
|
session := emptyTestSession(t)
|
||||||
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
|
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateIssueAttachment(t *testing.T) {
|
func testCreateUser2IssueAttachment(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
const repoURL = "user2/repo1"
|
const repoURL = "user2/repo1"
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
|
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||||
@ -90,8 +120,7 @@ func TestCreateIssueAttachment(t *testing.T) {
|
|||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAttachment(t *testing.T) {
|
func testGetAttachment(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
adminSession := loginUser(t, "user1")
|
adminSession := loginUser(t, "user1")
|
||||||
user2Session := loginUser(t, "user2")
|
user2Session := loginUser(t, "user2")
|
||||||
user8Session := loginUser(t, "user8")
|
user8Session := loginUser(t, "user8")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user