Skip to content

feat(backup)_: local user data backups #6722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/geth_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,10 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
}
}

if err = b.statusNode.StartLocalBackup(); err != nil {
return err
}

err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
if err != nil {
b.logger.Error("failed to update account")
Expand Down Expand Up @@ -943,6 +947,10 @@ func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, pass
}
}

if err = b.statusNode.StartLocalBackup(); err != nil {
return err
}

err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
if err != nil {
b.logger.Info("failed to update account")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN backup_path VARCHAR DEFAULT '';
34 changes: 34 additions & 0 deletions mobile/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2401,3 +2401,37 @@ func IntendedPanic(message string) string {
panic(err)
})
}

func performLocalBackup() string {
filePath, err := statusBackend.StatusNode().PerformLocalBackup()
if err != nil {
return makeJSONResponse(err)
}

respJSON, err := json.Marshal(map[string]interface{}{
"filePath": filePath,
})
if err != nil {
return makeJSONResponse(err)
}

return string(respJSON)
}

func PerformLocalBackup() string {
return callWithResponse(performLocalBackup)
}

func LoadLocalBackup(requestJSON string) string {
var request requests.LoadLocalBackup
err := json.Unmarshal([]byte(requestJSON), &request)
if err != nil {
return makeJSONResponse(err)
}
err = request.Validate()
if err != nil {
return makeJSONResponse(err)
}
err = statusBackend.StatusNode().LoadLocalBackup(request.FilePath)
return makeJSONResponse(err)
}
5 changes: 5 additions & 0 deletions multiaccounts/settings/columns.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ var (
dBColumnName: "backup_fetched",
valueHandler: BoolHandler,
}
BackupPath = SettingField{
reactFieldName: "backup-path",
dBColumnName: "backup_path",
}
ChaosMode = SettingField{
reactFieldName: "chaos-mode?",
dBColumnName: "chaos_mode",
Expand Down Expand Up @@ -548,6 +552,7 @@ var (
AutoMessageEnabled,
BackupEnabled,
BackupFetched,
BackupPath,
Bio,
ChaosMode,
CollectibleGroupByCollection,
Expand Down
29 changes: 24 additions & 5 deletions multiaccounts/settings/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"sync"
"time"

"github.com/status-im/status-go/common/dbsetup"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/multiaccounts/errors"
maErrors "github.com/status-im/status-go/multiaccounts/errors"
"github.com/status-im/status-go/nodecfg"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/sqlite"
Expand Down Expand Up @@ -213,7 +214,7 @@ func (db *Database) getSettingFieldFromReactName(reactName string) (SettingField
return s, nil
}
}
return SettingField{}, errors.ErrInvalidConfig
return SettingField{}, maErrors.ErrInvalidConfig
}

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

_, err = update.Exec(value)
result, err := update.Exec(value)
if err != nil {
return err
}

rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}

