fix(replication/s3sink): forward entry.Attributes.Mime as ContentType

Same gap as the remote_storage S3 client: filer.replicate uploads via
s3manager.Uploader without populating ContentType, so replicated objects
on S3-compatible backends (e.g. Backblaze B2) store binary/octet-stream
and browsers refuse to render HTML, CSS, etc.

Pass entry.Attributes.Mime through to UploadInput.ContentType, leaving
the header unset when no Mime is recorded so the remote keeps its own
default.
This commit is contained in:
Chris Lu
2026-05-27 11:14:14 -07:00
parent cd68313929
commit 3d4bef39dd
2 changed files with 77 additions and 0 deletions
+3
View File
@@ -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)))
}
@@ -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("<html></html>"),
}
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"),