Skip to content

Commit c16b32a

Browse files
committed
feat(backup)_: local user data backups
Fixes #18248 #18107 #18120 #18122 #18141 #18121 Implements all the backend for the local user data backups. Uses the scaffold @osmaczko created to make it more extendable. Instead of using the old Messenger code for backups, we now use a new controller that let's other modules register to be backed up. This includes the messenger for chats, contacts and communities, the accounts service for settings and the wallet service for watch-only accounts. I also created new Protobufs for those backups, so that we no longer user the super generic Waku Backup protobuf. Finally, I added some integration tests and a functional test that does the whole flow. A lot of cleanups can still be done to reduce duplication, but they can be done in another commit/issue, since the internal of the services can be modified without the backup process being affected, since the protobufs are in place already.
1 parent 10a9200 commit c16b32a

40 files changed

+1755
-47
lines changed

api/geth_backend.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,10 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
836836
}
837837
}
838838

839+
if err = b.statusNode.StartLocalBackup(); err != nil {
840+
return err
841+
}
842+
839843
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
840844
if err != nil {
841845
b.logger.Error("failed to update account")
@@ -949,6 +953,10 @@ func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, pass
949953
}
950954
}
951955

956+
if err = b.statusNode.StartLocalBackup(); err != nil {
957+
return err
958+
}
959+
952960
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
953961
if err != nil {
954962
b.logger.Info("failed to update account")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE settings ADD COLUMN backup_path VARCHAR DEFAULT '';

mobile/status.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2401,3 +2401,37 @@ func IntendedPanic(message string) string {
24012401
panic(err)
24022402
})
24032403
}
2404+
2405+
func performLocalBackup() string {
2406+
filePath, err := statusBackend.StatusNode().PerformLocalBackup()
2407+
if err != nil {
2408+
return makeJSONResponse(err)
2409+
}
2410+
2411+
respJSON, err := json.Marshal(map[string]interface{}{
2412+
"filePath": filePath,
2413+
})
2414+
if err != nil {
2415+
return makeJSONResponse(err)
2416+
}
2417+
2418+
return string(respJSON)
2419+
}
2420+
2421+
func PerformLocalBackup() string {
2422+
return callWithResponse(performLocalBackup)
2423+
}
2424+
2425+
func LoadLocalBackup(requestJSON string) string {
2426+
var request requests.LoadLocalBackup
2427+
err := json.Unmarshal([]byte(requestJSON), &request)
2428+
if err != nil {
2429+
return makeJSONResponse(err)
2430+
}
2431+
err = request.Validate()
2432+
if err != nil {
2433+
return makeJSONResponse(err)
2434+
}
2435+
err = statusBackend.StatusNode().LoadLocalBackup(request.FilePath)
2436+
return makeJSONResponse(err)
2437+
}

