Skip to content
Permalink
Browse files

feat(backups): implement local backup strategy

resolves #2214
  • Loading branch information...
korhaliv committed May 22, 2019
1 parent 0d4d342 commit 50fa5049726e905328bdf4be7e3333f59c51b9fc
@@ -0,0 +1,62 @@
import fs from 'fs'
import { promisify } from 'util'
import chainify from '@zap/utils/chainify'

const writeFileAsync = promisify(fs.writeFile)

class BackupService {
terminate() {
// no-op
}

init() {
// no-op
}

isLoggedIn() {
return true
}

/**
* Saves specified backup
*
* @param {string} fileName backup dir path
* @param {string} dir relative file path
* @param {Buffer} backup `Buffer` with backup data
* @returns {string} file name
* @memberof BackupService
*/
saveBackup = chainify(async (fileName, dir, backup) => {
const filePath = `${dir}/${fileName}`
await writeFileAsync(filePath, backup)
return dir
})

/**
* Provider name
*
* @readonly
* @memberof BackupService
*/
get name() {
return 'local'
}

/**
* This service doesn't use tokens and doesn't emit token events
*/
get isUsingTokens() {
return false
}
}

// singleton backup service

let backupService

export default function getBackupService() {
if (!backupService) {
backupService = new BackupService()
}
return backupService
}
@@ -6,28 +6,40 @@ export default function createBackupService(mainWindow) {
// helper func to send messages to the renderer process
const send = (msg, params) => mainWindow.webContents.send(msg, params)

// setups ipc to listen for backup service token updates and forward updates to
// the renderer process
const setupTokenUpdateListeners = (walletId, backupService) => {
const handleTokensReceived = tokens => {
// ensure the we are always storing the latest tokens available
send('backupTokensUpdated', {
tokens,
provider: backupService.name,
walletId,
})
mainLog.info('Tokens received: %o', tokens)
}
// re-subscribe for token updates
backupService.removeAllListeners('tokensReceived')
backupService.on('tokensReceived', handleTokensReceived)
}

ipcMain.on('initBackupService', async (event, { walletId, tokens, provider }) => {
mainLog.info('Initializing backup service powered by: %s for wallet: %s', provider, walletId)
try {
const backupService = getBackupService(provider)
// cleanup existing instance if any
backupService.logout()
const handleTokensReceived = tokens => {
// ensure the we are always storing the latest tokens available
send('backupTokensUpdated', {
tokens,
provider: backupService.name,
walletId,
})
mainLog.info('Tokens received: %o', tokens)
backupService.terminate()
// we are dealing with cloud based backup strategies that emit token updates
if (backupService.isUsingTokens) {
setupTokenUpdateListeners(walletId, backupService)
await backupService.init(tokens)
} else {
await backupService.init()
}
// re-subscribe for token updates
backupService.removeAllListeners('tokensReceived')
backupService.on('tokensReceived', handleTokensReceived)

await backupService.login(tokens)
send('backupServiceInitialized', { walletId, provider })
send('backupServiceInitialized', { walletId })
} catch (e) {
send('initBackupServiceError')
mainLog.warn('Unable to initialize backup service: %o', e)
}
})
@@ -43,7 +55,7 @@ export default function createBackupService(mainWindow) {
backupMetadata && backupMetadata.backupId,
backup
)
mainLog.info('Backup updated. GDrive fileID: %s', backupId)
mainLog.info('Backup updated, fileID: %s', backupId)
send('saveBackupSuccess', {
backupId,
provider: backupService.name,
@@ -53,10 +65,8 @@ export default function createBackupService(mainWindow) {
}
} catch (e) {
mainLog.warn('Unable to backup wallet: %o', e)
mainWindow.webContents.send('saveBackupError')
send('saveBackupError')
}
}
)
}

export getBackupService from './gdrive'
@@ -1,4 +1,5 @@
import getGDrive from './gdrive'
import getLocal from './local'

export const GOOGLE_DRIVE = 'gdrive'
export const DROPBOX = 'dropbox'
@@ -11,7 +12,7 @@ export default function getBackupService(provider) {
case DROPBOX:
throw new Error('not implemented')
case LOCAL:
throw new Error('not implemented')
return getLocal()
default:
throw new Error('not implemented')
}
@@ -20,6 +20,7 @@ import {
WalletCreate,
WalletRecover,
BackupSetup,
BackupSetupLocal,
} from './Steps'
import messages from './messages'

