diff --git a/pkg/handler/unrouted_handler.go b/pkg/handler/unrouted_handler.go index ad950b6..4bfde10 100644 --- a/pkg/handler/unrouted_handler.go +++ b/pkg/handler/unrouted_handler.go @@ -406,7 +406,10 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) // Directly finish the upload if the upload is empty (i.e. has a size of 0). // This statement is in an else-if block to avoid causing duplicate calls // to finishUploadIfComplete if an upload is empty and contains a chunk. - handler.finishUploadIfComplete(ctx, upload, info, r) + if err := handler.finishUploadIfComplete(ctx, upload, info, r); err != nil { + handler.sendError(w, r, err) + return + } } handler.sendResp(w, r, http.StatusCreated) diff --git a/pkg/s3store/s3store.go b/pkg/s3store/s3store.go index 6b77fec..02824ca 100644 --- a/pkg/s3store/s3store.go +++ b/pkg/s3store/s3store.go @@ -566,6 +566,30 @@ func (upload s3Upload) FinishUpload(ctx context.Context) error { return err } + if len(parts) == 0 { + // AWS expects at least one part to be present when completing the multipart + // upload. So if the tus upload has a size of 0, we create an empty part + // and use that for completing the multipart upload. + res, err := store.Service.UploadPartWithContext(ctx, &s3.UploadPartInput{ + Bucket: aws.String(store.Bucket), + Key: store.keyWithPrefix(uploadId), + UploadId: aws.String(multipartId), + PartNumber: aws.Int64(1), + Body: bytes.NewReader([]byte{}), + }) + if err != nil { + return err + } + + parts = []*s3.Part{ + &s3.Part{ + ETag: res.ETag, + PartNumber: aws.Int64(1), + }, + } + + } + // Transform the []*s3.Part slice to a []*s3.CompletedPart slice for the next // request. completedParts := make([]*s3.CompletedPart, len(parts)) diff --git a/pkg/s3store/s3store_test.go b/pkg/s3store/s3store_test.go index 8c1d4b0..2329df2 100644 --- a/pkg/s3store/s3store_test.go +++ b/pkg/s3store/s3store_test.go @@ -169,6 +169,72 @@ func TestNewUploadWithMetadataObjectPrefix(t *testing.T) { assert.NotNil(upload) } +func TestEmptyUpload(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + assert := assert.New(t) + + s3obj := NewMockS3API(mockCtrl) + store := New("bucket", s3obj) + + gomock.InOrder( + s3obj.EXPECT().CreateMultipartUploadWithContext(context.Background(), &s3.CreateMultipartUploadInput{ + Bucket: aws.String("bucket"), + Key: aws.String("uploadId"), + Metadata: map[string]*string{}, + }).Return(&s3.CreateMultipartUploadOutput{ + UploadId: aws.String("multipartId"), + }, nil), + s3obj.EXPECT().PutObjectWithContext(context.Background(), &s3.PutObjectInput{ + Bucket: aws.String("bucket"), + Key: aws.String("uploadId.info"), + Body: bytes.NewReader([]byte(`{"ID":"uploadId+multipartId","Size":0,"SizeIsDeferred":false,"Offset":0,"MetaData":null,"IsPartial":false,"IsFinal":false,"PartialUploads":null,"Storage":{"Bucket":"bucket","Key":"uploadId","Type":"s3store"}}`)), + ContentLength: aws.Int64(int64(208)), + }), + s3obj.EXPECT().ListPartsWithContext(context.Background(), &s3.ListPartsInput{ + Bucket: aws.String("bucket"), + Key: aws.String("uploadId"), + UploadId: aws.String("multipartId"), + PartNumberMarker: aws.Int64(0), + }).Return(&s3.ListPartsOutput{ + Parts: []*s3.Part{}, + }, nil), + s3obj.EXPECT().UploadPartWithContext(context.Background(), NewUploadPartInputMatcher(&s3.UploadPartInput{ + Bucket: aws.String("bucket"), + Key: aws.String("uploadId"), + UploadId: aws.String("multipartId"), + PartNumber: aws.Int64(1), + Body: bytes.NewReader([]byte("")), + })).Return(&s3.UploadPartOutput{ + ETag: aws.String("etag"), + }, nil), + s3obj.EXPECT().CompleteMultipartUploadWithContext(context.Background(), &s3.CompleteMultipartUploadInput{ + Bucket: aws.String("bucket"), + Key: aws.String("uploadId"), + UploadId: aws.String("multipartId"), + MultipartUpload: &s3.CompletedMultipartUpload{ + Parts: []*s3.CompletedPart{ + { + ETag: aws.String("etag"), + PartNumber: aws.Int64(1), + }, + }, + }, + }).Return(nil, nil), + ) + + info := handler.FileInfo{ + ID: "uploadId", + Size: 0, + } + + upload, err := store.NewUpload(context.Background(), info) + assert.Nil(err) + assert.NotNil(upload) + err = upload.FinishUpload(context.Background()) + assert.Nil(err) +} + func TestNewUploadLargerMaxObjectSize(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish()