Skip to content

Commit f25409f

Browse files
authored
Make Golang correctly delete temp files during uploading (#36128)
Fix #36127
1 parent 01351cc commit f25409f

File tree

7 files changed

+57
-11
lines changed

7 files changed

+57
-11
lines changed

modules/setting/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
370370
}
371371
}
372372

373+
// TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files.
374+
// So ideally we should set the TMPDIR environment variable to make them use our managed temp directory.
375+
// 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
376+
373377
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
374378
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
375379
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))

modules/web/routemock.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ func RouterMockPoint(pointName string) func(next http.Handler) http.Handler {
4646
//
4747
// Then the mock function will be executed as a middleware at the mock point.
4848
// It only takes effect in testing mode (setting.IsInTesting == true).
49-
func RouteMock(pointName string, h any) {
49+
func RouteMock(pointName string, h any) func() {
5050
if _, ok := routeMockPoints[pointName]; !ok {
5151
panic("route mock point not found: " + pointName)
5252
}
53+
old := routeMockPoints[pointName]
5354
routeMockPoints[pointName] = toHandlerProvider(h)
55+
return func() {
56+
routeMockPoints[pointName] = old
57+
}
5458
}
5559

5660
// RouteMockReset resets all mock points (no mock anymore)

modules/web/router.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func NewRouter() *Router {
5555
// Use supports two middlewares
5656
func (r *Router) Use(middlewares ...any) {
5757
for _, m := range middlewares {
58-
if m != nil {
58+
if !isNilOrFuncNil(m) {
5959
r.chiRouter.Use(toHandlerProvider(m))
6060
}
6161
}

routers/common/middleware.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,13 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
7272
req = req.WithContext(cache.WithCacheContext(ctx))
7373
ds.SetContextValue(httplib.RequestContextKey, req)
7474
ds.AddCleanUp(func() {
75-
if req.MultipartForm != nil {
76-
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
75+
// TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded files to temp directory (TMPDIR) when parsing multipart-form.
76+
// The "req" might have changed due to the new "req.WithContext" calls
77+
// For example: in NewBaseContext, a new "req" with context is created, and the multipart-form is parsed there.
78+
// So we always use the latest "req" from the data store.
79+
ctxReq := ds.GetContextValue(httplib.RequestContextKey).(*http.Request)
80+
if ctxReq.MultipartForm != nil {
81+
_ = ctxReq.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
7782
}
7883
})
7984
next.ServeHTTP(respWriter, req)

routers/web/web.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ func ctxDataSet(args ...any) func(ctx *context.Context) {
227227
}
228228
}
229229

230+
const RouterMockPointBeforeWebRoutes = "before-web-routes"
231+
230232
// Routes returns all web routes
231233
func Routes() *web.Router {
232234
routes := web.NewRouter()
@@ -285,7 +287,7 @@ func Routes() *web.Router {
285287

286288
webRoutes := web.NewRouter()
287289
webRoutes.Use(mid...)
288-
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS())
290+
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
289291
routes.Mount("", webRoutes)
290292
return routes
291293
}

services/context/base.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ type Base struct {
4343
Locale translation.Locale
4444
}
4545

46+
var ParseMultipartFormMaxMemory = int64(32 << 20)
47+
4648
func (b *Base) ParseMultipartForm() bool {
47-
err := b.Req.ParseMultipartForm(32 << 20)
49+
err := b.Req.ParseMultipartForm(ParseMultipartFormMaxMemory)
4850
if err != nil {
4951
// TODO: all errors caused by client side should be ignored (connection closed).
5052
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {

tests/integration/attachment_test.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@ import (
77
"bytes"
88
"image"
99
"image/png"
10+
"io/fs"
1011
"mime/multipart"
1112
"net/http"
13+
"os"
1214
"strings"
1315
"testing"
1416

1517
repo_model "code.gitea.io/gitea/models/repo"
1618
"code.gitea.io/gitea/modules/storage"
1719
"code.gitea.io/gitea/modules/test"
20+
"code.gitea.io/gitea/modules/web"
21+
route_web "code.gitea.io/gitea/routers/web"
22+
"code.gitea.io/gitea/services/context"
1823
"code.gitea.io/gitea/tests"
1924

2025
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
2127
)
2228

2329
func testGeneratePngBytes() []byte {
@@ -52,14 +58,38 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL
5258
return obj["uuid"]
5359
}
5460

55-
func TestCreateAnonymousAttachment(t *testing.T) {
61+
func TestAttachments(t *testing.T) {
5662
defer tests.PrepareTestEnv(t)()
63+
t.Run("CreateAnonymousAttachment", testCreateAnonymousAttachment)
64+
t.Run("CreateUser2IssueAttachment", testCreateUser2IssueAttachment)
65+
t.Run("UploadAttachmentDeleteTemp", testUploadAttachmentDeleteTemp)
66+
t.Run("GetAttachment", testGetAttachment)
67+
}
68+
69+
func testUploadAttachmentDeleteTemp(t *testing.T) {
70+
session := loginUser(t, "user2")
71+
countTmpFile := func() int {
72+
// TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded file to os.TempDir() when it exceeds the max memory limit.
73+
files, err := fs.Glob(os.DirFS(os.TempDir()), "multipart-*") //nolint:usetesting // Golang's "http" package's behavior
74+
require.NoError(t, err)
75+
return len(files)
76+
}
77+
var tmpFileCountDuringUpload int
78+
defer test.MockVariableValue(&context.ParseMultipartFormMaxMemory, 1)()
79+
defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
80+
tmpFileCountDuringUpload = countTmpFile()
81+
})()
82+
_ = testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
83+
assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
84+
assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
85+
}
86+
87+
func testCreateAnonymousAttachment(t *testing.T) {
5788
session := emptyTestSession(t)
5889
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
5990
}
6091

61-
func TestCreateIssueAttachment(t *testing.T) {
62-
defer tests.PrepareTestEnv(t)()
92+
func testCreateUser2IssueAttachment(t *testing.T) {
6393
const repoURL = "user2/repo1"
6494
session := loginUser(t, "user2")
6595
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
@@ -90,8 +120,7 @@ func TestCreateIssueAttachment(t *testing.T) {
90120
MakeRequest(t, req, http.StatusOK)
91121
}
92122

93-
func TestGetAttachment(t *testing.T) {
94-
defer tests.PrepareTestEnv(t)()
123+
func testGetAttachment(t *testing.T) {
95124
adminSession := loginUser(t, "user1")
96125
user2Session := loginUser(t, "user2")
97126
user8Session := loginUser(t, "user8")

0 commit comments

Comments
 (0)