mirror of
https://github.com/mattermost/focalboard.git
synced 2024-11-30 08:36:54 +02:00
implement data retention for boards (#2588)
* implement data retention for boards * fix lint errors * start of orphans * add and update tests * fixes for merge * fix tests * fix lint * reset from testing * update setting unit test variable Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
21667aa8d0
commit
0489de8bd3
@ -214,6 +214,7 @@ detach: setup-attach
|
||||
|
||||
## Runs any lints and unit tests defined for the server and webapp, if they exist.
|
||||
.PHONY: test
|
||||
test: export FB_UNIT_TESTING=1
|
||||
test: webapp/node_modules
|
||||
ifneq ($(HAS_SERVER),)
|
||||
$(GO) test -v $(GO_TEST_FLAGS) ./server/...
|
||||
|
@ -3,8 +3,10 @@ module github.com/mattermost/focalboard/mattermost-plugin
|
||||
go 1.16
|
||||
|
||||
replace github.com/mattermost/focalboard/server => ../server
|
||||
replace github.com/mattermost/focalboard/server/server => ../server/server
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/mattermost/focalboard/server v0.0.0-20220325164658-33557093b00d
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.27
|
||||
github.com/mattermost/mattermost-server/v6 v6.5.0
|
||||
|
@ -88,6 +88,11 @@ func (p *Plugin) OnConfigurationChange() error { //nolint
|
||||
|
||||
// handle feature flags
|
||||
p.server.Config().FeatureFlags = parseFeatureFlags(mmconfig.FeatureFlags.ToMap())
|
||||
|
||||
// handle Data Retention settings
|
||||
p.server.Config().EnableDataRetention = *mmconfig.DataRetentionSettings.EnableBoardsDeletion
|
||||
p.server.Config().DataRetentionDays = *mmconfig.DataRetentionSettings.BoardsRetentionDays
|
||||
|
||||
p.server.UpdateAppConfig()
|
||||
p.wsPluginAdapter.BroadcastConfigChange(*p.server.App().GetClientConfig())
|
||||
return nil
|
||||
|
@ -1,11 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
serverModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin/plugintest"
|
||||
@ -13,27 +14,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
Server *server.Server
|
||||
}
|
||||
|
||||
func SetupTestHelper() *TestHelper {
|
||||
th := &TestHelper{}
|
||||
th.Server = newTestServer()
|
||||
return th
|
||||
}
|
||||
|
||||
func newTestServer() *server.Server {
|
||||
srv, err := server.New(server.Params{
|
||||
Cfg: &config.Configuration{},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func TestConfigurationNullConfiguration(t *testing.T) {
|
||||
plugin := &Plugin{}
|
||||
assert.NotNil(t, plugin.getConfiguration())
|
||||
@ -53,16 +33,24 @@ func TestOnConfigurationChange(t *testing.T) {
|
||||
Directory: &stringRef,
|
||||
Plugins: basePlugins,
|
||||
}
|
||||
falseRef := false
|
||||
intRef := 365
|
||||
baseDataRetentionSettings := &serverModel.DataRetentionSettings{
|
||||
EnableBoardsDeletion: &falseRef,
|
||||
BoardsRetentionDays: &intRef,
|
||||
}
|
||||
|
||||
baseConfig := &serverModel.Config{
|
||||
FeatureFlags: baseFeatureFlags,
|
||||
PluginSettings: *basePluginSettings,
|
||||
FeatureFlags: baseFeatureFlags,
|
||||
PluginSettings: *basePluginSettings,
|
||||
DataRetentionSettings: *baseDataRetentionSettings,
|
||||
}
|
||||
|
||||
t.Run("Test Load Plugin Success", func(t *testing.T) {
|
||||
th := SetupTestHelper()
|
||||
th := SetupTestHelper(t)
|
||||
api := &plugintest.API{}
|
||||
api.On("GetUnsanitizedConfig").Return(baseConfig)
|
||||
api.On("GetConfig").Return(baseConfig)
|
||||
|
||||
p := Plugin{}
|
||||
p.SetAPI(api)
|
||||
|
68
mattermost-plugin/server/helpers_test.go
Normal file
68
mattermost-plugin/server/helpers_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||
"github.com/mattermost/focalboard/server/services/store/mockstore"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
Server *server.Server
|
||||
Store *mockstore.MockStore
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) *TestHelper {
|
||||
th := &TestHelper{}
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockStore := mockstore.NewMockStore(ctrl)
|
||||
|
||||
th.Server = newTestServer(t, mockStore)
|
||||
th.Store = mockStore
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, mockStore *mockstore.MockStore) *server.Server {
|
||||
config := &config.Configuration{
|
||||
EnableDataRetention: false,
|
||||
DataRetentionDays: 10,
|
||||
FilesDriver: "local",
|
||||
FilesPath: "./files",
|
||||
WebPath: "/",
|
||||
}
|
||||
|
||||
logger, err := mlog.NewLogger()
|
||||
if err = logger.Configure("", config.LoggingCfgJSON, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mockStore.EXPECT().GetTeam(gomock.Any()).Return(nil, nil).AnyTimes()
|
||||
mockStore.EXPECT().UpsertTeamSignupToken(gomock.Any()).AnyTimes()
|
||||
mockStore.EXPECT().GetSystemSettings().AnyTimes()
|
||||
mockStore.EXPECT().SetSystemSetting(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
permissionsService := localpermissions.New(mockStore, logger)
|
||||
|
||||
srv, err := server.New(server.Params{
|
||||
Cfg: config,
|
||||
DBStore: mockStore,
|
||||
Logger: logger,
|
||||
PermissionsService: permissionsService,
|
||||
SkipTemplateInit: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
@ -38,6 +40,8 @@ const (
|
||||
notifyFreqBoardSecondsKey = "notify_freq_board_seconds"
|
||||
)
|
||||
|
||||
var ErrInsufficientLicense = errors.New("appropriate license required")
|
||||
|
||||
type BoardsEmbed struct {
|
||||
OriginalPath string `json:"originalPath"`
|
||||
TeamID string `json:"teamID"`
|
||||
@ -236,6 +240,8 @@ func (p *Plugin) createBoardsConfig(mmconfig mmModel.Config, baseURL string, ser
|
||||
FeatureFlags: featureFlags,
|
||||
NotifyFreqCardSeconds: getPluginSettingInt(mmconfig, notifyFreqCardSecondsKey, 120),
|
||||
NotifyFreqBoardSeconds: getPluginSettingInt(mmconfig, notifyFreqBoardSecondsKey, 86400),
|
||||
EnableDataRetention: *mmconfig.DataRetentionSettings.EnableBoardsDeletion,
|
||||
DataRetentionDays: *mmconfig.DataRetentionSettings.BoardsRetentionDays,
|
||||
}
|
||||
}
|
||||
|
||||
@ -502,3 +508,24 @@ func isBoardsLink(link string) bool {
|
||||
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
|
||||
}
|
||||
|
||||
func (p *Plugin) RunDataRetention(nowTime, batchSize int64) (int64, error) {
|
||||
p.server.Logger().Debug("Boards RunDataRetention")
|
||||
license := p.server.Store().GetLicense()
|
||||
if license == nil || !(*license.Features.DataRetention) {
|
||||
return 0, ErrInsufficientLicense
|
||||
}
|
||||
|
||||
if p.server.Config().EnableDataRetention {
|
||||
boardsRetentionDays := p.server.Config().DataRetentionDays
|
||||
endTimeBoards := convertDaysToCutoff(boardsRetentionDays, time.Unix(nowTime/1000, 0))
|
||||
return p.server.Store().RunDataRetention(endTimeBoards, batchSize)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func convertDaysToCutoff(days int, now time.Time) int64 {
|
||||
upToStartOfDay := now.AddDate(0, 0, -days)
|
||||
cutoffDate := time.Date(upToStartOfDay.Year(), upToStartOfDay.Month(), upToStartOfDay.Day(), 0, 0, 0, 0, time.Local)
|
||||
return cutoffDate.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
@ -1,21 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// A very simple health check.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// In the future we could report back on the status of our DB, or our cache
|
||||
// (e.g. Redis) by performing a simple PING, and include them in the response.
|
||||
io.WriteString(w, "Hello, world!")
|
||||
}
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
th := SetupTestHelper(t)
|
||||
plugin := Plugin{}
|
||||
|
||||
testHandler := http.HandlerFunc(testHandler)
|
||||
th.Server.GetRootRouter().Handle("/", testHandler)
|
||||
|
||||
plugin.server = th.Server
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
// plugin.server.
|
||||
plugin.ServeHTTP(nil, w, r)
|
||||
|
||||
result := w.Result()
|
||||
@ -31,6 +54,7 @@ func TestServeHTTP(t *testing.T) {
|
||||
func TestSetConfiguration(t *testing.T) {
|
||||
plugin := Plugin{}
|
||||
boolTrue := true
|
||||
boolFalse := false
|
||||
stringRef := ""
|
||||
|
||||
baseFeatureFlags := &model.FeatureFlags{}
|
||||
@ -45,16 +69,25 @@ func TestSetConfiguration(t *testing.T) {
|
||||
}
|
||||
|
||||
directory := "testDirectory"
|
||||
maxFileSize := int64(1048576000)
|
||||
baseFileSettings := &model.FileSettings{
|
||||
DriverName: &driverName,
|
||||
Directory: &directory,
|
||||
DriverName: &driverName,
|
||||
Directory: &directory,
|
||||
MaxFileSize: &maxFileSize,
|
||||
}
|
||||
|
||||
days := 365
|
||||
baseDataRetentionSettings := &model.DataRetentionSettings{
|
||||
EnableBoardsDeletion: &boolFalse,
|
||||
BoardsRetentionDays: &days,
|
||||
}
|
||||
|
||||
baseConfig := &model.Config{
|
||||
FeatureFlags: baseFeatureFlags,
|
||||
PluginSettings: *basePluginSettings,
|
||||
SqlSettings: *baseSQLSettings,
|
||||
FileSettings: *baseFileSettings,
|
||||
FeatureFlags: baseFeatureFlags,
|
||||
PluginSettings: *basePluginSettings,
|
||||
SqlSettings: *baseSQLSettings,
|
||||
FileSettings: *baseFileSettings,
|
||||
DataRetentionSettings: *baseDataRetentionSettings,
|
||||
}
|
||||
|
||||
t.Run("test enable telemetry", func(t *testing.T) {
|
||||
@ -96,3 +129,65 @@ func TestSetConfiguration(t *testing.T) {
|
||||
assert.Equal(t, "true", config.FeatureFlags["myTest"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunDataRetention(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
plugin := Plugin{}
|
||||
plugin.server = th.Server
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
t.Run("test null license", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetLicense().Return(nil)
|
||||
_, err := plugin.RunDataRetention(now, 10)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, ErrInsufficientLicense, err)
|
||||
})
|
||||
|
||||
t.Run("test invalid license", func(t *testing.T) {
|
||||
falseValue := false
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
DataRetention: &falseValue,
|
||||
},
|
||||
},
|
||||
)
|
||||
_, err := plugin.RunDataRetention(now, 10)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, ErrInsufficientLicense, err)
|
||||
})
|
||||
|
||||
t.Run("test valid license, invalid config", func(t *testing.T) {
|
||||
trueValue := true
|
||||
th.Store.EXPECT().GetLicense().Return(
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
DataRetention: &trueValue,
|
||||
},
|
||||
})
|
||||
|
||||
count, err := plugin.RunDataRetention(now, 10)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), count)
|
||||
})
|
||||
|
||||
t.Run("test valid license, valid config", func(t *testing.T) {
|
||||
trueValue := true
|
||||
th.Store.EXPECT().GetLicense().Return(
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
DataRetention: &trueValue,
|
||||
},
|
||||
})
|
||||
|
||||
th.Store.EXPECT().RunDataRetention(gomock.Any(), int64(10)).Return(int64(100), nil)
|
||||
plugin.server.Config().EnableDataRetention = true
|
||||
|
||||
count, err := plugin.RunDataRetention(now, 10)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(100), count)
|
||||
})
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ type Params struct {
|
||||
WSAdapter ws.Adapter
|
||||
NotifyBackends []notify.Backend
|
||||
PermissionsService permissions.PermissionsService
|
||||
SkipTemplateInit bool
|
||||
}
|
||||
|
||||
func (p Params) CheckValid() error {
|
||||
|
@ -357,6 +357,10 @@ func (s *Server) App() *app.App {
|
||||
return s.app
|
||||
}
|
||||
|
||||
func (s *Server) Store() store.Store {
|
||||
return s.store
|
||||
}
|
||||
|
||||
func (s *Server) UpdateAppConfig() {
|
||||
s.app.SetConfig(s.config)
|
||||
}
|
||||
|
@ -50,6 +50,8 @@ type Configuration struct {
|
||||
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
|
||||
EnablePublicSharedBoards bool `json:"enablePublicSharedBoards" mapstructure:"enablePublicSharedBoards"`
|
||||
FeatureFlags map[string]string `json:"featureFlags" mapstructure:"featureFlags"`
|
||||
EnableDataRetention bool `json:"enable_data_retention" mapstructure:"enable_data_retention"`
|
||||
DataRetentionDays int `json:"data_retention_days" mapstructure:"data_retention_days"`
|
||||
|
||||
AuthMode string `json:"authMode" mapstructure:"authMode"`
|
||||
|
||||
@ -95,6 +97,8 @@ func ReadConfigFile(configFilePath string) (*Configuration, error) {
|
||||
viper.SetDefault("AuthMode", "native")
|
||||
viper.SetDefault("NotifyFreqCardSeconds", 120) // 2 minutes after last card edit
|
||||
viper.SetDefault("NotifyFreqBoardSeconds", 86400) // 1 day after last card edit
|
||||
viper.SetDefault("EnableDataRetention", false)
|
||||
viper.SetDefault("DataRetentionDays", 365) // 1 year is default
|
||||
viper.SetDefault("PrometheusAddress", "")
|
||||
|
||||
err := viper.ReadInConfig() // Find and read the config file
|
||||
|
@ -1128,6 +1128,21 @@ func (mr *MockStoreMockRecorder) RemoveDefaultTemplates(arg0 interface{}) *gomoc
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveDefaultTemplates", reflect.TypeOf((*MockStore)(nil).RemoveDefaultTemplates), arg0)
|
||||
}
|
||||
|
||||
// RunDataRetention mocks base method.
|
||||
func (m *MockStore) RunDataRetention(arg0, arg1 int64) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RunDataRetention", arg0, arg1)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RunDataRetention indicates an expected call of RunDataRetention.
|
||||
func (mr *MockStoreMockRecorder) RunDataRetention(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunDataRetention", reflect.TypeOf((*MockStore)(nil).RunDataRetention), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveMember mocks base method.
|
||||
func (m *MockStore) SaveMember(arg0 *model.BoardMember) (*model.BoardMember, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -4,9 +4,13 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
_ "github.com/lib/pq" // postgres driver
|
||||
@ -744,6 +748,113 @@ func (s *SQLStore) replaceBlockID(db sq.BaseRunner, currentID, newID, workspaceI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) runDataRetention(db sq.BaseRunner, globalRetentionDate int64, batchSize int64) (int64, error) {
|
||||
mlog.Info("Start Boards Data Retention",
|
||||
mlog.String("Global Retention Date", time.Unix(globalRetentionDate/1000, 0).String()),
|
||||
mlog.Int64("Raw Date", globalRetentionDate))
|
||||
deleteTables := map[string]string{
|
||||
"blocks": "board_id",
|
||||
"blocks_history": "board_id",
|
||||
"boards": "id",
|
||||
"boards_history": "id",
|
||||
"board_members": "board_id",
|
||||
"sharing": "id",
|
||||
}
|
||||
|
||||
subBuilder := s.getQueryBuilder(db).
|
||||
Select("board_id, MAX(update_at) AS maxDate").
|
||||
From(s.tablePrefix + "blocks").
|
||||
GroupBy("board_id")
|
||||
|
||||
subQuery, _, _ := subBuilder.ToSql()
|
||||
|
||||
builder := s.getQueryBuilder(db).
|
||||
Select("id").
|
||||
From(s.tablePrefix + "boards").
|
||||
LeftJoin("( " + subQuery + " ) As subquery ON (subquery.board_id = id)").
|
||||
Where(sq.Lt{"maxDate": globalRetentionDate}).
|
||||
Where(sq.NotEq{"team_id": "0"}).
|
||||
Where(sq.Eq{"is_template": false})
|
||||
|
||||
rows, err := builder.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`dataRetention subquery ERROR`, mlog.Err(err))
|
||||
return 0, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
deleteIds, err := idsFromRows(rows)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
totalAffected := 0
|
||||
if len(deleteIds) > 0 {
|
||||
mlog.Debug("Data Retention DeleteIDs " + strings.Join(deleteIds, ", "))
|
||||
for table, field := range deleteTables {
|
||||
affected, err := s.genericRetentionPoliciesDeletion(db, table, field, deleteIds, batchSize)
|
||||
if err != nil {
|
||||
return int64(totalAffected), err
|
||||
}
|
||||
totalAffected += int(affected)
|
||||
}
|
||||
}
|
||||
mlog.Info("Complete Boards Data Retention", mlog.Int("total deletion ids", len(deleteIds)), mlog.Int("TotalAffected", totalAffected))
|
||||
return int64(totalAffected), nil
|
||||
}
|
||||
|
||||
func idsFromRows(rows *sql.Rows) ([]string, error) {
|
||||
deleteIds := []string{}
|
||||
for rows.Next() {
|
||||
var boardID string
|
||||
err := rows.Scan(
|
||||
&boardID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deleteIds = append(deleteIds, boardID)
|
||||
}
|
||||
return deleteIds, nil
|
||||
}
|
||||
|
||||
// genericRetentionPoliciesDeletion actually executes the DELETE query using a sq.SelectBuilder
|
||||
// which selects the rows to delete.
|
||||
func (s *SQLStore) genericRetentionPoliciesDeletion(
|
||||
db sq.BaseRunner,
|
||||
table string,
|
||||
deleteColumn string,
|
||||
deleteIds []string,
|
||||
batchSize int64,
|
||||
) (int64, error) {
|
||||
whereClause := deleteColumn + ` IN ('` + strings.Join(deleteIds, `','`) + `')`
|
||||
deleteQuery := s.getQueryBuilder(db).
|
||||
Delete(s.tablePrefix + table).
|
||||
Where(whereClause)
|
||||
s1, _, _ := deleteQuery.ToSql()
|
||||
mlog.Debug(s1)
|
||||
|
||||
var totalRowsAffected int64
|
||||
var batchRowsAffected int64
|
||||
for {
|
||||
result, err := deleteQuery.Exec()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to delete "+table)
|
||||
}
|
||||
|
||||
batchRowsAffected, err = result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to get rows affected for "+table)
|
||||
}
|
||||
totalRowsAffected += batchRowsAffected
|
||||
if batchRowsAffected != batchSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
mlog.Debug("Rows Affected" + strconv.FormatInt(totalRowsAffected, 10))
|
||||
return totalRowsAffected, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) duplicateBlock(db sq.BaseRunner, boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) {
|
||||
blocks, err := s.getSubTree2(db, boardID, blockID, model.QuerySubtreeOptions{})
|
||||
if err != nil {
|
||||
|
@ -18,6 +18,7 @@ type Params struct {
|
||||
IsPlugin bool
|
||||
NewMutexFn MutexFactory
|
||||
PluginAPI *plugin.API
|
||||
SkipTemplateInit bool
|
||||
}
|
||||
|
||||
func (p Params) CheckValid() error {
|
||||
|
@ -648,6 +648,30 @@ func (s *SQLStore) RemoveDefaultTemplates(boards []*model.Board) error {
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error) {
|
||||
if s.dbType == model.SqliteDBType {
|
||||
return s.runDataRetention(s.db, globalRetentionDate, batchSize)
|
||||
}
|
||||
tx, txErr := s.db.BeginTx(context.Background(), nil)
|
||||
if txErr != nil {
|
||||
return 0, txErr
|
||||
}
|
||||
result, err := s.runDataRetention(tx, globalRetentionDate, batchSize)
|
||||
if err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "RunDataRetention"))
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error) {
|
||||
return s.saveMember(s.db, bm)
|
||||
|
||||
|
@ -134,6 +134,9 @@ type Store interface {
|
||||
RemoveDefaultTemplates(boards []*model.Board) error
|
||||
GetTemplateBoards(teamID, userID string) ([]*model.Board, error)
|
||||
|
||||
// @withTransaction
|
||||
RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error)
|
||||
|
||||
DBType() string
|
||||
|
||||
IsErrNotFound(err error) bool
|
||||
|
@ -69,6 +69,11 @@ func StoreTestBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, f
|
||||
defer tearDown()
|
||||
testDuplicateBlock(t, store)
|
||||
})
|
||||
t.Run("RunDataRetention", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testRunDataRetention(t, store)
|
||||
})
|
||||
t.Run("GetBlockMetadata", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
@ -795,6 +800,56 @@ func testGetBlock(t *testing.T, store store.Store) {
|
||||
})
|
||||
}
|
||||
|
||||
func testRunDataRetention(t *testing.T, store store.Store) {
|
||||
validBoard := model.Board{
|
||||
ID: "board-id-test",
|
||||
IsTemplate: false,
|
||||
ModifiedBy: "user-id-1",
|
||||
TeamID: "team-id",
|
||||
}
|
||||
board, err := store.InsertBoard(&validBoard, "user-id-1")
|
||||
require.NoError(t, err)
|
||||
|
||||
validBlock := model.Block{
|
||||
ID: "id-test",
|
||||
BoardID: board.ID,
|
||||
ModifiedBy: "user-id-1",
|
||||
}
|
||||
|
||||
validBlock2 := model.Block{
|
||||
ID: "id-test2",
|
||||
BoardID: board.ID,
|
||||
ModifiedBy: "user-id-1",
|
||||
}
|
||||
|
||||
newBlocks := []model.Block{validBlock, validBlock2}
|
||||
|
||||
err = store.InsertBlocks(newBlocks, "user-id-1")
|
||||
require.NoError(t, err)
|
||||
|
||||
blocks, err := store.GetBlocksWithBoardID(board.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, len(newBlocks))
|
||||
initialCount := len(blocks)
|
||||
|
||||
t.Run("test no deletions", func(t *testing.T) {
|
||||
deletions, err := store.RunDataRetention(utils.GetMillisForTime(time.Now().Add(-time.Hour*1)), 10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), deletions)
|
||||
})
|
||||
|
||||
t.Run("test all deletions", func(t *testing.T) {
|
||||
deletions, err := store.RunDataRetention(utils.GetMillisForTime(time.Now().Add(time.Hour*1)), 10)
|
||||
require.NoError(t, err)
|
||||
require.True(t, deletions > int64(initialCount))
|
||||
|
||||
// expect all blocks to be deleted.
|
||||
blocks, errBlocks := store.GetBlocksWithBoardID(board.ID)
|
||||
require.NoError(t, errBlocks)
|
||||
require.Equal(t, 0, len(blocks))
|
||||
})
|
||||
}
|
||||
|
||||
func testDuplicateBlock(t *testing.T, store store.Store) {
|
||||
InsertBlocks(t, store, subtreeSampleBlocks, "user-id-1")
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
Loading…
Reference in New Issue
Block a user