diff --git a/weed/replication/sink/s3sink/s3_sink.go b/weed/replication/sink/s3sink/s3_sink.go index 9d295292f..310af9878 100644 --- a/weed/replication/sink/s3sink/s3_sink.go +++ b/weed/replication/sink/s3sink/s3_sink.go @@ -209,6 +209,9 @@ func (s3sink *S3Sink) CreateEntry(key string, entry *filer_pb.Entry, signatures if tags != "" { uploadInput.Tagging = aws.String(tags) } + if entry.Attributes.Mime != "" { + uploadInput.ContentType = aws.String(entry.Attributes.Mime) + } if len(entry.Attributes.Md5) > 0 { uploadInput.ContentMD5 = aws.String(base64.StdEncoding.EncodeToString([]byte(entry.Attributes.Md5))) } diff --git a/weed/replication/sink/s3sink/s3_sink_test.go b/weed/replication/sink/s3sink/s3_sink_test.go index e20efa5ec..63bf8373a 100644 --- a/weed/replication/sink/s3sink/s3_sink_test.go +++ b/weed/replication/sink/s3sink/s3_sink_test.go @@ -1,11 +1,19 @@ package S3Sink import ( + "io" + "net/http" "net/url" "strings" "testing" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + awss3 "github.com/aws/aws-sdk-go/service/s3" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/stretchr/testify/require" ) func TestBuildTaggingString_ShouldStripTagPrefix(t *testing.T) { @@ -44,6 +52,72 @@ func TestBuildTaggingString_ShouldURLEncodeValues(t *testing.T) { } } +// capturePutRoundTripper records the s3manager.Uploader's PUT and replies 200 +// so the SDK is satisfied without making a real network call. +type capturePutRoundTripper struct { + uploadReq *http.Request +} + +func (c *capturePutRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if req.Method == http.MethodPut { + c.uploadReq = req.Clone(req.Context()) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + _ = req.Body.Close() + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("")), + Header: http.Header{"ETag": []string{"\"etag\""}}, + Request: req, + }, nil +} + +func newCapturingS3Sink(t *testing.T) (*S3Sink, *capturePutRoundTripper) { + t.Helper() + rt := &capturePutRoundTripper{} + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("us-east-1"), + Endpoint: aws.String("https://example.invalid"), + S3ForcePathStyle: aws.Bool(true), + Credentials: credentials.NewStaticCredentials("k", "s", ""), + HTTPClient: &http.Client{Transport: rt}, + }) + require.NoError(t, err) + s := &S3Sink{ + conn: awss3.New(sess), + bucket: "bucket", + uploaderConcurrency: 1, + uploaderPartSizeMb: 5, + } + return s, rt +} + +func TestS3SinkCreateEntryPassesMimeAsContentType(t *testing.T) { + sink, rt := newCapturingS3Sink(t) + entry := &filer_pb.Entry{ + Attributes: &filer_pb.FuseAttributes{Mime: "text/html"}, + Content: []byte(""), + } + require.NoError(t, sink.CreateEntry("/dir/test.html", entry, nil)) + + require.NotNil(t, rt.uploadReq, "uploader should have issued a PUT") + require.Equal(t, "text/html", rt.uploadReq.Header.Get("Content-Type")) +} + +func TestS3SinkCreateEntryOmitsContentTypeWhenMimeMissing(t *testing.T) { + sink, rt := newCapturingS3Sink(t) + entry := &filer_pb.Entry{ + Attributes: &filer_pb.FuseAttributes{}, + Content: []byte("data"), + } + require.NoError(t, sink.CreateEntry("/dir/test.bin", entry, nil)) + + require.NotNil(t, rt.uploadReq, "uploader should have issued a PUT") + require.Equal(t, "", rt.uploadReq.Header.Get("Content-Type")) +} + func TestBuildTaggingString_EmptyWhenNoTags(t *testing.T) { extended := map[string][]byte{ "Content-Encoding": []byte("gzip"),