From 9a03c09396facdd5cbe65a6fb668907c81ec7766 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 21 Oct 2026 14:15:04 -0700 Subject: [PATCH] s3api: cover the inline-content UploadPartCopy route at handler level (#11914) s3api: exercise the inline-content route through CopyObjectPartHandler Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../s3api_object_handlers_copy_etag_test.go | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/weed/s3api/s3api_object_handlers_copy_etag_test.go b/weed/s3api/s3api_object_handlers_copy_etag_test.go index 9860a03971..a6ec92bfac 100644 --- a/weed/s3api/s3api_object_handlers_copy_etag_test.go +++ b/weed/s3api/s3api_object_handlers_copy_etag_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/gorilla/mux" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" @@ -375,6 +376,63 @@ func TestCopyObjectPartInlineContent(t *testing.T) { } } +// The handler must route an inline-Content source away from the raw chunk +// copy — CopyObjectPartHandler has to send it through the plaintext stream +// path or the part lands as zeros. +func TestCopyObjectPartHandlerInlineContent(t *testing.T) { + volume := startFakeVolumeServer(t) + filerImpl := &ambiguousPutFiler{volume: volume, apply: true, entries: map[string]*filer_pb.Entry{}} + s3a := newPutTestServer(t, startFakeFiler(t, filerImpl)) + s3a.bucketConfigCache = NewBucketConfigCache(time.Minute) + util_http.InitGlobalHttpClient() + + content := []byte("inline-content") + uploadID := s3a.generateUploadID("dst") + filerImpl.entries["/buckets/b"] = &filer_pb.Entry{Name: "b", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}} + filerImpl.entries["/buckets/b/src"] = &filer_pb.Entry{ + Name: "src", + Attributes: &filer_pb.FuseAttributes{FileSize: uint64(len(content))}, + Content: content, + } + filerImpl.entries["/buckets/b/.uploads/"+uploadID] = &filer_pb.Entry{ + Name: uploadID, + IsDirectory: true, + Extended: map[string][]byte{s3_constants.ExtMultipartObjectKey: []byte("dst")}, + } + + r := httptest.NewRequest(http.MethodPut, "/b/dst?partNumber=1&uploadId="+uploadID, nil) + r = mux.SetURLVars(r, map[string]string{"bucket": "b", "object": "dst"}) + r.Header.Set("X-Amz-Copy-Source", "/b/src") + r.Header.Set("x-amz-copy-source-range", "bytes=2-5") + w := httptest.NewRecorder() + s3a.CopyObjectPartHandler(w, r) + if w.Code != http.StatusOK { + t.Fatalf("CopyObjectPartHandler = %d %s, want 200", w.Code, w.Body.String()) + } + wantMd5 := fmt.Sprintf("%x", md5.Sum(content[2:6])) + if !strings.Contains(w.Body.String(), wantMd5) { + t.Errorf("response %q does not contain copied-range ETag %q", w.Body.String(), wantMd5) + } + var partEntry *filer_pb.Entry + for key, e := range filerImpl.entries { + if strings.Contains(key, uploadID) && !e.IsDirectory { + partEntry = e + } + } + if partEntry == nil || len(partEntry.Chunks) == 0 { + t.Fatalf("no part entry with chunks stored: %+v", filerImpl.entries) + } + found := false + for _, stored := range volume.stored { + if bytes.Contains(stored, content[2:6]) { + found = true + } + } + if !found { + t.Errorf("destination volume does not hold the copied content bytes") + } +} + // An empty part must carry the empty-body MD5 on the stored entry too, so // ListParts and complete's composite ETag see the same digest CopyPartResult // reported.