[s3]: preserve multipart copy checksums (#9948)

* s3: preserve checksums for copied multipart parts

* s3: return checksums from multipart copy

* s3: pin the upload's checksum algorithm on copy-part re-stream

* s3: note why UploadPartCopy uses the re-stream slow path

* s3: explain the TLS proxy in the multipart copy checksum test

* s3: cover nil and unknown-algorithm edge cases in copy checksum tests

* s3: cover all checksum algorithms in the multipart copy test

* s3: run all checksum integration tests, not just presigned
This commit is contained in:
Chris Lu authored and GitHub committed 2026-06-14 00:16:14 -07:00
1 parent da243b9423
commit 561768a426
6 files changed
+331 -44

No files matched your search

+2 -2
View File
@@ -1,5 +1,5 @@
# S3 Checksum Integration Tests
# Covers flexible-checksum behavior on presigned URL uploads (issue #9075).
# Covers flexible-checksum behavior on presigned uploads and multipart copy.
.PHONY: help build-weed check-deps start-server stop-server test test-with-server logs clean health-check
@@ -8,7 +8,7 @@ S3_PORT := 8333
ACCESS_KEY ?= some_access_key1
SECRET_KEY ?= some_secret_key1
TEST_TIMEOUT := 10m
TEST_PATTERN ?= TestPresignedPut
TEST_PATTERN ?= .
SERVER_DIR := ./test-volume-data/server-data
help:
@@ -0,0 +1,124 @@
package checksum_test
import (
"bytes"
"context"
"fmt"
"net/http/httptest"
"net/http/httputil"
"net/url"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/require"
)
func TestMultipartCopyPreservesChecksum(t *testing.T) {
// aws-sdk-go-v2 sends flexible checksums as unsigned streaming trailers, which
// it refuses over plain HTTP, so front the HTTP endpoint with a TLS proxy.
target, err := url.Parse(defaultConfig.Endpoint)
require.NoError(t, err)
proxy := httputil.NewSingleHostReverseProxy(target)
server := httptest.NewTLSServer(proxy)
defer server.Close()
cfg, err := config.LoadDefaultConfig(context.Background(),
config.WithRegion(defaultConfig.Region),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
defaultConfig.AccessKey, defaultConfig.SecretKey, "")),
config.WithHTTPClient(server.Client()),
)
require.NoError(t, err)
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(server.URL)
o.UsePathStyle = true
})
bucket := uniqueBucket()
createBucket(t, client, bucket)
defer cleanupBucket(t, client, bucket)
cases := []struct {
algorithm types.ChecksumAlgorithm
srcSum func(*s3.PutObjectOutput) *string
partSum func(*types.CopyPartResult) *string
setPart func(*types.CompletedPart, *string)
}{
{
algorithm: types.ChecksumAlgorithmCrc32,
srcSum: func(o *s3.PutObjectOutput) *string { return o.ChecksumCRC32 },
partSum: func(r *types.CopyPartResult) *string { return r.ChecksumCRC32 },
setPart: func(p *types.CompletedPart, v *string) { p.ChecksumCRC32 = v },
},
{
algorithm: types.ChecksumAlgorithmCrc32c,
srcSum: func(o *s3.PutObjectOutput) *string { return o.ChecksumCRC32C },
partSum: func(r *types.CopyPartResult) *string { return r.ChecksumCRC32C },
setPart: func(p *types.CompletedPart, v *string) { p.ChecksumCRC32C = v },
},
{
algorithm: types.ChecksumAlgorithmCrc64nvme,
srcSum: func(o *s3.PutObjectOutput) *string { return o.ChecksumCRC64NVME },
partSum: func(r *types.CopyPartResult) *string { return r.ChecksumCRC64NVME },
setPart: func(p *types.CompletedPart, v *string) { p.ChecksumCRC64NVME = v },
},
{
algorithm: types.ChecksumAlgorithmSha1,
srcSum: func(o *s3.PutObjectOutput) *string { return o.ChecksumSHA1 },
partSum: func(r *types.CopyPartResult) *string { return r.ChecksumSHA1 },
setPart: func(p *types.CompletedPart, v *string) { p.ChecksumSHA1 = v },
},
{
algorithm: types.ChecksumAlgorithmSha256,
srcSum: func(o *s3.PutObjectOutput) *string { return o.ChecksumSHA256 },
partSum: func(r *types.CopyPartResult) *string { return r.ChecksumSHA256 },
setPart: func(p *types.CompletedPart, v *string) { p.ChecksumSHA256 = v },
},
}
for _, tc := range cases {
t.Run(string(tc.algorithm), func(t *testing.T) {
sourceKey := "source-" + string(tc.algorithm)
source, err := client.PutObject(context.Background(), &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(sourceKey),
Body: bytes.NewBufferString("trailing checksum"),
ChecksumAlgorithm: tc.algorithm,
})
require.NoError(t, err)
key := "multipart-copy-" + string(tc.algorithm)
create, err := client.CreateMultipartUpload(context.Background(), &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ChecksumAlgorithm: tc.algorithm,
})
require.NoError(t, err)
part, err := client.UploadPartCopy(context.Background(), &s3.UploadPartCopyInput{
Bucket: aws.String(bucket),
CopySource: aws.String(fmt.Sprintf("%s/%s", bucket, sourceKey)),
Key: aws.String(key),
UploadId: create.UploadId,
PartNumber: aws.Int32(1),
})
require.NoError(t, err)
require.Equal(t, aws.ToString(tc.srcSum(source)), aws.ToString(tc.partSum(part.CopyPartResult)))
completed := types.CompletedPart{ETag: part.CopyPartResult.ETag, PartNumber: aws.Int32(1)}
tc.setPart(&completed, tc.partSum(part.CopyPartResult))
_, err = client.CompleteMultipartUpload(context.Background(), &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: create.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{Parts: []types.CompletedPart{completed}},
})
require.NoError(t, err)
})
}
}