From b460d024bf44b7c590a602e0c13462f9c03bb123 Mon Sep 17 00:00:00 2001 From: MacKinley Smith Date: Mon, 25 Nov 2024 02:30:55 -0700 Subject: [PATCH] s3store: Pass Content-Type from `filetype` metadata field to S3 (#1217) * feat: pass along the contentType metadata field if supplied This change causes tusd for Go to match the NodeJS version's behavior, discussed here: https://stackoverflow.com/questions/74148196/how-to-resolve-application-octet-stream-in-s3-using-tus-node-tusd-uppy-or-net * feat: respect filetype if passed and contentType is not * chore: remove contentType handling * Move test into existing test function * Add documentation --------- Co-authored-by: Marius Kleidl --- docs/_storage-backends/aws-s3.md | 2 ++ pkg/s3store/s3store.go | 8 ++++++-- pkg/s3store/s3store_test.go | 15 +++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/_storage-backends/aws-s3.md b/docs/_storage-backends/aws-s3.md index 923a0d6e9..dcde1072f 100644 --- a/docs/_storage-backends/aws-s3.md +++ b/docs/_storage-backends/aws-s3.md @@ -89,6 +89,8 @@ If [metadata](https://tus.io/protocols/resumable-upload#upload-metadata) is asso In addition, the metadata is also stored in the informational object, which can be used to retrieve the original metadata without any characters being replaced. +If the metadata contains a `filetype` key, its value is used to set the `Content-Type` header of the file object. Setting the `Content-Disposition` or `Content-Encoding` headers is not yet supported. + ## Considerations When receiving a `PATCH` request, parts of its body will be temporarily stored on disk before they can be transferred to S3. This is necessary to meet the minimum part size for an S3 multipart upload enforced by S3 and to allow the AWS SDK to calculate a checksum. Once the part has been uploaded to S3, the temporary file will be removed immediately. Therefore, please ensure that the server running this storage backend has enough disk space available to hold these temporary files. diff --git a/pkg/s3store/s3store.go b/pkg/s3store/s3store.go index 7b5acafff..b64bf7802 100644 --- a/pkg/s3store/s3store.go +++ b/pkg/s3store/s3store.go @@ -326,11 +326,15 @@ func (store S3Store) NewUpload(ctx context.Context, info handler.FileInfo) (hand // Create the actual multipart upload t := time.Now() - res, err := store.Service.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + multipartUploadInput := &s3.CreateMultipartUploadInput{ Bucket: aws.String(store.Bucket), Key: store.keyWithPrefix(objectId), Metadata: metadata, - }) + } + if fileType, found := info.MetaData["filetype"]; found { + multipartUploadInput.ContentType = aws.String(fileType) + } + res, err := store.Service.CreateMultipartUpload(ctx, multipartUploadInput) store.observeRequestDuration(t, metricCreateMultipartUpload) if err != nil { return nil, fmt.Errorf("s3store: unable to create multipart upload:\n%s", err) diff --git a/pkg/s3store/s3store_test.go b/pkg/s3store/s3store_test.go index e69ba2e3f..15270c3b2 100644 --- a/pkg/s3store/s3store_test.go +++ b/pkg/s3store/s3store_test.go @@ -45,17 +45,19 @@ func TestNewUpload(t *testing.T) { Bucket: aws.String("bucket"), Key: aws.String("uploadId"), Metadata: map[string]string{ - "foo": "hello", - "bar": "men???hi", + "foo": "hello", + "bar": "men???hi", + "filetype": "application/pdf", }, + ContentType: aws.String("application/pdf"), }).Return(&s3.CreateMultipartUploadOutput{ UploadId: aws.String("multipartId"), }, nil), s3obj.EXPECT().PutObject(context.Background(), &s3.PutObjectInput{ Bucket: aws.String("bucket"), Key: aws.String("uploadId.info"), - Body: bytes.NewReader([]byte(`{"ID":"uploadId+multipartId","Size":500,"SizeIsDeferred":false,"Offset":0,"MetaData":{"bar":"menü\r\nhi","foo":"hello"},"IsPartial":false,"IsFinal":false,"PartialUploads":null,"Storage":{"Bucket":"bucket","Key":"uploadId","Type":"s3store"}}`)), - ContentLength: aws.Int64(241), + Body: bytes.NewReader([]byte(`{"ID":"uploadId+multipartId","Size":500,"SizeIsDeferred":false,"Offset":0,"MetaData":{"bar":"menü\r\nhi","filetype":"application/pdf","foo":"hello"},"IsPartial":false,"IsFinal":false,"PartialUploads":null,"Storage":{"Bucket":"bucket","Key":"uploadId","Type":"s3store"}}`)), + ContentLength: aws.Int64(270), }), ) @@ -63,8 +65,9 @@ func TestNewUpload(t *testing.T) { ID: "uploadId", Size: 500, MetaData: map[string]string{ - "foo": "hello", - "bar": "menü\r\nhi", + "foo": "hello", + "bar": "menü\r\nhi", + "filetype": "application/pdf", }, }