summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-07 00:44:17 +0300
committerPaul Buetow <paul@buetow.org>2026-05-07 00:44:17 +0300
commit432e83b513cd7efe6d042c2948e590ee3f712b36 (patch)
tree3e9396bae631b033ce4e90d61fceaf17a0437216
parente98f0edb56f3d2355adf83a79edcc8ec2fa65a18 (diff)
Fix upload multipart memory budget for t0
-rw-r--r--internal/api/handlers_media.go5
-rw-r--r--internal/api/handlers_more_test.go63
2 files changed, 67 insertions, 1 deletions
diff --git a/internal/api/handlers_media.go b/internal/api/handlers_media.go
index a2ac421..8eed3cc 100644
--- a/internal/api/handlers_media.go
+++ b/internal/api/handlers_media.go
@@ -12,6 +12,8 @@ import (
"codeberg.org/snonux/player/internal/service"
)
+const multipartFormMemoryLimit = 32 << 20
+
// ------------------------------------------------------------------
// Sets
// ------------------------------------------------------------------
@@ -114,7 +116,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
maxBytes := int64(s.cfg.MaxUploadSizeMB) << 20
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
- if err := r.ParseMultipartForm(maxBytes); err != nil {
+ if err := r.ParseMultipartForm(multipartFormMemoryLimit); err != nil {
var mbe *http.MaxBytesError
if errors.As(err, &mbe) {
writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "file too large"})
@@ -123,6 +125,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
badRequest(w, "invalid multipart form")
return
}
+ defer r.MultipartForm.RemoveAll()
file, fh, err := r.FormFile("file")
if err != nil {
diff --git a/internal/api/handlers_more_test.go b/internal/api/handlers_more_test.go
index 0403f25..8333ccc 100644
--- a/internal/api/handlers_more_test.go
+++ b/internal/api/handlers_more_test.go
@@ -492,6 +492,69 @@ func TestServer_Upload(t *testing.T) {
}
}
+func TestServer_UploadLargeFileUsesTempFile(t *testing.T) {
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+ cfg := &internal.Config{SessionTimeoutHours: 24, MaxUploadSizeMB: 64}
+
+ var called bool
+ ms := &service.MockMediaService{
+ UploadMediaFunc: func(ctx context.Context, setID, userID int64, filename string, data io.Reader, size int64) (*model.Media, error) {
+ called = true
+ if setID != 1 || userID != 1 {
+ t.Fatalf("unexpected ids: set=%d user=%d", setID, userID)
+ }
+ if filename != "large.mp4" {
+ t.Fatalf("unexpected filename %q", filename)
+ }
+ if size != int64(multipartFormMemoryLimit)+1 {
+ t.Fatalf("unexpected size %d", size)
+ }
+ if _, ok := data.(*os.File); !ok {
+ t.Fatalf("expected multipart file to spill to temp file, got %T", data)
+ }
+ return &model.Media{ID: 1, FileName: filename}, nil
+ },
+ }
+ srv := newTestServer(t, store, nil, sm, cfg, ms, ms, ms, ms, ms, ms, nil, nil, nil, nil)
+
+ var buf bytes.Buffer
+ w := multipart.NewWriter(&buf)
+ part, err := w.CreateFormFile("file", "large.mp4")
+ if err != nil {
+ t.Fatal(err)
+ }
+ chunk := bytes.Repeat([]byte{1}, 1024*1024)
+ remaining := int64(multipartFormMemoryLimit) + 1
+ for remaining > 0 {
+ n := int64(len(chunk))
+ if remaining < n {
+ n = remaining
+ }
+ if _, err := part.Write(chunk[:n]); err != nil {
+ t.Fatal(err)
+ }
+ remaining -= n
+ }
+ if err := w.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest(http.MethodPost, "/api/sets/1/upload", &buf)
+ req.Header.Set("Content-Type", w.FormDataContentType())
+ req.AddCookie(sessionCookieForStore(t, store, sm, 1))
+ rr := httptest.NewRecorder()
+
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
+ }
+ if !called {
+ t.Fatal("expected upload service to be called")
+ }
+}
+
// ------------------------------------------------------------------
// Media detail, favorite, tags
// ------------------------------------------------------------------