From c9d49536f8b02cdfe7dc1c59ae71ecd87efc724f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 30 Aug 2023 22:43:40 +0200 Subject: [PATCH] Adding SetFileSearchableContent plugin API endpoint (#24355) * Adding SetFileSearchableContent plugin API endpoint * Fixing CI problems * Fixing CI problems * Fixing CI problems * Fixing CI problems * Fixing CI problems * Exposing it to the public API * Fix CI problems * Adding SetSearchableContent to the pluginapi File struct --- server/channels/app/app_iface.go | 1 + server/channels/app/file.go | 20 +++++++++++++ server/channels/app/file_test.go | 27 +++++++++++++++++ .../app/opentracing/opentracing_layer.go | 22 ++++++++++++++ server/channels/app/plugin_api.go | 4 +++ server/channels/app/post.go | 20 ++++++------- server/i18n/en.json | 4 +++ server/public/plugin/api.go | 6 ++++ .../plugin/api_timer_layer_generated.go | 7 +++++ server/public/plugin/client_rpc_generated.go | 29 +++++++++++++++++++ server/public/plugin/plugintest/api.go | 16 ++++++++++ server/public/pluginapi/file.go | 8 +++++ 12 files changed, 154 insertions(+), 10 deletions(-) diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 3198690d4de..0a99412f342 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -1069,6 +1069,7 @@ type AppIface interface { SetChannels(ch *Channels) SetCustomStatus(c request.CTX, userID string, cs *model.CustomStatus) *model.AppError SetDefaultProfileImage(c request.CTX, user *model.User) *model.AppError + SetFileSearchableContent(fileID string, data string) *model.AppError SetPhase2PermissionsMigrationStatus(isComplete bool) error SetPluginKey(pluginID string, key string, value []byte) *model.AppError SetPluginKeyWithExpiry(pluginID string, key string, value []byte, expireInSeconds int64) *model.AppError diff --git a/server/channels/app/file.go b/server/channels/app/file.go index 31cf068367d..b4999864694 100644 --- a/server/channels/app/file.go +++ b/server/channels/app/file.go @@ -1224,6 +1224,26 @@ func (a *App) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) { return fileInfo, appErr } +func (a *App) SetFileSearchableContent(fileID string, data string) *model.AppError { + fileInfo, appErr := a.Srv().getFileInfo(fileID) + if appErr != nil { + return appErr + } + + err := a.Srv().Store().FileInfo().SetContent(fileInfo.Id, data) + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return model.NewAppError("SetFileSearchableContent", "app.file_info.set_searchable_content.app_error", nil, "", http.StatusNotFound).Wrap(err) + default: + return model.NewAppError("SetFileSearchableContent", "app.file_info.set_searchable_content.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + } + + return nil +} + func (a *App) getFileInfoIgnoreCloudLimit(fileID string) (*model.FileInfo, *model.AppError) { fileInfo, appErr := a.Srv().getFileInfo(fileID) if appErr == nil { diff --git a/server/channels/app/file_test.go b/server/channels/app/file_test.go index 06977c11f10..aa4a7024ddd 100644 --- a/server/channels/app/file_test.go +++ b/server/channels/app/file_test.go @@ -650,3 +650,30 @@ func TestComputeLastAccessibleFileTime(t *testing.T) { }) } + +func TestSetFileSearchableContent(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + fileInfo, err := th.App.Srv().Store().FileInfo().Save(&model.FileInfo{ + CreatorId: th.BasicUser.Id, + PostId: th.BasicPost.Id, + ChannelId: th.BasicPost.ChannelId, + Name: "test", + Path: "test", + Extension: "jpg", + MimeType: "image/jpeg", + }) + require.NoError(t, err) + + result, appErr := th.App.SearchFilesInTeamForUser(th.Context, "searchable", th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, 0, 60) + require.Nil(t, appErr) + assert.Equal(t, 0, len(result.Order)) + + appErr = th.App.SetFileSearchableContent(fileInfo.Id, "searchable") + require.Nil(t, appErr) + + result, appErr = th.App.SearchFilesInTeamForUser(th.Context, "searchable", th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, 0, 60) + require.Nil(t, appErr) + assert.Equal(t, 1, len(result.Order)) +} diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index 66590e9144e..8b490d0ec32 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -16138,6 +16138,28 @@ func (a *OpenTracingAppLayer) SetDefaultProfileImage(c request.CTX, user *model. return resultVar0 } +func (a *OpenTracingAppLayer) SetFileSearchableContent(fileID string, data string) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetFileSearchableContent") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.SetFileSearchableContent(fileID, data) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) SetPhase2PermissionsMigrationStatus(isComplete bool) error { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPhase2PermissionsMigrationStatus") diff --git a/server/channels/app/plugin_api.go b/server/channels/app/plugin_api.go index f069d47fdee..3c9cacb5858 100644 --- a/server/channels/app/plugin_api.go +++ b/server/channels/app/plugin_api.go @@ -766,6 +766,10 @@ func (api *PluginAPI) GetFileInfo(fileID string) (*model.FileInfo, *model.AppErr return api.app.GetFileInfo(fileID) } +func (api *PluginAPI) SetFileSearchableContent(fileID string, content string) *model.AppError { + return api.app.SetFileSearchableContent(fileID, content) +} + func (api *PluginAPI) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { return api.app.GetFileInfos(page, perPage, opt) } diff --git a/server/channels/app/post.go b/server/channels/app/post.go index 48651dc8e8a..edd837ec3bd 100644 --- a/server/channels/app/post.go +++ b/server/channels/app/post.go @@ -356,16 +356,6 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel // might be duplicating requests. a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, rpost.Id, PendingPostIDsCacheTTL) - // We make a copy of the post for the plugin hook to avoid a race condition, - // and to remove the non-GOB-encodable Metadata from it. - pluginPost := rpost.ForPlugin() - a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { - hooks.MessageHasBeenPosted(pluginContext, pluginPost) - return true - }, plugin.MessageHasBeenPostedID) - }) - if a.Metrics() != nil { a.Metrics().IncrementPostCreate() } @@ -380,6 +370,16 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } } + // We make a copy of the post for the plugin hook to avoid a race condition, + // and to remove the non-GOB-encodable Metadata from it. + pluginPost := rpost.ForPlugin() + a.Srv().Go(func() { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.MessageHasBeenPosted(pluginContext, pluginPost) + return true + }, plugin.MessageHasBeenPostedID) + }) + // Normally, we would let the API layer call PreparePostForClient, but we do it here since it also needs // to be done when we send the post over the websocket in handlePostEvents // PS: we don't want to include PostPriority from the db to avoid the replica lag, diff --git a/server/i18n/en.json b/server/i18n/en.json index 78a796ffe6f..c5e0e60fe9b 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -5247,6 +5247,10 @@ "id": "app.file_info.save.app_error", "translation": "Unable to save the file info." }, + { + "id": "app.file_info.set_searchable_content.app_error", + "translation": "Unable to set the searchable content of the file." + }, { "id": "app.group.crud_permission", "translation": "Unable to perform operation for that source type." diff --git a/server/public/plugin/api.go b/server/public/plugin/api.go index 5621943b4bc..f7c96cb2bb6 100644 --- a/server/public/plugin/api.go +++ b/server/public/plugin/api.go @@ -783,6 +783,12 @@ type API interface { // Minimum server version: 5.3 GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) + // SetFileSearchableContent update the File Info searchable text for full text search + // + // @tag File + // Minimum server version: 9.1 + SetFileSearchableContent(fileID string, content string) *model.AppError + // GetFileInfos gets File Infos with options // // @tag File diff --git a/server/public/plugin/api_timer_layer_generated.go b/server/public/plugin/api_timer_layer_generated.go index 063faa1ce2e..4376fbbab4e 100644 --- a/server/public/plugin/api_timer_layer_generated.go +++ b/server/public/plugin/api_timer_layer_generated.go @@ -852,6 +852,13 @@ func (api *apiTimerLayer) GetFileInfo(fileId string) (*model.FileInfo, *model.Ap return _returnsA, _returnsB } +func (api *apiTimerLayer) SetFileSearchableContent(fileID string, content string) *model.AppError { + startTime := timePkg.Now() + _returnsA := api.apiImpl.SetFileSearchableContent(fileID, content) + api.recordTime(startTime, "SetFileSearchableContent", _returnsA == nil) + return _returnsA +} + func (api *apiTimerLayer) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { startTime := timePkg.Now() _returnsA, _returnsB := api.apiImpl.GetFileInfos(page, perPage, opt) diff --git a/server/public/plugin/client_rpc_generated.go b/server/public/plugin/client_rpc_generated.go index 3e1e12a8235..8e589f49f91 100644 --- a/server/public/plugin/client_rpc_generated.go +++ b/server/public/plugin/client_rpc_generated.go @@ -4345,6 +4345,35 @@ func (s *apiRPCServer) GetFileInfo(args *Z_GetFileInfoArgs, returns *Z_GetFileIn return nil } +type Z_SetFileSearchableContentArgs struct { + A string + B string +} + +type Z_SetFileSearchableContentReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) SetFileSearchableContent(fileID string, content string) *model.AppError { + _args := &Z_SetFileSearchableContentArgs{fileID, content} + _returns := &Z_SetFileSearchableContentReturns{} + if err := g.client.Call("Plugin.SetFileSearchableContent", _args, _returns); err != nil { + log.Printf("RPC call to SetFileSearchableContent API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) SetFileSearchableContent(args *Z_SetFileSearchableContentArgs, returns *Z_SetFileSearchableContentReturns) error { + if hook, ok := s.impl.(interface { + SetFileSearchableContent(fileID string, content string) *model.AppError + }); ok { + returns.A = hook.SetFileSearchableContent(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API SetFileSearchableContent called but not implemented.")) + } + return nil +} + type Z_GetFileInfosArgs struct { A int B int diff --git a/server/public/plugin/plugintest/api.go b/server/public/plugin/plugintest/api.go index 5bb3f2f5ff2..c820ee969b5 100644 --- a/server/public/plugin/plugintest/api.go +++ b/server/public/plugin/plugintest/api.go @@ -3655,6 +3655,22 @@ func (_m *API) SendPushNotification(notification *model.PushNotification, userID return r0 } +// SetFileSearchableContent provides a mock function with given fields: fileID, content +func (_m *API) SetFileSearchableContent(fileID string, content string) *model.AppError { + ret := _m.Called(fileID, content) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string, string) *model.AppError); ok { + r0 = rf(fileID, content) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // SetProfileImage provides a mock function with given fields: userID, data func (_m *API) SetProfileImage(userID string, data []byte) *model.AppError { ret := _m.Called(userID, data) diff --git a/server/public/pluginapi/file.go b/server/public/pluginapi/file.go index d295eb414c3..5e33627e0e8 100644 --- a/server/public/pluginapi/file.go +++ b/server/public/pluginapi/file.go @@ -46,6 +46,14 @@ func (f *FileService) GetInfo(id string) (*model.FileInfo, error) { return info, normalizeAppErr(appErr) } +// SetSearchableContent update the File Info searchable text for full text search +// +// Minimum server version: 9.1 +func (f *FileService) SetSearchableContent(id string, content string) error { + appErr := f.api.SetFileSearchableContent(id, content) + return normalizeAppErr(appErr) +} + // GetLink gets the public link of a file by id. // // Minimum server version: 5.6