forked from LN-Zap/zap-desktop
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): implement basic backuping via gdrive
resolve LN-Zap#1971
- Loading branch information
Showing
9 changed files
with
300 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import EventEmitter from 'events' | ||
import config from 'config' | ||
import { mainLog } from '@zap/utils/log' | ||
import createClient from './gdrive' | ||
|
||
export function forwardEvent(service, event, target) { | ||
service.on(event, data => target.emit(event, data)) | ||
} | ||
|
||
class BackupService extends EventEmitter { | ||
drive = null | ||
|
||
constructor() { | ||
super() | ||
} | ||
|
||
async logout() { | ||
const { drive } = this | ||
drive && drive.removeAllListeners('tokensReceived') | ||
this.drive = null | ||
} | ||
|
||
async login(tokens) { | ||
const { redirectUrl, clientId, scope } = config.backup.gdrive | ||
const { drive } = this | ||
if (!drive) { | ||
this.drive = await createClient({ | ||
clientId, | ||
authRedirectUrl: redirectUrl, | ||
scope, | ||
tokens, | ||
}) | ||
mainLog.info('forwardEvent') | ||
forwardEvent(this.drive, 'tokensReceived', this) | ||
} | ||
|
||
return true | ||
} | ||
|
||
async isLoggedIn() { | ||
const { drive } = this | ||
return await drive.testConnection() | ||
} | ||
|
||
getTokens() { | ||
const { drive } = this | ||
return drive.getTokens() | ||
} | ||
getBackupId() {} | ||
async loadBackup(walletId) { | ||
const { drive, getBackupId } = this | ||
const fileId = getBackupId(walletId) | ||
if (fileId) { | ||
const backup = await drive.downloadToBuffer(fileId) | ||
return backup | ||
} | ||
return null | ||
} | ||
|
||
async saveBackup(walletId, fileId, backup) { | ||
const backupExists = async () => { | ||
try { | ||
await drive.getFileInfo(fileId) | ||
return true | ||
} catch (e) { | ||
return false | ||
} | ||
} | ||
const { drive } = this | ||
// if fileId is provded and backup exists - update it | ||
if (fileId && (await backupExists())) { | ||
await drive.updateFromBuffer(fileId, backup) | ||
return fileId | ||
} else { | ||
// create new file | ||
const { id } = await drive.uploadFromBuffer(walletId, backup) | ||
return id | ||
} | ||
} | ||
|
||
get name() { | ||
return 'gdrive' | ||
} | ||
} | ||
// singleton backup service | ||
|
||
let backupService | ||
|
||
export default function getBackupService() { | ||
if (!backupService) { | ||
backupService = new BackupService() | ||
} | ||
|
||
return backupService | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { ipcMain } from 'electron' | ||
import { mainLog } from '@zap/utils/log' | ||
import getBackupService from './serviceFactory' | ||
|
||
export default function createBackupService(mainWindow) { | ||
const send = (msg, params) => mainWindow.webContents.send(msg, params) | ||
|
||
ipcMain.on('initBackupService', async (event, { walletId, tokens, provider }) => { | ||
mainLog.info('Initializing backup service powered by: %o', provider) | ||
|
||
const backupService = getBackupService(provider) | ||
if (backupService) { | ||
// 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) | ||
} | ||
// re-subscribe for token updates | ||
backupService.removeAllListeners('tokensReceived') | ||
backupService.on('tokensReceived', handleTokensReceived) | ||
|
||
await backupService.login(tokens) | ||
send('backupServiceInitialized', { walletId, provider }) | ||
} | ||
}) | ||
|
||
ipcMain.on( | ||
'saveBackup', | ||
async (event, { backup, walletId, provider, backupMetadata, nodePub }) => { | ||
try { | ||
const backupService = getBackupService(provider) | ||
if (backupService) { | ||
const backupId = await backupService.saveBackup( | ||
nodePub, | ||
backupMetadata && backupMetadata.backupId, | ||
backup | ||
) | ||
mainLog.info('Backup updated. GDrive fileID: %o', backupId) | ||
mainWindow.webContents.send('backupSaveSuccess', { | ||
backupId, | ||
provider: backupService.name, | ||
walletId, | ||
}) | ||
mainLog.info(`saveBackup ${walletId} ${nodePub}`) | ||
} | ||
} catch (e) { | ||
mainLog.warn('Unable to backup wallet %o: ', e) | ||
mainWindow.webContents.send('backupSaveError') | ||
} | ||
} | ||
) | ||
} | ||
|
||
export getBackupService from './gdrive' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import getGDrive from './gdrive' | ||
|
||
export const GOOGLE_DRIVE = 'gdrive' | ||
export const DROPBOX = 'dropbox' | ||
export const LOCAL = 'local' | ||
|
||
export default function getBackupService(provider) { | ||
switch (provider) { | ||
case GOOGLE_DRIVE: | ||
return getGDrive() | ||
case DROPBOX: | ||
throw new Error('not implemented') | ||
case LOCAL: | ||
throw new Error('not implemented') | ||
default: | ||
throw new Error('not implemented') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { send } from 'redux-electron-ipc' | ||
import { grpcService } from 'workers' | ||
import { walletSelectors } from './wallet' | ||
import { infoSelectors } from './info' | ||
|
||
const getDbRec = async walletId => await window.db.backup.get(walletId) | ||
|
||
const setDbRec = async (walletId, update) => { | ||
const updated = await window.db.backup.update(walletId, update) | ||
if (updated === 0) { | ||
try { | ||
await window.db.backup.add({ id: walletId, ...update }) | ||
} catch (e) { | ||
// Do nothing if there was an error - this indicates that the item already exists and was unchanged. | ||
} | ||
} | ||
} | ||
|
||
export async function backupTokensUpdated(event, { provider, tokens, walletId }) { | ||
const backupDesc = await getDbRec(walletId) | ||
await setDbRec(walletId, { | ||
[provider]: { ...backupDesc[provider], tokens }, | ||
}) | ||
} | ||
|
||
export async function hasBackupSetup(walletId) { | ||
const backupDesc = await getDbRec(walletId) | ||
if (backupDesc) { | ||
const { activeProvider } = backupDesc | ||
return activeProvider && backupDesc[activeProvider] | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* | ||
* | ||
* @export | ||
* @param {string} walletId wallet identifier. if not specified uses current active wallet | ||
* @param {string} provider backup provider. if not specified attempts to use current active provider | ||
* @returns | ||
*/ | ||
export function initBackupService(walletId, provider) { | ||
return async (dispatch, getState) => { | ||
const wId = walletId || walletSelectors.activeWallet(getState()) | ||
|
||
const getServiceParams = async () => { | ||
const backupDesc = await getDbRec(wId) | ||
// attempt to initialize backup service with stored tokens | ||
if (backupDesc) { | ||
const { activeProvider } = backupDesc | ||
const { tokens } = backupDesc[activeProvider] || {} | ||
return { walletId: wId, tokens, provider: activeProvider } | ||
} | ||
|
||
return { walletId: wId, provider } | ||
} | ||
|
||
return dispatch(send('initBackupService', await getServiceParams())) | ||
} | ||
} | ||
|
||
export const backupCurrentWallet = backup => async (dispatch, getState) => { | ||
const getFreshBackup = async () => { | ||
const grpc = await grpcService | ||
if (grpc.services.Lightning.exportAllChannelBackups) { | ||
return await grpc.services.Lightning.exportAllChannelBackups({}) | ||
} | ||
return null | ||
} | ||
|
||
const getBackupBuff = backupData => | ||
backupData && backupData.multi_chan_backup && backupData.multi_chan_backup.multi_chan_backup | ||
|
||
try { | ||
const state = getState() | ||
const walletId = walletSelectors.activeWallet(state) | ||
const nodePub = infoSelectors.nodePub(state) | ||
if (walletId) { | ||
const backupData = backup || (await getFreshBackup()) | ||
const { activeProvider, ...rest } = (await getDbRec(walletId)) || {} | ||
const backupMetadata = activeProvider && rest[activeProvider] | ||
const canBackup = backupData && activeProvider | ||
canBackup && | ||
dispatch( | ||
send('saveBackup', { | ||
backup: getBackupBuff(backupData), | ||
walletId, | ||
backupMetadata, | ||
nodePub, | ||
provider: activeProvider, | ||
}) | ||
) | ||
} | ||
} catch (e) { | ||
// Do nothing | ||
} | ||
} | ||
|
||
export const backupSaveSuccess = async (event, { provider, backupId, walletId }) => { | ||
const backupDesc = await getDbRec(walletId) | ||
await setDbRec(walletId, { | ||
[provider]: { ...backupDesc[provider], backupId }, | ||
}) | ||
} | ||
export const backupServiceInitialized = (event, { walletId, provider }) => async dispatch => { | ||
await setDbRec(walletId, { | ||
activeProvider: provider, | ||
}) | ||
dispatch(backupCurrentWallet()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters