Add multipart support for new resumable uploads API endpoint (#15510)
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8118cac350
Коммит
605f96fbea
@@ -4,6 +4,9 @@
|
||||
package api4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/audit"
|
||||
@@ -95,18 +98,38 @@ func uploadData(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > (us.FileSize - us.FileOffset) {
|
||||
c.Err = model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length",
|
||||
nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if us.UserId != c.App.Session().UserId || !c.App.SessionHasPermissionToChannel(*c.App.Session(), us.ChannelId, model.PERMISSION_UPLOAD_FILE) {
|
||||
c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := c.App.UploadData(us, r.Body)
|
||||
boundary, parseErr := parseMultipartRequestHeader(r)
|
||||
if parseErr != nil && !errors.Is(parseErr, http.ErrNotMultipart) {
|
||||
c.Err = model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_type",
|
||||
nil, parseErr.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var rd io.Reader
|
||||
if boundary != "" {
|
||||
mr := multipart.NewReader(r.Body, boundary)
|
||||
p, partErr := mr.NextPart()
|
||||
if partErr != nil {
|
||||
c.Err = model.NewAppError("uploadData", "api.upload.upload_data.multipart_error",
|
||||
nil, partErr.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rd = p
|
||||
} else {
|
||||
if r.ContentLength > (us.FileSize - us.FileOffset) {
|
||||
c.Err = model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length",
|
||||
nil, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rd = r.Body
|
||||
}
|
||||
|
||||
info, err := c.App.UploadData(us, rd)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
||||
@@ -6,6 +6,7 @@ package api4
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -193,7 +194,12 @@ func TestUploadData(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("bad content-length", func(t *testing.T) {
|
||||
info, resp := th.Client.UploadData(us.Id, bytes.NewReader(append(data, 0x00)))
|
||||
u, resp := th.Client.CreateUpload(us)
|
||||
require.Nil(t, resp.Error)
|
||||
require.NotEmpty(t, u)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
info, resp := th.Client.UploadData(u.Id, bytes.NewReader(append(data, 0x00)))
|
||||
require.Nil(t, info)
|
||||
require.Error(t, resp.Error)
|
||||
require.Equal(t, "api.upload.upload_data.invalid_content_length", resp.Error.Id)
|
||||
@@ -240,3 +246,101 @@ func TestUploadData(t *testing.T) {
|
||||
require.Equal(t, file, data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadDataMultipart(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
if *th.App.Config().FileSettings.DriverName == "" {
|
||||
t.Skip("skipping because no file driver is enabled")
|
||||
}
|
||||
|
||||
us := &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeAttachment,
|
||||
CreateAt: model.GetMillis(),
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Filename: "upload",
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
us, resp := th.Client.CreateUpload(us)
|
||||
require.Nil(t, resp.Error)
|
||||
require.NotNil(t, us)
|
||||
require.NotEmpty(t, us)
|
||||
|
||||
data := randomBytes(t, int(us.FileSize))
|
||||
|
||||
genMultipartData := func(t *testing.T, data []byte) (io.Reader, string) {
|
||||
mpData := &bytes.Buffer{}
|
||||
mpWriter := multipart.NewWriter(mpData)
|
||||
part, err := mpWriter.CreateFormFile("data", us.Filename)
|
||||
require.Nil(t, err)
|
||||
n, err := part.Write(data)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, len(data), n)
|
||||
err = mpWriter.Close()
|
||||
require.Nil(t, err)
|
||||
return mpData, mpWriter.FormDataContentType()
|
||||
}
|
||||
|
||||
t.Run("bad content-type", func(t *testing.T) {
|
||||
info, resp := th.Client.DoUploadFile("/uploads/"+us.Id, data, "multipart/form-data;")
|
||||
require.Nil(t, info)
|
||||
require.Error(t, resp.Error)
|
||||
require.Equal(t, "api.upload.upload_data.invalid_content_type", resp.Error.Id)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
mpData, contentType := genMultipartData(t, data)
|
||||
|
||||
req, err := http.NewRequest("POST", th.Client.ApiUrl+"/uploads/"+us.Id, mpData)
|
||||
require.Nil(t, err)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
req.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken)
|
||||
res, err := th.Client.HttpClient.Do(req)
|
||||
require.Nil(t, err)
|
||||
info := model.FileInfoFromJson(res.Body)
|
||||
res.Body.Close()
|
||||
require.NotEmpty(t, info)
|
||||
require.Equal(t, us.Filename, info.Name)
|
||||
|
||||
file, resp := th.Client.GetFile(info.Id)
|
||||
require.Nil(t, resp.Error)
|
||||
require.Equal(t, file, data)
|
||||
})
|
||||
|
||||
t.Run("resume success", func(t *testing.T) {
|
||||
mpData, contentType := genMultipartData(t, data[:5*1024*1024])
|
||||
|
||||
u, resp := th.Client.CreateUpload(us)
|
||||
require.Nil(t, resp.Error)
|
||||
require.NotNil(t, u)
|
||||
require.NotEmpty(t, u)
|
||||
|
||||
req, err := http.NewRequest("POST", th.Client.ApiUrl+"/uploads/"+u.Id, mpData)
|
||||
require.Nil(t, err)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
req.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken)
|
||||
res, err := th.Client.HttpClient.Do(req)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
||||
require.Equal(t, int64(0), res.ContentLength)
|
||||
|
||||
mpData, contentType = genMultipartData(t, data[5*1024*1024:])
|
||||
|
||||
req, err = http.NewRequest("POST", th.Client.ApiUrl+"/uploads/"+u.Id, mpData)
|
||||
require.Nil(t, err)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
req.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken)
|
||||
res, err = th.Client.HttpClient.Do(req)
|
||||
require.Nil(t, err)
|
||||
info := model.FileInfoFromJson(res.Body)
|
||||
res.Body.Close()
|
||||
require.NotEmpty(t, info)
|
||||
require.Equal(t, u.Filename, info.Name)
|
||||
|
||||
file, resp := th.Client.GetFile(info.Id)
|
||||
require.Nil(t, resp.Error)
|
||||
require.Equal(t, file, data)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2734,6 +2734,14 @@
|
||||
"id": "api.upload.upload_data.invalid_content_length",
|
||||
"translation": "Invalid Content-Length."
|
||||
},
|
||||
{
|
||||
"id": "api.upload.upload_data.invalid_content_type",
|
||||
"translation": "Invalid Content-Type for multipart upload."
|
||||
},
|
||||
{
|
||||
"id": "api.upload.upload_data.multipart_error",
|
||||
"translation": "Failed to process the multipart data."
|
||||
},
|
||||
{
|
||||
"id": "api.user.activate_mfa.email_and_ldap_only.app_error",
|
||||
"translation": "MFA is not available for this account type."
|
||||
|
||||
Ссылка в новой задаче
Block a user