s3store: Fix bug when completing empty upload

This commit is contained in:
Marius 2020-02-23 20:26:00 +01:00
parent 8ef7648713
commit 973a4fe066
3 changed files with 94 additions and 1 deletions

View File

@ -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)

View File

@ -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))

View File

@ -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()