if rowsAffected == 0 {
// If no rows were affected, it means the setting does not exist
return errors.New("settings not initialized, please call CreateSettings first")
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Bug" we found with @osmaczko . If the settings are not created at the start (of a test for example), then saveSetting still "works", ie doesn't send an error. It makes debugging the failing test very annoying.


if db.notifier != nil {
db.notifier(setting, value)
}
Expand Down Expand Up @@ -334,7 +344,7 @@ func (db *Database) SaveSyncSetting(setting SettingField, value interface{}, clo
return err
}
if clock <= ls {
return errors.ErrNewClockOlderThanCurrent
return maErrors.ErrNewClockOlderThanCurrent
}

err = db.SetSettingLastSynced(setting, clock)
Expand Down Expand Up @@ -398,7 +408,7 @@ func (db *Database) GetSettings() (Settings, error) {
test_networks_enabled, mutual_contact_enabled, profile_migration_needed, wallet_token_preferences_group_by_community, url_unfurling_mode,
mnemonic_was_not_shown, wallet_show_community_asset_when_sending_tokens, wallet_display_assets_below_balance,
wallet_display_assets_below_balance_threshold, wallet_collectible_preferences_group_by_collection, wallet_collectible_preferences_group_by_community,
peer_syncing_enabled, auto_refresh_tokens_enabled, last_tokens_update, news_feed_enabled, news_feed_last_fetched_timestamp, news_rss_enabled
peer_syncing_enabled, auto_refresh_tokens_enabled, last_tokens_update, news_feed_enabled, news_feed_last_fetched_timestamp, news_rss_enabled, backup_path
FROM
settings
WHERE
Expand Down Expand Up @@ -487,6 +497,7 @@ func (db *Database) GetSettings() (Settings, error) {
&s.NewsFeedEnabled,
&newsFeedLastFetchedTimestamp,
&s.NewsRSSEnabled,
&s.BackupPath,
)

if err != nil {
Expand Down Expand Up @@ -928,3 +939,11 @@ func (db *Database) NewsRSSEnabled() (result bool, err error) {
}
return result, err
}

func (db *Database) BackupPath() (result string, err error) {
err = db.makeSelectRow(BackupPath).Scan(&result)
if err == sql.ErrNoRows {
return result, nil
}
return result, err
}
1 change: 1 addition & 0 deletions multiaccounts/settings/database_settings_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type DatabaseSettingsManager interface {
CanSyncOnMobileNetwork() (result bool, err error)
ShouldBroadcastUserStatus() (result bool, err error)
BackupEnabled() (result bool, err error)
BackupPath() (result string, err error)
AutoMessageEnabled() (result bool, err error)
LastBackup() (result uint64, err error)
BackupFetched() (result bool, err error)
Expand Down
20 changes: 20 additions & 0 deletions multiaccounts/settings/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,23 @@ func TestDatabase_NewsRSSEnabled(t *testing.T) {
require.NoError(t, err)
require.Equal(t, false, settings.NewsRSSEnabled)
}

func TestDatabase_BackupPath(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()

require.NoError(t, db.CreateSettings(settings, config))

path, err := db.BackupPath()
require.NoError(t, err)
// The default backup path is empty
require.Equal(t, "", path)

testPath := "/path/to/backup"
err = db.SaveSetting(BackupPath.GetReactName(), testPath)
require.NoError(t, err)

settings, err = db.GetSettings()
require.NoError(t, err)
require.Equal(t, testPath, settings.BackupPath)
}
1 change: 1 addition & 0 deletions multiaccounts/settings/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ type Settings struct {
LastBackup uint64 `json:"last-backup,omitempty"`
BackupEnabled bool `json:"backup-enabled?,omitempty"`
BackupFetched bool `json:"backup-fetched?,omitempty"`
BackupPath string `json:"backup-path,omitempty"`
AutoMessageEnabled bool `json:"auto-message-enabled?,omitempty"`
GifAPIKey string `json:"gifs/api-key"`
TestNetworksEnabled bool `json:"test-networks-enabled?,omitempty"`
Expand Down
134 changes: 134 additions & 0 deletions node/backup/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package backup

import (
"errors"
"os"
"path/filepath"
"sync"
"time"

"go.uber.org/zap"

"github.com/status-im/status-go/common"
)

type BackupConfig struct {
PrivateKey []byte
FileNameGetter func() (string, error)
BackupEnabled bool
Interval time.Duration
}

type Controller struct {
config BackupConfig
core *core
logger *zap.Logger
quit chan struct{}
mutex sync.Mutex
}

func NewController(config BackupConfig, logger *zap.Logger) (*Controller, error) {
if len(config.PrivateKey) == 0 {
return nil, errors.New("private key must be provided")
}
if config.FileNameGetter == nil {
return nil, errors.New("filename getter must be provided")
}

return &Controller{
config: config,
core: newCore(),
logger: logger,
quit: make(chan struct{}),
}, nil
}

func (c *Controller) Register(componentName string, dumpFunc func() ([]byte, error), loadFunc func([]byte) error) {
c.mutex.Lock()
defer c.mutex.Unlock()

c.core.Register(componentName, dumpFunc, loadFunc)
}

func (c *Controller) Start() {
if !c.config.BackupEnabled {
return
}

go func() {
defer common.LogOnPanic()
ticker := time.NewTicker(c.config.Interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_, err := c.PerformBackup()
if err != nil {
c.logger.Error("Error performing backup: %v\n", zap.Error(err))
}
case <-c.quit:
return
}
}
}()
}

func (c *Controller) Stop() {
close(c.quit)
}

func (c *Controller) PerformBackup() (string, error) {
c.mutex.Lock()
defer c.mutex.Unlock()

backupData, err := c.core.Create(c.config.PrivateKey)
if err != nil {
return "", err
}

fileName, err := c.config.FileNameGetter()
if err != nil {
return "", err
}

if err := os.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
return "", err
}

file, err := os.Create(fileName)
if err != nil {
return "", err
}
defer file.Close()

_, err = file.Write(backupData)
if err != nil {
return "", err
}

return fileName, nil
}

func (c *Controller) LoadBackup(filePath string) error {
c.mutex.Lock()
defer c.mutex.Unlock()

file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()

fileInfo, err := file.Stat()
if err != nil {
return err
}

backupData := make([]byte, fileInfo.Size())
_, err = file.Read(backupData)
if err != nil {
return err
}

return c.core.Restore(c.config.PrivateKey, backupData)
}
Loading