diff --git a/ts/background.ts b/ts/background.ts index 55c627f874d3..f1dcd7ed842c 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -736,6 +736,7 @@ export async function startApp(): Promise { ); log.info('background/shutdown: shutting down messageReceiver'); server.unregisterRequestHandler(messageReceiver); + StorageService.disableStorageService(); messageReceiver.stopProcessing(); await window.waitForAllBatchers(); } @@ -1312,12 +1313,8 @@ export async function startApp(): Promise { }); async function runStorageService({ reason }: { reason: string }) { - if (window.storage.get('backupDownloadPath')) { - log.info( - 'background: not running storage service while downloading backup' - ); - return; - } + await backupReady.promise; + StorageService.enableStorageService(); StorageService.runStorageServiceSyncJob({ reason: `runStorageService/${reason}`, @@ -1431,10 +1428,7 @@ export async function startApp(): Promise { drop(connect(true)); // Connect messageReceiver back to websocket - afterStart(); - - // Run storage service after linking - drop(runStorageService({ reason: 'background/registration_done' })); + drop(afterStart()); }); cancelInitializationMessage(); @@ -1519,12 +1513,10 @@ export async function startApp(): Promise { resolveOnAppView = undefined; } - afterStart(); + drop(afterStart()); } - const backupReady = explodePromise(); - - function afterStart() { + async function afterStart() { strictAssert(messageReceiver, 'messageReceiver must be initialized'); strictAssert(server, 'server must be initialized'); @@ -1590,45 +1582,25 @@ export async function startApp(): Promise { onOffline(); } - drop(downloadBackup()); - } + // Download backup before enabling request handler and storage service + try { + await backupsService.download({ + onProgress: (currentBytes, totalBytes) => { + window.reduxActions.installer.updateBackupImportProgress({ + currentBytes, + totalBytes, + }); + }, + }); - async function downloadBackup() { - strictAssert(server != null, 'server must be initialized'); - strictAssert( - messageReceiver != null, - 'MessageReceiver must be initialized' - ); - - const backupDownloadPath = window.storage.get('backupDownloadPath'); - if (!backupDownloadPath) { - log.warn('downloadBackup: no backup download path, skipping'); backupReady.resolve(); - server.registerRequestHandler(messageReceiver); - drop(runStorageService({ reason: 'downloadBackup/noPath' })); - return; + } catch (error) { + backupReady.reject(error); + throw error; } - const absoluteDownloadPath = - window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath); - log.info('downloadBackup: downloading...'); - const hasBackup = await backupsService.download(absoluteDownloadPath, { - onProgress: (currentBytes, totalBytes) => { - window.reduxActions.installer.updateBackupImportProgress({ - currentBytes, - totalBytes, - }); - }, - }); - await window.storage.remove('backupDownloadPath'); - - log.info(`downloadBackup: done, had backup=${hasBackup}`); - - // Start storage service sync, etc - log.info('downloadBackup: processing websocket messages, storage service'); - backupReady.resolve(); server.registerRequestHandler(messageReceiver); - drop(runStorageService({ reason: 'downloadBackup/complete' })); + drop(runStorageService({ reason: 'afterStart' })); } window.getSyncRequest = (timeoutMillis?: number) => { @@ -1693,6 +1665,8 @@ export async function startApp(): Promise { ); } + let backupReady = explodePromise(); + let connectCount = 0; let connectPromise: ExplodePromiseResultType | undefined; let remotelyExpired = false; @@ -1727,7 +1701,14 @@ export async function startApp(): Promise { strictAssert(server !== undefined, 'WebAPI not connected'); - await backupReady.promise; + // Wait for backup to be downloaded + try { + await backupReady.promise; + } catch (error) { + log.error('background: backup download failed, not reconnecting', error); + return; + } + log.info('background: connect unblocked by backups'); try { connectPromise = explodePromise(); @@ -3039,8 +3020,12 @@ export async function startApp(): Promise { log.info('unlinkAndDisconnect: logging out'); strictAssert(server !== undefined, 'WebAPI not initialized'); server.unregisterRequestHandler(messageReceiver); + StorageService.disableStorageService(); messageReceiver.stopProcessing(); + backupReady.reject(new Error('Aborted')); + backupReady = explodePromise(); + await server.logout(); await window.waitForAllBatchers(); } diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index fa2f58c7a26e..edc680a7a69d 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -76,6 +76,25 @@ export class BackupsService { }); } + public async download( + options: Omit + ): Promise { + const backupDownloadPath = window.storage.get('backupDownloadPath'); + if (!backupDownloadPath) { + log.warn('backups.download: no backup download path, skipping'); + return; + } + + const absoluteDownloadPath = + window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath); + log.info('backups.download: downloading...'); + const hasBackup = await this.doDownload(absoluteDownloadPath, options); + + await window.storage.remove('backupDownloadPath'); + + log.info(`backups.download: done, had backup=${hasBackup}`); + } + public async upload(): Promise { const fileName = `backup-${randomBytes(32).toString('hex')}`; const filePath = join(window.BasePaths.temp, fileName); @@ -155,86 +174,6 @@ export class BackupsService { } } - public async download( - downloadPath: string, - { onProgress }: Omit - ): Promise { - const controller = new AbortController(); - - // Abort previous download - this.downloadController?.abort(); - this.downloadController = controller; - - let downloadOffset = 0; - try { - ({ size: downloadOffset } = await stat(downloadPath)); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - - // File is missing - start from the beginning - } - - try { - await ensureFile(downloadPath); - - if (controller.signal.aborted) { - return false; - } - - const stream = await this.api.download({ - downloadOffset, - onProgress, - abortSignal: controller.signal, - }); - - if (controller.signal.aborted) { - return false; - } - - await pipeline( - stream, - createWriteStream(downloadPath, { - flags: 'a', - start: downloadOffset, - }) - ); - - if (controller.signal.aborted) { - return false; - } - - this.downloadController = undefined; - - // Too late to cancel now - try { - await this.importFromDisk(downloadPath); - } finally { - await unlink(downloadPath); - } - } catch (error) { - // Download canceled - if (error.name === 'AbortError') { - return false; - } - - // No backup on the server - if (error instanceof HTTPError && error.code === 404) { - return false; - } - - try { - await unlink(downloadPath); - } catch { - // Best-effort - } - throw error; - } - - return true; - } - public async importBackup( createBackupStream: () => Readable, backupType = BackupType.Ciphertext @@ -357,6 +296,86 @@ export class BackupsService { return { isInBackupTier: true, cdnNumber: storedInfo.cdnNumber }; } + private async doDownload( + downloadPath: string, + { onProgress }: Omit + ): Promise { + const controller = new AbortController(); + + // Abort previous download + this.downloadController?.abort(); + this.downloadController = controller; + + let downloadOffset = 0; + try { + ({ size: downloadOffset } = await stat(downloadPath)); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + + // File is missing - start from the beginning + } + + try { + await ensureFile(downloadPath); + + if (controller.signal.aborted) { + return false; + } + + const stream = await this.api.download({ + downloadOffset, + onProgress, + abortSignal: controller.signal, + }); + + if (controller.signal.aborted) { + return false; + } + + await pipeline( + stream, + createWriteStream(downloadPath, { + flags: 'a', + start: downloadOffset, + }) + ); + + if (controller.signal.aborted) { + return false; + } + + this.downloadController = undefined; + + // Too late to cancel now + try { + await this.importFromDisk(downloadPath); + } finally { + await unlink(downloadPath); + } + } catch (error) { + // Download canceled + if (error.name === 'AbortError') { + return false; + } + + // No backup on the server + if (error instanceof HTTPError && error.code === 404) { + return false; + } + + try { + await unlink(downloadPath); + } catch { + // Best-effort + } + throw error; + } + + return true; + } + private async exportBackup( sink: Writable, backupLevel: BackupLevel = BackupLevel.Messages, diff --git a/ts/services/storage.ts b/ts/services/storage.ts index dc8111096280..35a8d580cb5c 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -2048,6 +2048,10 @@ export function enableStorageService(): void { storageServiceEnabled = true; } +export function disableStorageService(): void { + storageServiceEnabled = false; +} + export async function eraseAllStorageServiceState({ keepUnknownFields = false, }: { keepUnknownFields?: boolean } = {}): Promise { diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index e6a13ff20b96..b6213957fe20 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -952,6 +952,7 @@ export default class AccountManager extends EventTarget { const numberChanged = !previousACI && previousNumber && previousNumber !== number; + let cleanStart = !previousACI && !previousPNI && !previousNumber; if (uuidChanged || numberChanged || backupFile !== undefined) { if (uuidChanged) { log.warn( @@ -973,6 +974,8 @@ export default class AccountManager extends EventTarget { try { await storage.protocol.removeAllData(); log.info('createAccount: Successfully deleted previous data'); + + cleanStart = true; } catch (error) { log.error( 'Something went wrong deleting data from previous number', @@ -1091,6 +1094,13 @@ export default class AccountManager extends EventTarget { throw missingCaseError(options); } + // Set backup download path before storing credentials to ensure that + // storage service and message receiver are not operating + // until the backup is downloaded and imported. + if (isBackupEnabled() && cleanStart) { + await storage.put('backupDownloadPath', getRelativePath(createName())); + } + // `setCredentials` needs to be called // before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes` // indirectly calls `ConversationController.getConversationId()` which @@ -1163,9 +1173,6 @@ export default class AccountManager extends EventTarget { const regionCode = getRegionCodeForNumber(number); await storage.put('regionCode', regionCode); - if (isBackupEnabled()) { - await storage.put('backupDownloadPath', getRelativePath(createName())); - } await storage.protocol.hydrateCaches(); const store = storage.protocol;