Disable storage service while importing backup
This commit is contained in:
parent
17c908bbf4
commit
a527b88867
4 changed files with 147 additions and 132 deletions
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue