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'); log.info('background/shutdown: shutting down messageReceiver');
server.unregisterRequestHandler(messageReceiver); server.unregisterRequestHandler(messageReceiver);
StorageService.disableStorageService();
messageReceiver.stopProcessing(); messageReceiver.stopProcessing();
await window.waitForAllBatchers(); await window.waitForAllBatchers();
} }
@ -1312,12 +1313,8 @@ export async function startApp(): Promise<void> {
}); });
async function runStorageService({ reason }: { reason: string }) { async function runStorageService({ reason }: { reason: string }) {
if (window.storage.get('backupDownloadPath')) { await backupReady.promise;
log.info(
'background: not running storage service while downloading backup'
);
return;
}
StorageService.enableStorageService(); StorageService.enableStorageService();
StorageService.runStorageServiceSyncJob({ StorageService.runStorageServiceSyncJob({
reason: `runStorageService/${reason}`, reason: `runStorageService/${reason}`,
@ -1431,10 +1428,7 @@ export async function startApp(): Promise<void> {
drop(connect(true)); drop(connect(true));
// Connect messageReceiver back to websocket // Connect messageReceiver back to websocket
afterStart(); drop(afterStart());
// Run storage service after linking
drop(runStorageService({ reason: 'background/registration_done' }));
}); });
cancelInitializationMessage(); cancelInitializationMessage();
@ -1519,12 +1513,10 @@ export async function startApp(): Promise<void> {
resolveOnAppView = undefined; resolveOnAppView = undefined;
} }
afterStart(); drop(afterStart());
} }
const backupReady = explodePromise<void>(); async function afterStart() {
function afterStart() {
strictAssert(messageReceiver, 'messageReceiver must be initialized'); strictAssert(messageReceiver, 'messageReceiver must be initialized');
strictAssert(server, 'server must be initialized'); strictAssert(server, 'server must be initialized');
@ -1590,29 +1582,9 @@ export async function startApp(): Promise<void> {
onOffline(); onOffline();
} }
drop(downloadBackup()); // Download backup before enabling request handler and storage service
} try {
await backupsService.download({
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;
}
const absoluteDownloadPath =
window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath);
log.info('downloadBackup: downloading...');
const hasBackup = await backupsService.download(absoluteDownloadPath, {
onProgress: (currentBytes, totalBytes) => { onProgress: (currentBytes, totalBytes) => {
window.reduxActions.installer.updateBackupImportProgress({ window.reduxActions.installer.updateBackupImportProgress({
currentBytes, currentBytes,
@ -1620,15 +1592,15 @@ export async function startApp(): Promise<void> {
}); });
}, },
}); });
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(); backupReady.resolve();
} catch (error) {
backupReady.reject(error);
throw error;
}
server.registerRequestHandler(messageReceiver); server.registerRequestHandler(messageReceiver);
drop(runStorageService({ reason: 'downloadBackup/complete' })); drop(runStorageService({ reason: 'afterStart' }));
} }
window.getSyncRequest = (timeoutMillis?: number) => { window.getSyncRequest = (timeoutMillis?: number) => {
@ -1693,6 +1665,8 @@ export async function startApp(): Promise<void> {
); );
} }
let backupReady = explodePromise<void>();
let connectCount = 0; let connectCount = 0;
let connectPromise: ExplodePromiseResultType<void> | undefined; let connectPromise: ExplodePromiseResultType<void> | undefined;
let remotelyExpired = false; let remotelyExpired = false;
@ -1727,7 +1701,14 @@ export async function startApp(): Promise<void> {
strictAssert(server !== undefined, 'WebAPI not connected'); strictAssert(server !== undefined, 'WebAPI not connected');
// Wait for backup to be downloaded
try {
await backupReady.promise; await backupReady.promise;
} catch (error) {
log.error('background: backup download failed, not reconnecting', error);
return;
}
log.info('background: connect unblocked by backups');
try { try {
connectPromise = explodePromise(); connectPromise = explodePromise();
@ -3039,8 +3020,12 @@ export async function startApp(): Promise<void> {
log.info('unlinkAndDisconnect: logging out'); log.info('unlinkAndDisconnect: logging out');
strictAssert(server !== undefined, 'WebAPI not initialized'); strictAssert(server !== undefined, 'WebAPI not initialized');
server.unregisterRequestHandler(messageReceiver); server.unregisterRequestHandler(messageReceiver);
StorageService.disableStorageService();
messageReceiver.stopProcessing(); messageReceiver.stopProcessing();
backupReady.reject(new Error('Aborted'));
backupReady = explodePromise();
await server.logout(); await server.logout();
await window.waitForAllBatchers(); 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> { public async upload(): Promise<void> {
const fileName = `backup-${randomBytes(32).toString('hex')}`; const fileName = `backup-${randomBytes(32).toString('hex')}`;
const filePath = join(window.BasePaths.temp, fileName); 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( public async importBackup(
createBackupStream: () => Readable, createBackupStream: () => Readable,
backupType = BackupType.Ciphertext backupType = BackupType.Ciphertext
@ -357,6 +296,86 @@ export class BackupsService {
return { isInBackupTier: true, cdnNumber: storedInfo.cdnNumber }; 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( private async exportBackup(
sink: Writable, sink: Writable,
backupLevel: BackupLevel = BackupLevel.Messages, backupLevel: BackupLevel = BackupLevel.Messages,

View file

@ -2048,6 +2048,10 @@ export function enableStorageService(): void {
storageServiceEnabled = true; storageServiceEnabled = true;
} }
export function disableStorageService(): void {
storageServiceEnabled = false;
}
export async function eraseAllStorageServiceState({ export async function eraseAllStorageServiceState({
keepUnknownFields = false, keepUnknownFields = false,
}: { keepUnknownFields?: boolean } = {}): Promise<void> { }: { keepUnknownFields?: boolean } = {}): Promise<void> {

View file

@ -952,6 +952,7 @@ export default class AccountManager extends EventTarget {
const numberChanged = const numberChanged =
!previousACI && previousNumber && previousNumber !== number; !previousACI && previousNumber && previousNumber !== number;
let cleanStart = !previousACI && !previousPNI && !previousNumber;
if (uuidChanged || numberChanged || backupFile !== undefined) { if (uuidChanged || numberChanged || backupFile !== undefined) {
if (uuidChanged) { if (uuidChanged) {
log.warn( log.warn(
@ -973,6 +974,8 @@ export default class AccountManager extends EventTarget {
try { try {
await storage.protocol.removeAllData(); await storage.protocol.removeAllData();
log.info('createAccount: Successfully deleted previous data'); log.info('createAccount: Successfully deleted previous data');
cleanStart = true;
} catch (error) { } catch (error) {
log.error( log.error(
'Something went wrong deleting data from previous number', 'Something went wrong deleting data from previous number',
@ -1091,6 +1094,13 @@ export default class AccountManager extends EventTarget {
throw missingCaseError(options); 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 // `setCredentials` needs to be called
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes` // before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
// indirectly calls `ConversationController.getConversationId()` which // indirectly calls `ConversationController.getConversationId()` which
@ -1163,9 +1173,6 @@ export default class AccountManager extends EventTarget {
const regionCode = getRegionCodeForNumber(number); const regionCode = getRegionCodeForNumber(number);
await storage.put('regionCode', regionCode); await storage.put('regionCode', regionCode);
if (isBackupEnabled()) {
await storage.put('backupDownloadPath', getRelativePath(createName()));
}
await storage.protocol.hydrateCaches(); await storage.protocol.hydrateCaches();
const store = storage.protocol; const store = storage.protocol;