From 97d667a46f55f70946120abe6b4ea3d38a2d77be Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Mon, 27 Sep 2021 11:34:37 +0200 Subject: [PATCH] feat-preferences: data backup export and import (#646) * feat: data backups initial implementation * feat: improve data backups design in preferences * feat: split import backup in multiple segments * feat(preferences): move import backup spinner next to import button * fix(data-backups): padding between radio btn and buttons --- .../preferences/panes/Security.tsx | 3 +- .../panes/security-segments/DataBackups.tsx | 181 ++++++++++++++++++ .../panes/security-segments/index.ts | 1 + 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index 900940d7a..007dad57f 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -2,7 +2,7 @@ import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; -import { Encryption, PasscodeLock, Protections } from './security-segments'; +import { Encryption, PasscodeLock, Protections, DataBackups } from './security-segments'; import { TwoFactorAuthWrapper } from './two-factor-auth'; import { MfaProps } from './two-factor-auth/MfaProps'; @@ -20,5 +20,6 @@ export const Security: FunctionComponent = (props) => ( userProvider={props.userProvider} /> + ); diff --git a/app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx b/app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx new file mode 100644 index 000000000..f40193285 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx @@ -0,0 +1,181 @@ +import { isDesktopApplication } from '@/utils'; +import { alertDialog } from '@Services/alertService'; +import { + STRING_IMPORT_SUCCESS, + STRING_INVALID_IMPORT_FILE, + STRING_UNSUPPORTED_BACKUP_FILE_VERSION, + StringImportError +} from '@/strings'; +import { BackupFile } from '@standardnotes/snjs'; +import { useRef, useState } from 'preact/hooks'; +import { WebApplication } from '@/ui_models/application'; +import { JSXInternal } from 'preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { PreferencesGroup, PreferencesSegment, Title, Text, Subtitle } from '../../components'; +import { Button } from '@/components/Button'; + +type Props = { + application: WebApplication; + appState: AppState; +} + +export const DataBackups = observer(({ + application, + appState +}: Props) => { + + const fileInputRef = useRef(null); + const [isImportDataLoading, setIsImportDataLoading] = useState(false); + + const { isBackupEncrypted, isEncryptionEnabled, setIsBackupEncrypted } = appState.accountMenu; + + const downloadDataArchive = () => { + application.getArchiveService().downloadBackup(isBackupEncrypted); + }; + + const readFile = async (file: File): Promise => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target!.result as string); + resolve(data); + } catch (e) { + application.alertService.alert(STRING_INVALID_IMPORT_FILE); + } + }; + reader.readAsText(file); + }); + }; + + const performImport = async (data: BackupFile) => { + setIsImportDataLoading(true); + + const result = await application.importData(data); + + setIsImportDataLoading(false); + + if (!result) { + return; + } + + let statusText = STRING_IMPORT_SUCCESS; + if ('error' in result) { + statusText = result.error; + } else if (result.errorCount) { + statusText = StringImportError(result.errorCount); + } + void alertDialog({ + text: statusText + }); + }; + + const importFileSelected = async (event: TargetedEvent) => { + const { files } = (event.target as HTMLInputElement); + + if (!files) { + return; + } + const file = files[0]; + const data = await readFile(file); + if (!data) { + return; + } + + const version = data.version || data.keyParams?.version || data.auth_params?.version; + if (!version) { + await performImport(data); + return; + } + + if ( + application.protocolService.supportedVersions().includes(version) + ) { + await performImport(data); + } else { + setIsImportDataLoading(false); + void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION }); + } + }; + + // Whenever "Import Backup" is either clicked or key-pressed, proceed the import + const handleImportFile = (event: TargetedEvent | KeyboardEvent) => { + if (event instanceof KeyboardEvent) { + const { code } = event; + + // Process only when "Enter" or "Space" keys are pressed + if (code !== 'Enter' && code !== 'Space') { + return; + } + // Don't proceed the event's default action + // (like scrolling in case the "space" key is pressed) + event.preventDefault(); + } + + (fileInputRef.current as HTMLInputElement).click(); + }; + + return ( + <> + + + Data Backups + + {!isDesktopApplication() && ( + + Backups are automatically created on desktop and can be managed + via the "Backups" top-level menu. + + )} + + Download a backup of all your data + + {isEncryptionEnabled && ( +
+
+ + +
+
+ )} + +