@@ -41,6 +42,7 @@ function removeSteps(formSteps, steps) {
class Onboarding extends React.Component {
static propTypes = {
autopilot: PropTypes.bool, // eslint-disable-line react/boolean-prop-naming
backupProvider: PropTypes.string.isRequired,
clearCreateWalletError: PropTypes.func.isRequired,
clearStartLndError: PropTypes.func.isRequired,
connectionCert: PropTypes.string,
@@ -51,7 +53,6 @@ class Onboarding extends React.Component {
createWallet: PropTypes.func.isRequired,
createWalletError: PropTypes.string,
fetchSeed: PropTypes.func.isRequired,
initBackupService: PropTypes.func.isRequired,
isCreatingWallet: PropTypes.bool,
isFetchingSeed: PropTypes.bool,
isLightningGrpcActive: PropTypes.bool,
@@ -70,12 +71,14 @@ class Onboarding extends React.Component {
setConnectionString: PropTypes.func.isRequired,
setConnectionType: PropTypes.func.isRequired,
setLndconnect: PropTypes.func.isRequired,
setLocalPath: PropTypes.func.isRequired,
setName: PropTypes.func.isRequired,
setNetwork: PropTypes.func.isRequired,
setPassphrase: PropTypes.func.isRequired,
setPassword: PropTypes.func.isRequired,
setSeed: PropTypes.func.isRequired,
setUnlockWalletError: PropTypes.func.isRequired,
setupBackupService: PropTypes.func.isRequired,
startLnd: PropTypes.func.isRequired,
startLndCertError: PropTypes.string,
startLndHostError: PropTypes.string,
@@ -146,8 +149,9 @@ class Onboarding extends React.Component {
createWallet,
stopLnd,
unlockWallet,
initBackupService,
setupBackupService,
setBackupProvider,
setLocalPath,
} = this.props

let formSteps = []
@@ -169,6 +173,7 @@ class Onboarding extends React.Component {
<Wizard.Step key="Network" component={Network} {...{ network, setNetwork }} />,
<Wizard.Step key="Autopilot" component={Autopilot} {...{ autopilot, setAutopilot }} />,
<Wizard.Step key="BackupSetup" component={BackupSetup} {...{ setBackupProvider }} />,
<Wizard.Step key="BackupSetupLocal" component={BackupSetupLocal} {...{ setLocalPath }} />,
<Wizard.Step
key="WalletCreate"
component={WalletCreate}
@@ -177,7 +182,7 @@ class Onboarding extends React.Component {
isCreatingWallet,
createWallet,
createWalletError,
initBackupService,
setupBackupService,
}}
/>,
]
@@ -273,11 +278,12 @@ class Onboarding extends React.Component {
...formSteps,
]

// It is currently not recommended to use autopilot on mainnet.
// If user has selected mainnet, remove the autopilot form step.
return removeSteps(steps, [
// It is currently not recommended to use autopilot on mainnet.
// If user has selected mainnet, remove the autopilot form step.
['Autopilot', network === 'mainnet' && !isMainnetAutopilot()],
['Network', !isNetworkSelectionEnabled()],
['BackupSetupLocal', this.shouldRemoveBackupSetupLocalStep()],
])
}

@@ -303,6 +309,10 @@ class Onboarding extends React.Component {
return seed.length > 0 ? 0 : null
}

shouldRemoveBackupSetupLocalStep() {
const { backupProvider } = this.props
return backupProvider != 'local'
}
render() {
const { connectionType, isLightningGrpcActive } = this.props
const steps = this.getSteps()
@@ -2,21 +2,16 @@ import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import { withFieldApi } from 'informed'
import styled from 'styled-components'
import { Flex } from 'rebass'
import { Form, RadioGroup, Heading, Bar } from 'components/UI'
import { BACKUP_FORM_WIDTH, BACKUP_FORM_HEIGHT } from './components/settings'
import BaseBackupTypeItem from './components/BackupTypeItem'
import Container from './components/Container'

import messages from './messages'

const BackupTypeItem = withFieldApi('backupType')(BaseBackupTypeItem)

const Container = styled(Flex)`
position: absolute;
top: -30px;
bottom: 0;
visibility: ${props => (props.lndConnect ? 'hidden' : 'visible')};
`

class BackupSetup extends React.Component {
static propTypes = {
setBackupProvider: PropTypes.func.isRequired,
@@ -46,6 +41,8 @@ class BackupSetup extends React.Component {
return (
<Container alignItems="center" flexDirection="column" justifyContent="center" mt={3}>
<Form
height={BACKUP_FORM_HEIGHT}
width={BACKUP_FORM_WIDTH}
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
@@ -76,7 +73,6 @@ class BackupSetup extends React.Component {
width={1 / 3}
/>
<BackupTypeItem
isDisabled
label={<FormattedMessage {...messages.backup_type_local} />}
mx={5}
value="local"
@@ -0,0 +1,83 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import { Form, OpenDialogInput, Heading, Bar } from 'components/UI'
import { BACKUP_FORM_WIDTH, BACKUP_FORM_HEIGHT } from './components/settings'
import Container from './components/Container'
import messages from './messages'

class BackupSetupLocal extends React.Component {
static propTypes = {
setLocalPath: PropTypes.func.isRequired,
wizardApi: PropTypes.object,
wizardState: PropTypes.object,
}

static defaultProps = {
wizardApi: {},
wizardState: {},
}

validatePath = () => {
return true
}

handleSubmit = values => {
const { setLocalPath } = this.props
setLocalPath(values.path)
}

setFormApi = formApi => {
this.formApi = formApi
}

render() {
const { wizardApi, wizardState, setLocalPath, ...rest } = this.props
const { getApi, onChange, onSubmit, onSubmitFailure } = wizardApi
const { currentItem } = wizardState

return (
<Container alignItems="center" flexDirection="column" justifyContent="center" mt={3}>
<Form
height={BACKUP_FORM_HEIGHT}
width={BACKUP_FORM_WIDTH}
{...rest}
getApi={formApi => {
this.setFormApi(formApi)
if (getApi) {
getApi(formApi)
}
}}
onChange={onChange && (formState => onChange(formState, currentItem))}
onSubmit={values => {
this.handleSubmit(values)
if (onSubmit) {
onSubmit(values)
}
}}
onSubmitFailure={onSubmitFailure}
>
<Heading.h1 mb={3}>
<FormattedMessage {...messages.backup_header} />
</Heading.h1>
<Bar mb={4} />
<OpenDialogInput
description={<FormattedMessage {...messages.backup_path_description} />}
field="path"
isRequired
label={<FormattedMessage {...messages.backup_path_label} />}
mb={3}
mode="openDirectory"
name="path"
onBlur={this.validatePath}
validateOnBlur
validateOnChange
width={1}
/>
</Form>
</Container>
)
}
}

export default BackupSetupLocal

0 comments on commit 50fa504

Please sign in to comment.
You can’t perform that action at this time.