From 5c350a0e3c1e4571de207da83279e4824e1bf92e Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:22:48 -0700 Subject: [PATCH] Download backup on link --- ts/background.ts | 6 ++++++ ts/services/backups/api.ts | 13 +++++++++++++ ts/services/backups/index.ts | 17 ++++++++++++++++- ts/textsecure/WebAPI.ts | 25 ++++++++++++++++++++++++- ts/util/isBackupEnabled.ts | 8 ++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/ts/background.ts b/ts/background.ts index 04614fd06..19244f689 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1482,6 +1482,12 @@ export async function startApp(): Promise { ) ); + // Now that we authenticated - time to download the backup! + if (isBackupEnabled()) { + backupsService.start(); + drop(backupsService.download()); + } + // Cancel throttled calls to refreshRemoteConfig since our auth changed. window.Signal.RemoteConfig.maybeRefreshRemoteConfig.cancel(); drop(window.Signal.RemoteConfig.maybeRefreshRemoteConfig(server)); diff --git a/ts/services/backups/api.ts b/ts/services/backups/api.ts index 8e6bcb1a3..5ee4ab4f5 100644 --- a/ts/services/backups/api.ts +++ b/ts/services/backups/api.ts @@ -1,6 +1,7 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { type Readable } from 'node:stream'; import { strictAssert } from '../../util/assert'; import type { WebAPIType, @@ -66,6 +67,18 @@ export class BackupAPI { }); } + public async download(): Promise { + const { cdn, backupDir, backupName } = await this.getInfo(); + const { headers } = await this.credentials.getCDNReadCredentials(cdn); + + return this.server.getBackupStream({ + cdn, + backupDir, + backupName, + headers, + }); + } + public async getMediaUploadForm(): Promise { return this.server.getBackupMediaUploadForm( await this.credentials.getHeadersForToday() diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 158ef97ef..537d467b1 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -129,6 +129,21 @@ export class BackupsService { return backupsService.importBackup(() => createReadStream(backupFile)); } + public async download(): Promise { + const path = window.Signal.Migrations.getAbsoluteTempPath( + randomBytes(32).toString('hex') + ); + + const stream = await this.api.download(); + await pipeline(stream, createWriteStream(path)); + + try { + await this.importFromDisk(path); + } finally { + await unlink(path); + } + } + public async importBackup(createBackupStream: () => Readable): Promise { strictAssert(!this.isRunning, 'BackupService is already running'); @@ -281,7 +296,7 @@ export class BackupsService { await this.api.refresh(); log.info('Backup: refreshed'); } catch (error) { - log.error('Backup: periodic refresh kufailed', Errors.toLogFormat(error)); + log.error('Backup: periodic refresh failed', Errors.toLogFormat(error)); } } } diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index bba19b624..430e87167 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -1147,8 +1147,15 @@ export type GetBackupCDNCredentialsResponseType = z.infer< typeof getBackupCDNCredentialsResponseSchema >; +export type GetBackupStreamOptionsType = Readonly<{ + cdn: number; + backupDir: string; + backupName: string; + headers: Record; +}>; + export const getBackupInfoResponseSchema = z.object({ - cdn: z.number(), + cdn: z.literal(3), backupDir: z.string(), mediaDir: z.string(), backupName: z.string(), @@ -1380,6 +1387,7 @@ export type WebAPIType = { getBackupInfo: ( headers: BackupPresentationHeadersType ) => Promise; + getBackupStream: (options: GetBackupStreamOptionsType) => Promise; getBackupUploadForm: ( headers: BackupPresentationHeadersType ) => Promise; @@ -1707,6 +1715,7 @@ export function initialize({ getBackupCredentials, getBackupCDNCredentials, getBackupInfo, + getBackupStream, getBackupMediaUploadForm, getBackupUploadForm, getBadgeImageFile, @@ -2764,6 +2773,20 @@ export function initialize({ return getBackupInfoResponseSchema.parse(res); } + async function getBackupStream({ + headers, + cdn, + backupDir, + backupName, + }: GetBackupStreamOptionsType): Promise { + return _getAttachment({ + cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`, + cdnNumber: cdn, + redactor: _createRedactor(backupDir, backupName), + headers, + }); + } + async function getBackupMediaUploadForm( headers: BackupPresentationHeadersType ) { diff --git a/ts/util/isBackupEnabled.ts b/ts/util/isBackupEnabled.ts index 3a4ddce1d..83ce52948 100644 --- a/ts/util/isBackupEnabled.ts +++ b/ts/util/isBackupEnabled.ts @@ -2,7 +2,15 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as RemoteConfig from '../RemoteConfig'; +import { Environment, getEnvironment } from '../environment'; +import { isStaging } from './version'; export function isBackupEnabled(): boolean { + if (getEnvironment() === Environment.Staging) { + return true; + } + if (isStaging(window.getVersion())) { + return true; + } return Boolean(RemoteConfig.isEnabled('desktop.backup.credentialFetch')); }