diff --git a/app/file.go b/app/file.go index 1901a8beeb..410e3ae9b3 100644 --- a/app/file.go +++ b/app/file.go @@ -397,6 +397,25 @@ func (a *App) UploadFiles(teamId string, channelId string, userId string, files return resStruct, nil } +// UploadFile uploads a single file in form of a completely constructed byte array for a channel. +func (a *App) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + info, _, appError := a.DoUploadFileExpectModification(time.Now(), "noteam", channelId, "nouser", filename, data) + + if appError != nil { + return nil, appError + } + + if info.PreviewPath != "" || info.ThumbnailPath != "" { + previewPathList := []string{info.PreviewPath} + thumbnailPathList := []string{info.ThumbnailPath} + imageDataList := [][]byte{data} + + a.HandleImages(previewPathList, thumbnailPathList, imageDataList) + } + + return info, nil +} + func (a *App) DoUploadFile(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) { info, _, err := a.DoUploadFileExpectModification(now, rawTeamId, rawChannelId, rawUserId, rawFilename, data) return info, err diff --git a/app/file_test.go b/app/file_test.go index c736328cfa..ee1319dca2 100644 --- a/app/file_test.go +++ b/app/file_test.go @@ -107,6 +107,30 @@ func TestDoUploadFile(t *testing.T) { } } +func TestUploadFile(t *testing.T) { + th := Setup() + defer th.TearDown() + + channelId := model.NewId() + filename := "test" + data := []byte("abcd") + + info1, err := th.App.UploadFile(data, channelId, filename) + if err != nil { + t.Fatal(err) + } else { + defer func() { + <-th.App.Srv.Store.FileInfo().PermanentDelete(info1.Id) + th.App.RemoveFile(info1.Path) + }() + } + + if info1.Path != fmt.Sprintf("%v/teams/noteam/channels/%v/users/nouser/%v/%v", + time.Now().Format("20060102"), channelId, info1.Id, filename) { + t.Fatal("stored file at incorrect path", info1.Path) + } +} + func TestGetInfoForFilename(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/plugin_api.go b/app/plugin_api.go index ef297694f3..a0c449b874 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -430,6 +430,10 @@ func (api *PluginAPI) ReadFile(path string) ([]byte, *model.AppError) { return api.app.ReadFile(path) } +func (api *PluginAPI) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + return api.app.UploadFile(data, channelId, filename) +} + func (api *PluginAPI) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) { return api.app.GetEmojiImage(emojiId) } diff --git a/model/file_info.go b/model/file_info.go index e0bbfcfc48..7a3e54a187 100644 --- a/model/file_info.go +++ b/model/file_info.go @@ -85,7 +85,7 @@ func (o *FileInfo) IsValid() *AppError { return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "", http.StatusBadRequest) } - if len(o.CreatorId) != 26 { + if len(o.CreatorId) != 26 && o.CreatorId != "nouser" { return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+o.Id, http.StatusBadRequest) } diff --git a/plugin/api.go b/plugin/api.go index 7d95006efa..2589fa9e01 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -309,6 +309,11 @@ type API interface { // Minimum server version: 5.6 GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) + // UploadFile will upload a file to a channel using a multipart request, to be later attached to a post. + // + // Minimum server version: 5.6 + UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) + // KVSet will store a key-value pair, unique per plugin. KVSet(key string, value []byte) *model.AppError diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 864d60a89c..6b20dd8679 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -2697,6 +2697,37 @@ func (s *apiRPCServer) GetEmojiImage(args *Z_GetEmojiImageArgs, returns *Z_GetEm return nil } +type Z_UploadFileArgs struct { + A []byte + B string + C string +} + +type Z_UploadFileReturns struct { + A *model.FileInfo + B *model.AppError +} + +func (g *apiRPCClient) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + _args := &Z_UploadFileArgs{data, channelId, filename} + _returns := &Z_UploadFileReturns{} + if err := g.client.Call("Plugin.UploadFile", _args, _returns); err != nil { + log.Printf("RPC call to UploadFile API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) UploadFile(args *Z_UploadFileArgs, returns *Z_UploadFileReturns) error { + if hook, ok := s.impl.(interface { + UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) + }); ok { + returns.A, returns.B = hook.UploadFile(args.A, args.B, args.C) + } else { + return encodableError(fmt.Errorf("API UploadFile called but not implemented.")) + } + return nil +} + type Z_KVSetArgs struct { A string B []byte diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index 9b03ed0b7b..14628095d2 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -1950,3 +1950,28 @@ func (_m *API) UpdateUserStatus(userId string, status string) (*model.Status, *m return r0, r1 } + +// UploadFile provides a mock function with given fields: data, channelId, filename +func (_m *API) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + ret := _m.Called(data, channelId, filename) + + var r0 *model.FileInfo + if rf, ok := ret.Get(0).(func([]byte, string, string) *model.FileInfo); ok { + r0 = rf(data, channelId, filename) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.FileInfo) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func([]byte, string, string) *model.AppError); ok { + r1 = rf(data, channelId, filename) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +}