diff --git a/api4/apitestlib.go b/api4/apitestlib.go index eb60ffab94..ac0c6f4a3b 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -709,6 +709,33 @@ func (th *TestHelper) CreateMessagePost(message string) *model.Post { return th.CreateMessagePostWithClient(th.Client, th.BasicChannel, message) } +func (th *TestHelper) CreatePostWithFiles(files ...*model.FileInfo) *model.Post { + return th.CreatePostWithFilesWithClient(th.Client, th.BasicChannel, files...) +} + +func (th *TestHelper) CreatePostInChannelWithFiles(channel *model.Channel, files ...*model.FileInfo) *model.Post { + return th.CreatePostWithFilesWithClient(th.Client, channel, files...) +} + +func (th *TestHelper) CreatePostWithFilesWithClient(client *model.Client4, channel *model.Channel, files ...*model.FileInfo) *model.Post { + var fileIds model.StringArray + for i := range files { + fileIds = append(fileIds, files[i].Id) + } + + post := &model.Post{ + ChannelId: channel.Id, + Message: "message_" + model.NewId(), + FileIds: fileIds, + } + + rpost, _, err := client.CreatePost(post) + if err != nil { + panic(err) + } + return rpost +} + func (th *TestHelper) CreatePostWithClient(client *model.Client4, channel *model.Channel) *model.Post { id := model.NewId() diff --git a/api4/channel.go b/api4/channel.go index 1500465e2c..1f4bc4d031 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -642,11 +642,18 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { return } + filesCount, err := c.App.GetChannelFileCount(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + stats := model.ChannelStats{ ChannelId: c.Params.ChannelId, MemberCount: memberCount, GuestCount: guestCount, PinnedPostCount: pinnedPostCount, + FilesCount: filesCount, } if err := json.NewEncoder(w).Encode(stats); err != nil { mlog.Warn("Error while writing response", mlog.Err(err)) diff --git a/api4/channel_test.go b/api4/channel_test.go index 38c8a83891..3e103c42fe 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -22,6 +22,7 @@ import ( "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" "github.com/mattermost/mattermost-server/v6/store/storetest/mocks" + "github.com/mattermost/mattermost-server/v6/utils/testutils" ) func TestCreateChannel(t *testing.T) { @@ -2507,12 +2508,24 @@ func TestGetChannelStats(t *testing.T) { require.Equal(t, channel.Id, stats.ChannelId, "couldn't get extra info") require.Equal(t, int64(1), stats.MemberCount, "got incorrect member count") require.Equal(t, int64(0), stats.PinnedPostCount, "got incorrect pinned post count") + require.Equal(t, int64(0), stats.FilesCount, "got incorrect file count") th.CreatePinnedPostWithClient(th.Client, channel) stats, _, err = client.GetChannelStats(channel.Id, "") require.NoError(t, err) require.Equal(t, int64(1), stats.PinnedPostCount, "should have returned 1 pinned post count") + // create a post with a file + sent, err := testutils.ReadTestFile("test.png") + require.NoError(t, err) + fileResp, _, err := client.UploadFile(sent, channel.Id, "test.png") + require.NoError(t, err) + th.CreatePostInChannelWithFiles(channel, fileResp.FileInfos...) + // make sure the file count channel stats is updated + stats, _, err = client.GetChannelStats(channel.Id, "") + require.NoError(t, err) + require.Equal(t, int64(1), stats.FilesCount, "should have returned 1 file count") + _, resp, err := client.GetChannelStats("junk", "") require.Error(t, err) CheckBadRequestStatus(t, resp) diff --git a/app/app_iface.go b/app/app_iface.go index dd94e666d5..1beab9fa9e 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -561,6 +561,7 @@ type AppIface interface { GetChannelByName(channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) GetChannelByNameForTeamName(channelName, teamName string, includeDeleted bool) (*model.Channel, *model.AppError) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, *model.AppError) + GetChannelFileCount(channelID string) (int64, *model.AppError) GetChannelGuestCount(channelID string) (int64, *model.AppError) GetChannelMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, *model.AppError) GetChannelMemberCount(channelID string) (int64, *model.AppError) diff --git a/app/channel.go b/app/channel.go index c7bcf727ed..85505aaad1 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2009,6 +2009,15 @@ func (a *App) GetChannelMemberCount(channelID string) (int64, *model.AppError) { return count, nil } +func (a *App) GetChannelFileCount(channelID string) (int64, *model.AppError) { + count, err := a.Srv().Store.Channel().GetFileCount(channelID) + if err != nil { + return 0, model.NewAppError("SqlChannelStore.GetFileCount", "app.channel.get_file_count.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return count, nil +} + func (a *App) GetChannelGuestCount(channelID string) (int64, *model.AppError) { count, err := a.Srv().Store.Channel().GetGuestCount(channelID, true) if err != nil { diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 4f8a18bd4a..e41ebbf076 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -4890,6 +4890,28 @@ func (a *OpenTracingAppLayer) GetChannelCounts(teamID string, userID string) (*m return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetChannelFileCount(channelID string) (int64, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelFileCount") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetChannelFileCount(channelID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelGroupUsers") diff --git a/i18n/en.json b/i18n/en.json index 7efef76220..3ea21a708a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4591,6 +4591,10 @@ "id": "app.channel.get_deleted.missing.app_error", "translation": "No deleted channels exist." }, + { + "id": "app.channel.get_file_count.app_error", + "translation": "Unable to get the file count for the channel" + }, { "id": "app.channel.get_for_post.app_error", "translation": "Unable to get the channel for the given post." diff --git a/model/channel_stats.go b/model/channel_stats.go index 05ec3bd77a..96631c1135 100644 --- a/model/channel_stats.go +++ b/model/channel_stats.go @@ -8,6 +8,7 @@ type ChannelStats struct { MemberCount int64 `json:"member_count"` GuestCount int64 `json:"guest_count"` PinnedPostCount int64 `json:"pinnedpost_count"` + FilesCount int64 `json:"files_count"` } func (o *ChannelStats) MemberCount_() float64 { diff --git a/model/cluster_message.go b/model/cluster_message.go index 9db02ffcc0..90999ab03a 100644 --- a/model/cluster_message.go +++ b/model/cluster_message.go @@ -26,6 +26,7 @@ const ( ClusterEventInvalidateCacheForWebhooks ClusterEvent = "inv_webhooks" ClusterEventInvalidateCacheForEmojisById ClusterEvent = "inv_emojis_by_id" ClusterEventInvalidateCacheForEmojisIdByName ClusterEvent = "inv_emojis_id_by_name" + ClusterEventInvalidateCacheForChannelFileCount ClusterEvent = "inv_channel_file_count" ClusterEventInvalidateCacheForChannelPinnedpostsCounts ClusterEvent = "inv_channel_pinnedposts_counts" ClusterEventInvalidateCacheForChannelMemberCounts ClusterEvent = "inv_channel_member_counts" ClusterEventInvalidateCacheForLastPosts ClusterEvent = "inv_last_posts" diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 7cd9da28b6..86b2e00c65 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -1249,6 +1249,24 @@ func (s *OpenTracingLayerChannelStore) GetDeletedByName(team_id string, name str return result, err } +func (s *OpenTracingLayerChannelStore) GetFileCount(channelID string) (int64, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetFileCount") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetFileCount(channelID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GetForPost(postID string) (*model.Channel, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetForPost") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 0e24bd37fc..4925f706ee 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -1402,6 +1402,27 @@ func (s *RetryLayerChannelStore) GetDeletedByName(team_id string, name string) ( } +func (s *RetryLayerChannelStore) GetFileCount(channelID string) (int64, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetFileCount(channelID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) GetForPost(postID string) (*model.Channel, error) { tries := 0 diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 9b929479fa..5ee7d58948 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -2188,6 +2188,25 @@ func (s SqlChannelStore) GetMemberCountFromCache(channelId string) int64 { return count } +func (s SqlChannelStore) GetFileCount(channelId string) (int64, error) { + var count int64 + err := s.GetReplicaX().Get(&count, ` + SELECT + COUNT(*) + FROM + FileInfo + LEFT JOIN Posts as P ON FileInfo.PostId=P.Id + LEFT JOIN Channels as C on C.Id=P.ChannelId + WHERE + FileInfo.DeleteAt = 0 + AND C.Id = ?`, channelId) + if err != nil { + return 0, errors.Wrapf(err, "failed to count files with channelId=%s", channelId) + } + + return count, nil +} + //nolint:unparam func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, error) { var count int64 diff --git a/store/store.go b/store/store.go index 632f7c4d1b..421496aeac 100644 --- a/store/store.go +++ b/store/store.go @@ -213,6 +213,7 @@ type ChannelStore interface { GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) InvalidateMemberCount(channelID string) GetMemberCountFromCache(channelID string) int64 + GetFileCount(channelID string) (int64, error) GetMemberCount(channelID string, allowFromCache bool) (int64, error) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) InvalidatePinnedPostCount(channelID string) diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 8bdd402f35..04d9e41828 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -881,6 +881,27 @@ func (_m *ChannelStore) GetDeletedByName(team_id string, name string) (*model.Ch return r0, r1 } +// GetFileCount provides a mock function with given fields: channelID +func (_m *ChannelStore) GetFileCount(channelID string) (int64, error) { + ret := _m.Called(channelID) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string) int64); ok { + r0 = rf(channelID) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(channelID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetForPost provides a mock function with given fields: postID func (_m *ChannelStore) GetForPost(postID string) (*model.Channel, error) { ret := _m.Called(postID) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index fff076ae9f..4789dc0161 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -1157,6 +1157,22 @@ func (s *TimerLayerChannelStore) GetDeletedByName(team_id string, name string) ( return result, err } +func (s *TimerLayerChannelStore) GetFileCount(channelID string) (int64, error) { + start := timemodule.Now() + + result, err := s.ChannelStore.GetFileCount(channelID) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetFileCount", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GetForPost(postID string) (*model.Channel, error) { start := timemodule.Now()