From a78d43b16684865667d32ccc6bdcae6d7f06a324 Mon Sep 17 00:00:00 2001 From: Huxley Date: Wed, 24 Jun 2026 20:27:22 +0800 Subject: [PATCH] Remove client MIME type parameter from Upload - Fix: Always detect MIME type server-side from file content instead of trusting or forwarding the client-supplied value. --- internal/handler/file.go | 6 ++++-- internal/service/file.go | 23 +++++++++----------- internal/service/file_test.go | 40 +++++++++++++++++------------------ 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/internal/handler/file.go b/internal/handler/file.go index ce420ef..56ac17a 100644 --- a/internal/handler/file.go +++ b/internal/handler/file.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "mime" "net/http" "net/url" "strconv" @@ -44,7 +45,8 @@ func (h *FileHandler) Upload(c *gin.Context) { contentType := c.GetHeader("Content-Type") // Directory creation via JSON. - if len(contentType) >= 16 && contentType[:16] == "application/json" { + mediaType, _, _ := mime.ParseMediaType(contentType) + if mediaType == "application/json" { var req createDirRequest if err := c.ShouldBindJSON(&req); err != nil { api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error()) @@ -86,7 +88,7 @@ func (h *FileHandler) Upload(c *gin.Context) { } defer file.Close() - info, err := h.fileService.Upload(c.Request.Context(), userID, parentID, header.Filename, file, header.Header.Get("Content-Type")) + info, err := h.fileService.Upload(c.Request.Context(), userID, parentID, header.Filename, file) if err != nil { api.Error(c, mapError(err), err.Error()) return diff --git a/internal/service/file.go b/internal/service/file.go index 1a7a86e..99f2f87 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -60,7 +60,8 @@ func NewFileService( } // Upload stores a file's content and creates its metadata record. -func (s *FileService) Upload(ctx context.Context, userID string, parentID *string, fileName string, reader io.Reader, clientMime string) (*FileInfo, error) { +// The MIME type is always detected server-side from the file content. +func (s *FileService) Upload(ctx context.Context, userID string, parentID *string, fileName string, reader io.Reader) (*FileInfo, error) { if err := validateFileName(fileName); err != nil { return nil, err } @@ -83,19 +84,15 @@ func (s *FileService) Upload(ctx context.Context, userID string, parentID *strin reader = io.LimitReader(reader, s.maxUploadSize+1) } - // Detect MIME type when the client does not provide a meaningful one. - mimeType := clientMime - if mimeType == "" || mimeType == "application/octet-stream" { - // Read the first 512 bytes for detection, then replay them. - head := make([]byte, 512) - n, readErr := io.ReadFull(reader, head) - if readErr != nil && readErr != io.ErrUnexpectedEOF && readErr != io.EOF { - return nil, fmt.Errorf("read for mime detection: %w", readErr) - } - head = head[:n] - mimeType = mimetype.Detect(head).String() - reader = io.MultiReader(bytes.NewReader(head), reader) + // Detect MIME type from file content. + head := make([]byte, 512) + n, readErr := io.ReadFull(reader, head) + if readErr != nil && readErr != io.ErrUnexpectedEOF && readErr != io.EOF { + return nil, fmt.Errorf("read for mime detection: %w", readErr) } + head = head[:n] + mimeType := mimetype.Detect(head).String() + reader = io.MultiReader(bytes.NewReader(head), reader) fileID := uuid.NewString() storagePath := fmt.Sprintf("%s/%s", userID, fileID) diff --git a/internal/service/file_test.go b/internal/service/file_test.go index 8d0d2cd..fcce35c 100644 --- a/internal/service/file_test.go +++ b/internal/service/file_test.go @@ -77,7 +77,7 @@ func TestFileService_Upload(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - info, err := svc.Upload(ctx, "user1", nil, "hello.txt", strings.NewReader("hello world"), "") + info, err := svc.Upload(ctx, "user1", nil, "hello.txt", strings.NewReader("hello world")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -112,7 +112,7 @@ func TestFileService_UploadIntoDirectory(t *testing.T) { t.Fatalf("CreateDir = %v", err) } - info, err := svc.Upload(ctx, "user1", &dir.ID, "readme.txt", strings.NewReader("README"), "") + info, err := svc.Upload(ctx, "user1", &dir.ID, "readme.txt", strings.NewReader("README")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -127,7 +127,7 @@ func TestFileService_UploadInvalidName(t *testing.T) { invalidNames := []string{"", "foo/bar", "foo\\bar", ".", "..", "foo\x00bar"} for _, name := range invalidNames { - _, err := svc.Upload(ctx, "user1", nil, name, strings.NewReader("data"), "") + _, err := svc.Upload(ctx, "user1", nil, name, strings.NewReader("data")) if err == nil { t.Errorf("expected error for name %q, got nil", name) } @@ -138,12 +138,12 @@ func TestFileService_UploadDuplicateName(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - _, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("first"), "") + _, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("first")) if err != nil { t.Fatalf("first Upload = %v", err) } - _, err = svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("second"), "") + _, err = svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("second")) if err == nil { t.Fatal("expected duplicate name error, got nil") } @@ -154,7 +154,7 @@ func TestFileService_UploadNonexistentParent(t *testing.T) { ctx := context.Background() fakeParentID := "nonexistent-id" - _, err := svc.Upload(ctx, "user1", &fakeParentID, "file.txt", strings.NewReader("data"), "") + _, err := svc.Upload(ctx, "user1", &fakeParentID, "file.txt", strings.NewReader("data")) if err == nil { t.Fatal("expected error for nonexistent parent, got nil") } @@ -164,12 +164,12 @@ func TestFileService_UploadParentNotDirectory(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - file, err := svc.Upload(ctx, "user1", nil, "not-a-dir.txt", strings.NewReader("data"), "") + file, err := svc.Upload(ctx, "user1", nil, "not-a-dir.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } - _, err = svc.Upload(ctx, "user1", &file.ID, "child.txt", strings.NewReader("data"), "") + _, err = svc.Upload(ctx, "user1", &file.ID, "child.txt", strings.NewReader("data")) if err == nil { t.Fatal("expected error for non-directory parent, got nil") } @@ -180,7 +180,7 @@ func TestFileService_Download(t *testing.T) { ctx := context.Background() content := "download test content" - info, err := svc.Upload(ctx, "user1", nil, "download.txt", strings.NewReader(content), "") + info, err := svc.Upload(ctx, "user1", nil, "download.txt", strings.NewReader(content)) if err != nil { t.Fatalf("Upload = %v", err) } @@ -208,7 +208,7 @@ func TestFileService_DownloadForbidden(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data"), "") + info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -238,7 +238,7 @@ func TestFileService_Get(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - created, err := svc.Upload(ctx, "user1", nil, "info.txt", strings.NewReader("data"), "") + created, err := svc.Upload(ctx, "user1", nil, "info.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -266,7 +266,7 @@ func TestFileService_GetForbidden(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data"), "") + info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -285,11 +285,11 @@ func TestFileService_List(t *testing.T) { if err != nil { t.Fatalf("CreateDir = %v", err) } - _, err = svc.Upload(ctx, "user1", nil, "root.txt", strings.NewReader("root"), "") + _, err = svc.Upload(ctx, "user1", nil, "root.txt", strings.NewReader("root")) if err != nil { t.Fatalf("Upload root = %v", err) } - _, err = svc.Upload(ctx, "user1", &dir.ID, "nested.txt", strings.NewReader("nested"), "") + _, err = svc.Upload(ctx, "user1", &dir.ID, "nested.txt", strings.NewReader("nested")) if err != nil { t.Fatalf("Upload nested = %v", err) } @@ -317,7 +317,7 @@ func TestFileService_Update(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - info, err := svc.Upload(ctx, "user1", nil, "oldname.txt", strings.NewReader("data"), "") + info, err := svc.Upload(ctx, "user1", nil, "oldname.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -344,7 +344,7 @@ func TestFileService_UpdateMove(t *testing.T) { t.Fatalf("CreateDir 2 = %v", err) } - info, err := svc.Upload(ctx, "user1", &dir1.ID, "move.txt", strings.NewReader("data"), "") + info, err := svc.Upload(ctx, "user1", &dir1.ID, "move.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -362,7 +362,7 @@ func TestFileService_Delete(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - info, err := svc.Upload(ctx, "user1", nil, "delete-me.txt", strings.NewReader("data"), "") + info, err := svc.Upload(ctx, "user1", nil, "delete-me.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -385,7 +385,7 @@ func TestFileService_DeleteNonEmptyDirectory(t *testing.T) { if err != nil { t.Fatalf("CreateDir = %v", err) } - _, err = svc.Upload(ctx, "user1", &dir.ID, "child.txt", strings.NewReader("data"), "") + _, err = svc.Upload(ctx, "user1", &dir.ID, "child.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -400,7 +400,7 @@ func TestFileService_DeleteForbidden(t *testing.T) { svc := setupFileService(t) ctx := context.Background() - info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data"), "") + info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } @@ -451,7 +451,7 @@ func TestFileService_VerifyParentForbidden(t *testing.T) { t.Fatalf("CreateDir = %v", err) } - _, err = svc.Upload(ctx, "user2", &dir.ID, "stolen.txt", strings.NewReader("data"), "") + _, err = svc.Upload(ctx, "user2", &dir.ID, "stolen.txt", strings.NewReader("data")) if err != model.ErrForbidden { t.Errorf("expected ErrForbidden, got %v", err) }