mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-02 08:13:37 +01:00
Allow disabling local backups
This commit is contained in:
@@ -8365,6 +8365,22 @@
|
||||
"messageformat": "Change",
|
||||
"description": "Button to change the folder in which local on-device backups are stored."
|
||||
},
|
||||
"icu:Preferences__local-backups-turn-off": {
|
||||
"messageformat": "Turn off backups",
|
||||
"description": "Label for settings row to disable local on-device backups."
|
||||
},
|
||||
"icu:Preferences__local-backups-turn-off-action": {
|
||||
"messageformat": "Turn off",
|
||||
"description": "Button text in local backups settings and confirmation modal to disable local backups."
|
||||
},
|
||||
"icu:Preferences__local-backups-turn-off-confirmation": {
|
||||
"messageformat": "You will no longer be able to create backups on this computer without re-enabling.",
|
||||
"description": "Confirmation modal body for disabling local on-device backups."
|
||||
},
|
||||
"icu:Preferences__local-backups-turn-off-delete": {
|
||||
"messageformat": "Delete my backup from this computer",
|
||||
"description": "Checkbox label in confirmation modal for disabling local backups and deleting existing backup files."
|
||||
},
|
||||
"icu:Preferences__local-backups-copy-key": {
|
||||
"messageformat": "Copy to clipboard",
|
||||
"description": "Button label for copying the backup key to clipboard in the settings for local on-device backups"
|
||||
|
||||
@@ -824,7 +824,7 @@ $secondary-text-color: light-dark(
|
||||
}
|
||||
}
|
||||
|
||||
.Preferences--BackupsRow .Preferences__control {
|
||||
.Preferences--BackupsRow .Preferences__flow-control {
|
||||
padding-block: 10px;
|
||||
align-items: initial;
|
||||
}
|
||||
|
||||
@@ -214,12 +214,20 @@ export namespace AxoAlertDialog {
|
||||
|
||||
export type CancelProps = Readonly<{
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
focusableWhenDisabled?: boolean;
|
||||
}>;
|
||||
|
||||
export const Cancel: FC<CancelProps> = memo(props => {
|
||||
return (
|
||||
<AlertDialog.Cancel asChild>
|
||||
<AxoButton.Root variant="secondary" size="md" width="grow">
|
||||
<AxoButton.Root
|
||||
variant="secondary"
|
||||
size="md"
|
||||
width="grow"
|
||||
disabled={props.disabled}
|
||||
focusableWhenDisabled={props.focusableWhenDisabled}
|
||||
>
|
||||
{props.children}
|
||||
</AxoButton.Root>
|
||||
</AlertDialog.Cancel>
|
||||
|
||||
@@ -386,6 +386,8 @@ export namespace AxoDialog {
|
||||
symbol?: AxoSymbol.InlineGlyphName;
|
||||
arrow?: boolean;
|
||||
experimentalSpinner?: { 'aria-label': string } | null;
|
||||
disabled?: boolean;
|
||||
focusableWhenDisabled?: boolean;
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
@@ -397,6 +399,8 @@ export namespace AxoDialog {
|
||||
symbol={props.symbol}
|
||||
arrow={props.arrow}
|
||||
experimentalSpinner={props.experimentalSpinner}
|
||||
disabled={props.disabled}
|
||||
focusableWhenDisabled={props.focusableWhenDisabled}
|
||||
size="md"
|
||||
width="grow"
|
||||
onClick={props.onClick}
|
||||
|
||||
@@ -598,6 +598,7 @@ export default {
|
||||
openFileInFolder: action('openFileInFolder'),
|
||||
pickLocalBackupFolder: () =>
|
||||
Promise.resolve('/home/signaluser/Signal Backups/'),
|
||||
disableLocalBackups: () => Promise.resolve(),
|
||||
promptOSAuth: () => Promise.resolve('success'),
|
||||
refreshCloudBackupStatus: action('refreshCloudBackupStatus'),
|
||||
refreshBackupSubscriptionStatus: action('refreshBackupSubscriptionStatus'),
|
||||
|
||||
@@ -245,6 +245,11 @@ type PropsFunctionType = {
|
||||
|
||||
// Other props
|
||||
addCustomColor: (color: CustomColorType) => unknown;
|
||||
disableLocalBackups: ({
|
||||
deleteExistingBackups,
|
||||
}: {
|
||||
deleteExistingBackups: boolean;
|
||||
}) => Promise<void>;
|
||||
doDeleteAllData: () => unknown;
|
||||
editCustomColor: (colorId: string, color: CustomColorType) => unknown;
|
||||
getMessageCountBySchemaVersion: () => Promise<MessageCountBySchemaVersionType>;
|
||||
@@ -403,6 +408,7 @@ export function Preferences({
|
||||
customColors,
|
||||
defaultConversationColor,
|
||||
deviceName = '',
|
||||
disableLocalBackups,
|
||||
doDeleteAllData,
|
||||
editCustomColor,
|
||||
emojiSkinToneDefault,
|
||||
@@ -2283,6 +2289,7 @@ export function Preferences({
|
||||
openFileInFolder={openFileInFolder}
|
||||
osName={osName}
|
||||
pickLocalBackupFolder={pickLocalBackupFolder}
|
||||
disableLocalBackups={disableLocalBackups}
|
||||
settingsLocation={settingsLocation}
|
||||
promptOSAuth={promptOSAuth}
|
||||
refreshCloudBackupStatus={refreshCloudBackupStatus}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
LightIconLabel,
|
||||
SettingsRow,
|
||||
} from './PreferencesUtil.dom.js';
|
||||
import { ButtonVariant } from './Button.dom.js';
|
||||
import type { SettingsLocation } from '../types/Nav.std.js';
|
||||
import { SettingsPage } from '../types/Nav.std.js';
|
||||
import { I18n } from './I18n.dom.js';
|
||||
@@ -26,7 +25,6 @@ import type {
|
||||
PromptOSAuthReasonType,
|
||||
PromptOSAuthResultType,
|
||||
} from '../util/os/promptOSAuthMain.main.js';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.dom.js';
|
||||
import { AxoButton } from '../axo/AxoButton.dom.js';
|
||||
import { BackupLevel } from '../services/backups/types.std.js';
|
||||
import {
|
||||
@@ -65,6 +63,7 @@ export function PreferencesBackups({
|
||||
openFileInFolder,
|
||||
osName,
|
||||
pickLocalBackupFolder,
|
||||
disableLocalBackups,
|
||||
backupMediaDownloadStatus,
|
||||
cancelBackupMediaDownload,
|
||||
pauseBackupMediaDownload,
|
||||
@@ -94,6 +93,11 @@ export function PreferencesBackups({
|
||||
settingsLocation: SettingsLocation;
|
||||
backupMediaDownloadStatus: BackupMediaDownloadStatusType | undefined;
|
||||
cancelBackupMediaDownload: () => void;
|
||||
disableLocalBackups: ({
|
||||
deleteExistingBackups,
|
||||
}: {
|
||||
deleteExistingBackups: boolean;
|
||||
}) => Promise<void>;
|
||||
pauseBackupMediaDownload: () => void;
|
||||
resumeBackupMediaDownload: () => void;
|
||||
pickLocalBackupFolder: () => Promise<string | undefined>;
|
||||
@@ -106,8 +110,6 @@ export function PreferencesBackups({
|
||||
showToast: ShowToastAction;
|
||||
startLocalBackupExport: () => void;
|
||||
}): React.JSX.Element | null {
|
||||
const [authError, setAuthError] =
|
||||
useState<Omit<PromptOSAuthResultType, 'success'>>();
|
||||
const [isAuthPending, setIsAuthPending] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -162,6 +164,7 @@ export function PreferencesBackups({
|
||||
osName={osName}
|
||||
settingsLocation={settingsLocation}
|
||||
pickLocalBackupFolder={pickLocalBackupFolder}
|
||||
disableLocalBackups={disableLocalBackups}
|
||||
promptOSAuth={promptOSAuth}
|
||||
setSettingsLocation={setSettingsLocation}
|
||||
showToast={showToast}
|
||||
@@ -253,72 +256,54 @@ export function PreferencesBackups({
|
||||
|
||||
function renderLocalBackups() {
|
||||
return (
|
||||
<>
|
||||
<SettingsRow
|
||||
className="Preferences--BackupsRow"
|
||||
title={i18n('icu:Preferences__backup-other-ways')}
|
||||
>
|
||||
<FlowingControl>
|
||||
<div className="Preferences__two-thirds-flow">
|
||||
<LightIconLabel icon="Preferences__LocalBackupsIcon">
|
||||
<label>
|
||||
{i18n('icu:Preferences__local-backups')}{' '}
|
||||
<div className="Preferences__description">
|
||||
{i18n('icu:Preferences--local-backups-off-description')}
|
||||
</div>
|
||||
</label>
|
||||
</LightIconLabel>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'Preferences__flow-button',
|
||||
'Preferences__one-third-flow',
|
||||
'Preferences__one-third-flow--align-right'
|
||||
)}
|
||||
>
|
||||
<AxoButton.Root
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={isAuthPending}
|
||||
onClick={async () => {
|
||||
setAuthError(undefined);
|
||||
|
||||
if (!isLocalBackupsSetup) {
|
||||
try {
|
||||
setIsAuthPending(true);
|
||||
const result = await promptOSAuth('enable-backups');
|
||||
if (result !== 'success' && result !== 'unsupported') {
|
||||
setAuthError(result);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setIsAuthPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
setSettingsLocation({ page: SettingsPage.LocalBackups });
|
||||
}}
|
||||
>
|
||||
{isLocalBackupsSetup
|
||||
? i18n('icu:Preferences__button--manage')
|
||||
: i18n('icu:Preferences__button--set-up')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</FlowingControl>
|
||||
</SettingsRow>
|
||||
|
||||
{authError && (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="PreferencesLocalBackups--ErrorDialog"
|
||||
onClose={() => setAuthError(undefined)}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
cancelText={i18n('icu:ok')}
|
||||
<SettingsRow
|
||||
className="Preferences--BackupsRow"
|
||||
title={i18n('icu:Preferences__backup-other-ways')}
|
||||
>
|
||||
<FlowingControl>
|
||||
<div className="Preferences__two-thirds-flow">
|
||||
<LightIconLabel icon="Preferences__LocalBackupsIcon">
|
||||
<label>
|
||||
{i18n('icu:Preferences__local-backups')}{' '}
|
||||
<div className="Preferences__description">
|
||||
{i18n('icu:Preferences--local-backups-off-description')}
|
||||
</div>
|
||||
</label>
|
||||
</LightIconLabel>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'Preferences__flow-button',
|
||||
'Preferences__one-third-flow',
|
||||
'Preferences__one-third-flow--align-right'
|
||||
)}
|
||||
>
|
||||
{getOSAuthErrorString(authError) ?? i18n('icu:error')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</>
|
||||
<AxoButton.Root
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={isAuthPending}
|
||||
onClick={async () => {
|
||||
if (!isLocalBackupsSetup) {
|
||||
try {
|
||||
setIsAuthPending(true);
|
||||
const result = await promptOSAuth('enable-backups');
|
||||
if (result !== 'success' && result !== 'unsupported') {
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setIsAuthPending(false);
|
||||
}
|
||||
}
|
||||
setSettingsLocation({ page: SettingsPage.LocalBackups });
|
||||
}}
|
||||
>
|
||||
{isLocalBackupsSetup
|
||||
? i18n('icu:Preferences__button--manage')
|
||||
: i18n('icu:Preferences__button--set-up')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</FlowingControl>
|
||||
</SettingsRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -380,22 +365,3 @@ export function renderFreeBackupsSummary({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function getOSAuthErrorString(
|
||||
authError: Omit<PromptOSAuthResultType, 'success'> | undefined
|
||||
): string | undefined {
|
||||
if (!authError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: DESKTOP-8895
|
||||
if (authError === 'unauthorized') {
|
||||
return 'This action could not be completed because system authentication failed. Please try again or open the Signal app on your mobile device and go to Backup Settings to view your backup key.';
|
||||
}
|
||||
|
||||
if (authError === 'unauthorized-no-windows-ucv') {
|
||||
return 'This action could not be completed because Windows Hello is not enabled on your computer. Please set up Windows Hello and try again, or open the Signal app on your mobile device and go to Backup Settings to view your backup key.';
|
||||
}
|
||||
|
||||
return 'The action could not be completed because authentication is not available on this computer. Please open the Signal app on your mobile device and go to Backup Settings to view your backup key.';
|
||||
}
|
||||
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
FlowingSettingsControl as FlowingControl,
|
||||
SettingsRow,
|
||||
} from './PreferencesUtil.dom.js';
|
||||
import { ButtonVariant } from './Button.dom.js';
|
||||
import {
|
||||
getOSAuthErrorString,
|
||||
SIGNAL_BACKUPS_LEARN_MORE_URL,
|
||||
} from './PreferencesBackups.dom.js';
|
||||
import { SIGNAL_BACKUPS_LEARN_MORE_URL } from './PreferencesBackups.dom.js';
|
||||
import { I18n } from './I18n.dom.js';
|
||||
import type { SettingsLocation } from '../types/Nav.std.js';
|
||||
import { SettingsPage } from '../types/Nav.std.js';
|
||||
@@ -33,17 +29,24 @@ import type {
|
||||
PromptOSAuthReasonType,
|
||||
PromptOSAuthResultType,
|
||||
} from '../util/os/promptOSAuthMain.main.js';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.dom.js';
|
||||
import { AxoButton } from '../axo/AxoButton.dom.js';
|
||||
import { AxoDialog } from '../axo/AxoDialog.dom.js';
|
||||
import { AxoCheckbox } from '../axo/AxoCheckbox.dom.js';
|
||||
import { SECOND } from '../util/durations/constants.std.js';
|
||||
import { formatTimestamp } from '../util/formatTimestamp.dom.js';
|
||||
import type { LocalBackupExportMetadata } from '../types/LocalExport.std.js';
|
||||
import { tw } from '../axo/tw.dom.js';
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
import { toLogFormat } from '../types/errors.std.js';
|
||||
import { AxoAlertDialog } from '../axo/AxoAlertDialog.dom.js';
|
||||
|
||||
const { noop } = lodash;
|
||||
const log = createLogger('PreferencesLocalBackups');
|
||||
|
||||
export function PreferencesLocalBackups({
|
||||
accountEntropyPool,
|
||||
backupKeyViewed,
|
||||
disableLocalBackups,
|
||||
i18n,
|
||||
lastLocalBackup,
|
||||
localBackupFolder,
|
||||
@@ -59,6 +62,11 @@ export function PreferencesLocalBackups({
|
||||
}: {
|
||||
accountEntropyPool: string | undefined;
|
||||
backupKeyViewed: boolean;
|
||||
disableLocalBackups: ({
|
||||
deleteExistingBackups,
|
||||
}: {
|
||||
deleteExistingBackups: boolean;
|
||||
}) => Promise<void>;
|
||||
i18n: LocalizerType;
|
||||
lastLocalBackup: LocalBackupExportMetadata | undefined;
|
||||
localBackupFolder: string | undefined;
|
||||
@@ -77,6 +85,7 @@ export function PreferencesLocalBackups({
|
||||
const [authError, setAuthError] =
|
||||
React.useState<Omit<PromptOSAuthResultType, 'success'>>();
|
||||
const [isAuthPending, setIsAuthPending] = useState<boolean>(false);
|
||||
const [isDisablePending, setIsDisablePending] = useState<boolean>(false);
|
||||
|
||||
if (!localBackupFolder) {
|
||||
return (
|
||||
@@ -237,6 +246,28 @@ export function PreferencesLocalBackups({
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</FlowingControl>
|
||||
<FlowingControl>
|
||||
<div className="Preferences__two-thirds-flow">
|
||||
<label>{i18n('icu:Preferences__local-backups-turn-off')}</label>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'Preferences__flow-button',
|
||||
'Preferences__one-third-flow',
|
||||
'Preferences__one-third-flow--align-right'
|
||||
)}
|
||||
>
|
||||
<AxoButton.Root
|
||||
variant="subtle-destructive"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
setIsDisablePending(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:Preferences__local-backups-turn-off-action')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</FlowingControl>
|
||||
</SettingsRow>
|
||||
<SettingsRow className="Preferences--BackupsRow">
|
||||
<div className="Preferences__padding">
|
||||
@@ -252,21 +283,169 @@ export function PreferencesLocalBackups({
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
{authError && (
|
||||
<ConfirmationDialog
|
||||
{isDisablePending ? (
|
||||
<DisableLocalBackupsDialog
|
||||
i18n={i18n}
|
||||
dialogName="PreferencesLocalBackups--ErrorDialog"
|
||||
onClose={() => setAuthError(undefined)}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
cancelText={i18n('icu:ok')}
|
||||
disableLocalBackups={disableLocalBackups}
|
||||
onCancel={() => setIsDisablePending(false)}
|
||||
onComplete={() => {
|
||||
setIsDisablePending(false);
|
||||
setSettingsLocation({ page: SettingsPage.Backups });
|
||||
}}
|
||||
onError={() => {
|
||||
showToast({
|
||||
toastType: ToastType.Error,
|
||||
});
|
||||
setIsDisablePending(false);
|
||||
setSettingsLocation({ page: SettingsPage.Backups });
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{authError ? (
|
||||
<AxoAlertDialog.Root
|
||||
open
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
setAuthError(undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getOSAuthErrorString(authError) ?? i18n('icu:error')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
<AxoAlertDialog.Content escape="cancel-is-noop">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Description>
|
||||
{getOSAuthErrorString(authError) ?? i18n('icu:error')}
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer>
|
||||
<AxoAlertDialog.Cancel>{i18n('icu:ok')}</AxoAlertDialog.Cancel>
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DisableLocalBackupsDialog({
|
||||
i18n,
|
||||
disableLocalBackups,
|
||||
onComplete,
|
||||
onCancel,
|
||||
onError,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
disableLocalBackups: ({
|
||||
deleteExistingBackups,
|
||||
}: {
|
||||
deleteExistingBackups: boolean;
|
||||
}) => Promise<void>;
|
||||
onComplete: () => void;
|
||||
onCancel: () => void;
|
||||
onError: (e: unknown) => void;
|
||||
}) {
|
||||
const [isPending, setIsPending] = useState<boolean>(false);
|
||||
|
||||
const [deleteExistingBackups, setDeleteExistingBackups] =
|
||||
useState<boolean>(true);
|
||||
|
||||
const handleDisableLocalBackups = useCallback(async () => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsPending(true);
|
||||
await disableLocalBackups({ deleteExistingBackups });
|
||||
onComplete();
|
||||
} catch (e) {
|
||||
log.error(
|
||||
'Error when disabling local backups',
|
||||
{ deleteExistingBackups },
|
||||
toLogFormat(e)
|
||||
);
|
||||
onError(e);
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
}, [
|
||||
isPending,
|
||||
deleteExistingBackups,
|
||||
onComplete,
|
||||
onError,
|
||||
disableLocalBackups,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AxoDialog.Root
|
||||
open
|
||||
onOpenChange={(open: boolean) => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AxoDialog.Content
|
||||
size="md"
|
||||
escape={isPending ? 'cancel-is-destructive' : 'cancel-is-noop'}
|
||||
>
|
||||
<div className={tw('p-2')}>
|
||||
<AxoDialog.Header>
|
||||
<AxoDialog.Title>
|
||||
{i18n('icu:Preferences__local-backups-turn-off')}
|
||||
</AxoDialog.Title>
|
||||
</AxoDialog.Header>
|
||||
<AxoDialog.Body padding="normal">
|
||||
<AxoDialog.Description>
|
||||
<div className={tw('mb-2 text-label-secondary')}>
|
||||
{i18n('icu:Preferences__local-backups-turn-off-confirmation')}
|
||||
</div>
|
||||
</AxoDialog.Description>
|
||||
|
||||
<label
|
||||
className={tw('flex items-center gap-3 px-4 py-2.5')}
|
||||
htmlFor="deleteLocalBackupsCheckbox"
|
||||
>
|
||||
<AxoCheckbox.Root
|
||||
id="deleteLocalBackupsCheckbox"
|
||||
variant="square"
|
||||
checked={deleteExistingBackups}
|
||||
disabled={isPending}
|
||||
onCheckedChange={setDeleteExistingBackups}
|
||||
/>
|
||||
{i18n('icu:Preferences__local-backups-turn-off-delete')}
|
||||
</label>
|
||||
</AxoDialog.Body>
|
||||
<AxoDialog.Footer>
|
||||
<AxoDialog.Actions>
|
||||
<AxoDialog.Action
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
disabled={isPending}
|
||||
>
|
||||
{i18n('icu:cancel')}
|
||||
</AxoDialog.Action>
|
||||
<AxoDialog.Action
|
||||
variant="destructive"
|
||||
experimentalSpinner={
|
||||
isPending ? { 'aria-label': i18n('icu:loading') } : null
|
||||
}
|
||||
onClick={handleDisableLocalBackups}
|
||||
>
|
||||
{i18n('icu:Preferences__local-backups-turn-off-action')}
|
||||
</AxoDialog.Action>
|
||||
</AxoDialog.Actions>
|
||||
</AxoDialog.Footer>
|
||||
</div>
|
||||
</AxoDialog.Content>
|
||||
</AxoDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function LocalBackupsSetupFolderPicker({
|
||||
i18n,
|
||||
pickLocalBackupFolder,
|
||||
@@ -525,3 +704,22 @@ function LocalBackupsBackupKeyTextarea({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getOSAuthErrorString(
|
||||
authError: Omit<PromptOSAuthResultType, 'success'> | undefined
|
||||
): string | undefined {
|
||||
if (!authError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: DESKTOP-8895
|
||||
if (authError === 'unauthorized') {
|
||||
return 'This action could not be completed because system authentication failed. Please try again or open the Signal app on your mobile device and go to Backup Settings to view your backup key.';
|
||||
}
|
||||
|
||||
if (authError === 'unauthorized-no-windows-ucv') {
|
||||
return 'This action could not be completed because Windows Hello is not enabled on your computer. Please set up Windows Hello and try again, or open the Signal app on your mobile device and go to Backup Settings to view your backup key.';
|
||||
}
|
||||
|
||||
return 'The action could not be completed because authentication is not available on this computer. Please open the Signal app on your mobile device and go to Backup Settings to view your backup key.';
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { PassThrough } from 'node:stream';
|
||||
import type { Readable, Writable } from 'node:stream';
|
||||
import { createReadStream, createWriteStream } from 'node:fs';
|
||||
import { mkdir, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
||||
import fsExtra, { exists } from 'fs-extra';
|
||||
import { join } from 'node:path';
|
||||
import fsExtra from 'fs-extra';
|
||||
import { basename, join } from 'node:path';
|
||||
import { createGzip, createGunzip } from 'node:zlib';
|
||||
import { createCipheriv, createHmac, randomBytes } from 'node:crypto';
|
||||
import lodash from 'lodash';
|
||||
@@ -94,6 +94,7 @@ import {
|
||||
validateLocalBackupStructure,
|
||||
getLocalBackupFilesDirectory,
|
||||
getLocalBackupSnapshotDirectory,
|
||||
LOCAL_BACKUP_DIR_NAME,
|
||||
} from './util/localBackup.node.js';
|
||||
import {
|
||||
AttachmentPermanentlyMissingError,
|
||||
@@ -118,7 +119,7 @@ import {
|
||||
import { getFreeDiskSpace } from '../../util/getFreeDiskSpace.node.js';
|
||||
import { isFeaturedEnabledNoRedux } from '../../util/isFeatureEnabled.dom.js';
|
||||
|
||||
const { ensureFile } = fsExtra;
|
||||
const { ensureFile, exists } = fsExtra;
|
||||
|
||||
const { throttle } = lodashFp;
|
||||
|
||||
@@ -1443,13 +1444,44 @@ export class BackupsService {
|
||||
return;
|
||||
}
|
||||
|
||||
const localBackupsBaseDir = join(backupsParentDir, 'SignalBackups');
|
||||
const localBackupsBaseDir = join(backupsParentDir, LOCAL_BACKUP_DIR_NAME);
|
||||
|
||||
await mkdir(localBackupsBaseDir, { recursive: true });
|
||||
|
||||
await itemStorage.put('localBackupFolder', localBackupsBaseDir);
|
||||
return localBackupsBaseDir;
|
||||
}
|
||||
|
||||
async disableLocalBackups({
|
||||
deleteExistingBackups,
|
||||
}: {
|
||||
deleteExistingBackups: boolean;
|
||||
}): Promise<void> {
|
||||
const backupsBaseDir = itemStorage.get('localBackupFolder');
|
||||
|
||||
await Promise.all([
|
||||
itemStorage.remove('lastLocalBackup'),
|
||||
itemStorage.remove('localBackupFolder'),
|
||||
itemStorage.remove('backupKeyViewed'),
|
||||
]);
|
||||
|
||||
if (deleteExistingBackups) {
|
||||
if (!backupsBaseDir) {
|
||||
log.error('disableLocalBackups: backups dir not set');
|
||||
return;
|
||||
}
|
||||
|
||||
if (basename(backupsBaseDir) !== LOCAL_BACKUP_DIR_NAME) {
|
||||
log.warn(
|
||||
'disableLocalBackups: backups dir does not have expected name, bailing on deleting backups'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await rm(backupsBaseDir, { force: true, recursive: true });
|
||||
log.info('disableLocalBackups: deleted backups directory');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const backupsService = new BackupsService();
|
||||
|
||||
@@ -28,6 +28,8 @@ const LOCAL_BACKUP_SNAPSHOT_DIR_PREFIX = 'signal-backup-';
|
||||
const LOCAL_BACKUP_SNAPSHOT_DIR_PATTERN =
|
||||
/^signal-backup-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}$/;
|
||||
|
||||
export const LOCAL_BACKUP_DIR_NAME = 'SignalBackups';
|
||||
|
||||
export function getLocalBackupSnapshotDirectory(
|
||||
backupsBaseDir: string,
|
||||
timestamp: number
|
||||
@@ -55,7 +57,14 @@ export async function getAllPathsInLocalBackupFilesDirectory({
|
||||
const allEntries = await readdir(filesDir, {
|
||||
withFileTypes: true,
|
||||
recursive: true,
|
||||
}).catch(error => {
|
||||
// Be resilient to files folder not exist
|
||||
if ('code' in error && error.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
return allEntries
|
||||
.filter(entry => entry.isFile())
|
||||
.map(entry => join(entry.parentPath, entry.name));
|
||||
@@ -105,7 +114,7 @@ export async function pruneLocalBackups({
|
||||
if (snapshotDirsToDelete.length === 1) {
|
||||
fnLog.info('pruning one snapshot');
|
||||
} else {
|
||||
fnLog.error(`pruning ${snapshotDirsToDelete.length} snapshots`);
|
||||
fnLog.warn(`pruning ${snapshotDirsToDelete.length} snapshots`);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -829,6 +829,7 @@ export function SmartPreferences(): React.JSX.Element | null {
|
||||
customColors={customColors}
|
||||
defaultConversationColor={defaultConversationColor}
|
||||
deviceName={deviceName}
|
||||
disableLocalBackups={backupsService.disableLocalBackups}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault}
|
||||
phoneNumber={phoneNumber}
|
||||
doDeleteAllData={doDeleteAllData}
|
||||
|
||||
Reference in New Issue
Block a user