multiaccounts/settings/columns.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ var (
3434
dBColumnName: "backup_fetched",
3535
valueHandler: BoolHandler,
3636
}
37+
BackupPath = SettingField{
38+
reactFieldName: "backup-path",
39+
dBColumnName: "backup_path",
40+
}
3741
ChaosMode = SettingField{
3842
reactFieldName: "chaos-mode?",
3943
dBColumnName: "chaos_mode",
@@ -548,6 +552,7 @@ var (
548552
AutoMessageEnabled,
549553
BackupEnabled,
550554
BackupFetched,
555+
BackupPath,
551556
Bio,
552557
ChaosMode,
553558
CollectibleGroupByCollection,

multiaccounts/settings/database.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import (
44
"context"
55
"database/sql"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"sync"
910
"time"
1011

1112
"github.com/status-im/status-go/common/dbsetup"
1213
"github.com/status-im/status-go/eth-node/types"
1314
"github.com/status-im/status-go/logutils"
14-
"github.com/status-im/status-go/multiaccounts/errors"
15+
maErrors "github.com/status-im/status-go/multiaccounts/errors"
1516
"github.com/status-im/status-go/nodecfg"
1617
"github.com/status-im/status-go/params"
1718
"github.com/status-im/status-go/sqlite"
@@ -213,7 +214,7 @@ func (db *Database) getSettingFieldFromReactName(reactName string) (SettingField
213214
return s, nil
214215
}
215216
}
216-
return SettingField{}, errors.ErrInvalidConfig
217+
return SettingField{}, maErrors.ErrInvalidConfig
217218
}
218219

219220
func (db *Database) makeSelectRow(setting SettingField) *sql.Row {
@@ -243,12 +244,21 @@ func (db *Database) saveSetting(setting SettingField, value interface{}) error {
243244
return err
244245
}
245246

246-
_, err = update.Exec(value)
247+
result, err := update.Exec(value)
248+
if err != nil {
249+
return err
250+
}
247251

252+
rowsAffected, err := result.RowsAffected()
248253
if err != nil {
249254
return err
250255
}
251256

257+
if rowsAffected == 0 {
258+
// If no rows were affected, it means the setting does not exist
259+
return errors.New("settings not initialized, please call CreateSettings first")
260+
}
261+
252262
if db.notifier != nil {
253263
db.notifier(setting, value)
254264
}
@@ -334,7 +344,7 @@ func (db *Database) SaveSyncSetting(setting SettingField, value interface{}, clo
334344
return err
335345
}
336346
if clock <= ls {
337-
return errors.ErrNewClockOlderThanCurrent
347+
return maErrors.ErrNewClockOlderThanCurrent
338348
}
339349

340350
err = db.SetSettingLastSynced(setting, clock)
@@ -398,7 +408,7 @@ func (db *Database) GetSettings() (Settings, error) {
398408
test_networks_enabled, mutual_contact_enabled, profile_migration_needed, wallet_token_preferences_group_by_community, url_unfurling_mode,
399409
mnemonic_was_not_shown, wallet_show_community_asset_when_sending_tokens, wallet_display_assets_below_balance,
400410
wallet_display_assets_below_balance_threshold, wallet_collectible_preferences_group_by_collection, wallet_collectible_preferences_group_by_community,
401-
peer_syncing_enabled, auto_refresh_tokens_enabled, last_tokens_update, news_feed_enabled, news_feed_last_fetched_timestamp, news_rss_enabled
411+
peer_syncing_enabled, auto_refresh_tokens_enabled, last_tokens_update, news_feed_enabled, news_feed_last_fetched_timestamp, news_rss_enabled, backup_path
402412
FROM
403413
settings
404414
WHERE
@@ -487,6 +497,7 @@ func (db *Database) GetSettings() (Settings, error) {
487497
&s.NewsFeedEnabled,
488498
&newsFeedLastFetchedTimestamp,
489499
&s.NewsRSSEnabled,
500+
&s.BackupPath,
490501
)
491502

492503
if err != nil {
@@ -928,3 +939,11 @@ func (db *Database) NewsRSSEnabled() (result bool, err error) {
928939
}
929940
return result, err
930941
}
942+
943+
func (db *Database) BackupPath() (result string, err error) {
944+
err = db.makeSelectRow(BackupPath).Scan(&result)
945+
if err == sql.ErrNoRows {
946+
return result, nil
947+
}
948+
return result, err
949+
}

multiaccounts/settings/database_settings_manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type DatabaseSettingsManager interface {
6363
CanSyncOnMobileNetwork() (result bool, err error)
6464
ShouldBroadcastUserStatus() (result bool, err error)
6565
BackupEnabled() (result bool, err error)
66+
BackupPath() (result string, err error)
6667
AutoMessageEnabled() (result bool, err error)
6768
LastBackup() (result uint64, err error)
6869
BackupFetched() (result bool, err error)

multiaccounts/settings/database_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,23 @@ func TestDatabase_NewsRSSEnabled(t *testing.T) {
296296
require.NoError(t, err)
297297
require.Equal(t, false, settings.NewsRSSEnabled)
298298
}
299+
300+
func TestDatabase_BackupPath(t *testing.T) {
301+
db, stop := setupTestDB(t)
302+
defer stop()
303+
304+
require.NoError(t, db.CreateSettings(settings, config))
305+
306+
path, err := db.BackupPath()
307+
require.NoError(t, err)
308+
// The default backup path is empty
309+
require.Equal(t, "", path)
310+
311+
testPath := "/path/to/backup"
312+
err = db.SaveSetting(BackupPath.GetReactName(), testPath)
313+
require.NoError(t, err)
314+
315+
settings, err = db.GetSettings()
316+
require.NoError(t, err)
317+
require.Equal(t, testPath, settings.BackupPath)
318+
}

multiaccounts/settings/structs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ type Settings struct {
215215
LastBackup uint64 `json:"last-backup,omitempty"`
216216
BackupEnabled bool `json:"backup-enabled?,omitempty"`
217217
BackupFetched bool `json:"backup-fetched?,omitempty"`
218+
BackupPath string `json:"backup-path,omitempty"`
218219
AutoMessageEnabled bool `json:"auto-message-enabled?,omitempty"`
219220
GifAPIKey string `json:"gifs/api-key"`
220221
TestNetworksEnabled bool `json:"test-networks-enabled?,omitempty"`

node/backup/controller.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package backup
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"sync"
8+
"time"
9+
10+
"github.com/status-im/status-go/common"
11+
)
12+
13+
type BackupConfig struct {
14+
PrivateKey []byte
15+
FileNameGetter func() (string, error)
16+
BackupEnabled bool
17+
Interval time.Duration
18+
}
19+
20+
type Controller struct {
21+
config BackupConfig
22+
core *core
23+
quit chan struct{}
24+
mutex sync.Mutex
25+
}
26+
27+
func NewController(config BackupConfig) (*Controller, error) {
28+
if len(config.PrivateKey) == 0 {
29+
return nil, fmt.Errorf("private key must be provided")
30+
}
31+
if config.FileNameGetter == nil {
32+
return nil, fmt.Errorf("filename getter must be provided")
33+
}
34+
35+
return &Controller{
36+
config: config,
37+
core: newCore(),
38+
quit: make(chan struct{}),
39+
}, nil
40+
}
41+
42+
func (c *Controller) Register(componentName string, dumpFunc func() ([]byte, error), loadFunc func([]byte) error) {
43+
c.mutex.Lock()
44+
defer c.mutex.Unlock()
45+
46+
c.core.Register(componentName, dumpFunc, loadFunc)
47+
}
48+
49+
func (c *Controller) Start() {
50+
if !c.config.BackupEnabled {
51+
return
52+
}
53+
54+
go func() {
55+
defer common.LogOnPanic()
56+
ticker := time.NewTicker(c.config.Interval)
57+
defer ticker.Stop()
58+
for {
59+
select {
60+
case <-ticker.C:
61+
_, err := c.PerformBackup()
62+
if err != nil {
63+
// TODDO use a logger
64+
fmt.Printf("Error performing backup: %v\n", err)
65+
}
66+
case <-c.quit:
67+
return
68+
}
69+
}
70+
}()
71+
}
72+
73+
func (c *Controller) Stop() {
74+
close(c.quit)
75+
}
76+
77+
func (c *Controller) PerformBackup() (string, error) {
78+
c.mutex.Lock()
79+
defer c.mutex.Unlock()
80+
81+
backupData, err := c.core.Create(c.config.PrivateKey)
82+
if err != nil {
83+
return "", err
84+
}
85+
86+
fileName, err := c.config.FileNameGetter()
87+
if err != nil {
88+
return "", err
89+
}
90+
91+
if err := os.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
92+
return "", err
93+
}
94+
95+
file, err := os.Create(fileName)
96+
if err != nil {
97+
return "", err
98+
}
99+
defer file.Close()
100+
101+
_, err = file.Write(backupData)
102+
if err != nil {
103+
return "", err
104+
}
105+
106+
return fileName, nil
107+
}
108+
109+
func (c *Controller) LoadBackup(filePath string) error {
110+
c.mutex.Lock()
111+
defer c.mutex.Unlock()
112+
113+
file, err := os.Open(filePath)
114+
if err != nil {
115+
return err
116+
}
117+
defer file.Close()
118+
119+
fileInfo, err := file.Stat()
120+
if err != nil {
121+
return err
122+
}
123+
124+
backupData := make([]byte, fileInfo.Size())
125+
_, err = file.Read(backupData)
126+
if err != nil {
127+
return err
128+
}
129+
130+
return c.core.Restore(c.config.PrivateKey, backupData)
131+
}

node/backup/controller_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package backup
2+
3+
import (
4+
"encoding/json"
5+
"encoding/xml"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
type Foo struct {
13+
Value int
14+
PreciseValue float64
15+
}
16+
17+
type Bar struct {
18+
Names []string
19+
Surname string
20+
}
21+
22+
func TestController(t *testing.T) {
23+
filename := t.TempDir() + "/test_backup.bak"
24+
controller, err := NewController(BackupConfig{
25+
FileNameGetter: func() (string, error) { return filename, nil },
26+
PrivateKey: []byte("0123456789abcdef0123456789abcdef"),
27+
})
28+
require.NoError(t, err)
29+
30+
foo := Foo{
31+
Value: 123,
32+
PreciseValue: 456.789,
33+
}
34+
bar := Bar{
35+
Names: []string{"Bob", "Tom"},
36+
Surname: "Smith",
37+
}
38+
39+
var fooFromBackup Foo
40+
var barFromBackup Bar
41+
42+
controller.Register("foo", func() ([]byte, error) { return json.Marshal(foo) }, func(data []byte) error { return json.Unmarshal(data, &fooFromBackup) })
43+
controller.Register("bar", func() ([]byte, error) { return xml.Marshal(bar) }, func(data []byte) error { return xml.Unmarshal(data, &barFromBackup) })
44+
45+
filename, err = controller.PerformBackup()
46+
require.NoError(t, err)
47+
require.Equal(t, filename, filename)
48+
49+
require.False(t, reflect.DeepEqual(bar, barFromBackup))
50+
require.False(t, reflect.DeepEqual(foo, fooFromBackup))
51+
52+
err = controller.LoadBackup(filename)
53+
require.NoError(t, err)
54+
55+
require.True(t, reflect.DeepEqual(bar, barFromBackup))
56+
require.True(t, reflect.DeepEqual(foo, fooFromBackup))
57+
}

0 commit comments

Comments
 (0)