Skip to content

Commit

Permalink
Upload-Length in POST requests
Browse files Browse the repository at this point in the history
  • Loading branch information
Acconut committed Nov 2, 2024
1 parent df25c35 commit afba1b0
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 3 deletions.
128 changes: 128 additions & 0 deletions pkg/handler/post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,134 @@ func TestPost(t *testing.T) {
},
}, res.InformationalResponses)
})

if interopVersion != "3" && interopVersion != "4" && interopVersion != "5" {
SubTest(t, "UploadLengthAndContentLengthMatch", func(t *testing.T, store *MockFullDataStore, _ *StoreComposer) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
locker := NewMockFullLocker(ctrl)
lock := NewMockFullLock(ctrl)
upload := NewMockFullUpload(ctrl)

gomock.InOrder(
store.EXPECT().NewUpload(gomock.Any(), FileInfo{
SizeIsDeferred: false,
Size: 11,
MetaData: map[string]string{},
}).Return(upload, nil),
upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{
ID: "foo",
SizeIsDeferred: false,
Size: 11,
}, nil),
locker.EXPECT().NewLock("foo").Return(lock, nil),
lock.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil),
upload.EXPECT().WriteChunk(gomock.Any(), int64(0), NewReaderMatcher("hello world")).Return(int64(11), nil),
upload.EXPECT().FinishUpload(gomock.Any()).Return(nil),
lock.EXPECT().Unlock().Return(nil),
)

composer := NewStoreComposer()
composer.UseCore(store)
composer.UseLocker(locker)

handler, _ := NewHandler(Config{
StoreComposer: composer,
BasePath: "/files/",
EnableExperimentalProtocol: true,
})

(&httpTest{
Method: "POST",
ReqHeader: map[string]string{
"Upload-Draft-Interop-Version": interopVersion,
"Upload-Length": "11",
"Upload-Complete": "?1",
},
ReqBody: strings.NewReader("hello world"),
Code: http.StatusCreated,
ResHeader: map[string]string{
"Upload-Draft-Interop-Version": interopVersion,
"Location": "http://tus.io/files/foo",
"Upload-Offset": "11",
},
}).Run(handler, t)
})

SubTest(t, "UploadLengthAndContentLengthMismatch", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

handler, _ := NewHandler(Config{
StoreComposer: composer,
BasePath: "/files/",
EnableExperimentalProtocol: true,
})

(&httpTest{
Method: "POST",
ReqHeader: map[string]string{
"Upload-Draft-Interop-Version": interopVersion,
"Upload-Length": "999999",
"Upload-Complete": "?1",
},
ReqBody: strings.NewReader("hello world"),
Code: http.StatusBadRequest,
ResBody: "ERR_INVALID_UPLOAD_LENGTH: missing or invalid Upload-Length header\n",
}).Run(handler, t)
})

SubTest(t, "OnlyUploadLength", func(t *testing.T, store *MockFullDataStore, _ *StoreComposer) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
locker := NewMockFullLocker(ctrl)
lock := NewMockFullLock(ctrl)
upload := NewMockFullUpload(ctrl)

gomock.InOrder(
store.EXPECT().NewUpload(gomock.Any(), FileInfo{
SizeIsDeferred: false,
Size: 11,
MetaData: map[string]string{},
}).Return(upload, nil),
upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{
ID: "foo",
SizeIsDeferred: false,
Size: 11,
}, nil),
locker.EXPECT().NewLock("foo").Return(lock, nil),
lock.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil),
upload.EXPECT().WriteChunk(gomock.Any(), int64(0), NewReaderMatcher("hello ")).Return(int64(6), nil),
lock.EXPECT().Unlock().Return(nil),
)

composer := NewStoreComposer()
composer.UseCore(store)
composer.UseLocker(locker)

handler, _ := NewHandler(Config{
StoreComposer: composer,
BasePath: "/files/",
EnableExperimentalProtocol: true,
})

(&httpTest{
Method: "POST",
ReqHeader: map[string]string{
"Upload-Draft-Interop-Version": interopVersion,
"Upload-Length": "11",
"Upload-Complete": "?0",
},
ReqBody: strings.NewReader("hello "),
Code: http.StatusCreated,
ResHeader: map[string]string{
"Upload-Draft-Interop-Version": interopVersion,
"Location": "http://tus.io/files/foo",
"Upload-Offset": "6",
},
}).Run(handler, t)
})
}
})
}
})
Expand Down
55 changes: 52 additions & 3 deletions pkg/handler/unrouted_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,15 @@ func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Reques
info := FileInfo{
MetaData: make(MetaData),
}
if willCompleteUpload && r.ContentLength != -1 {
// If the client wants to perform the upload in one request with Content-Length, we know the final upload size.
info.Size = r.ContentLength

size, sizeIsDeferred, err := getIETFDraftUploadLength(r)
if err != nil {
handler.sendError(c, err)
return
}

if !sizeIsDeferred {
info.Size = size
} else {
// Error out if the storage does not support upload length deferring, but we need it.
if !handler.composer.UsesLengthDeferrer {
Expand Down Expand Up @@ -1432,6 +1438,49 @@ func setIETFDraftUploadComplete(r *http.Request, resp HTTPResponse, isComplete b
}
}

// getIETFDraftUploadLength returns the length of an upload as defined in the
// resumable upload draft from IETF. This can either be in the Upload-Length
// header or in the Content-Length header.
func getIETFDraftUploadLength(r *http.Request) (length int64, lengthIsDeferred bool, err error) {
var lengthFromUploadLength int64
hasLengthFromUploadLength := false
var lengthFromContentLength int64
hasLengthFromContentLength := false

willCompleteUpload := isIETFDraftUploadComplete(r)
if willCompleteUpload && r.ContentLength != -1 {
lengthFromContentLength = r.ContentLength
hasLengthFromContentLength = true
}

uploadLengthStr := r.Header.Get("Upload-Length")
if uploadLengthStr != "" {
var err error
lengthFromUploadLength, err = strconv.ParseInt(uploadLengthStr, 10, 64)
if err != nil {
return 0, false, ErrInvalidUploadLength
}

hasLengthFromUploadLength = true
}

// If both lengths are set, they must match
if hasLengthFromContentLength && hasLengthFromUploadLength && lengthFromUploadLength != lengthFromContentLength {
return 0, false, ErrInvalidUploadLength
}

// Return whichever length is set
if hasLengthFromUploadLength {
return lengthFromUploadLength, false, nil
}
if hasLengthFromContentLength {
return lengthFromContentLength, false, nil
}

// No length set, so it's deferred
return 0, true, nil
}

// ParseMetadataHeader parses the Upload-Metadata header as defined in the
// File Creation extension.
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
Expand Down

0 comments on commit afba1b0

Please sign in to comment.