Disable storage service while importing backup

This commit is contained in:
Fedor Indutny 2024-10-02 06:36:19 -07:00 committed by GitHub
parent 17c908bbf4
commit a527b88867
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 147 additions and 132 deletions

View file

@ -736,6 +736,7 @@ export async function startApp(): Promise<void> {
);
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<void> {
});
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<void> {
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<void> {
resolveOnAppView = undefined;
}
afterStart();
drop(afterStart());
}
const backupReady = explodePromise<void>();
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<void> {
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<void> {
);
}
let backupReady = explodePromise<void>();
let connectCount = 0;
let connectPromise: ExplodePromiseResultType<void> | undefined;
let remotelyExpired = false;
@ -1727,7 +1701,14 @@ export async function startApp(): Promise<void> {
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<void> {
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();
}

View file

@ -76,6 +76,25 @@ export class BackupsService {
});
}
public async download(
options: Omit<DownloadOptionsType, 'downloadOffset'>
): Promise<void> {
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<void> {
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<DownloadOptionsType, 'downloadOffset'>
): Promise<boolean> {
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<DownloadOptionsType, 'downloadOffset'>
): Promise<boolean> {
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,

View file

@ -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<void> {

View file

@ -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;