mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Make link-and-sync downloads resumable
This commit is contained in:
+21
-22
@@ -11,12 +11,11 @@ import type {
|
||||
BackupMediaItemType,
|
||||
BackupMediaBatchResponseType,
|
||||
BackupListMediaResponseType,
|
||||
TransferArchiveType,
|
||||
} from '../../textsecure/WebAPI';
|
||||
import type { BackupCredentials } from './credentials';
|
||||
import { BackupCredentialType } from '../../types/backups';
|
||||
import { uploadFile } from '../../util/uploadAttachment';
|
||||
import { ContinueWithoutSyncingError, RelinkRequestedError } from './errors';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
export type DownloadOptionsType = Readonly<{
|
||||
downloadOffset: number;
|
||||
@@ -24,6 +23,14 @@ export type DownloadOptionsType = Readonly<{
|
||||
abortSignal?: AbortSignal;
|
||||
}>;
|
||||
|
||||
export type EphemeralDownloadOptionsType = Readonly<{
|
||||
archive: Readonly<{
|
||||
cdn: number;
|
||||
key: string;
|
||||
}>;
|
||||
}> &
|
||||
DownloadOptionsType;
|
||||
|
||||
export class BackupAPI {
|
||||
#cachedBackupInfo = new Map<
|
||||
BackupCredentialType,
|
||||
@@ -106,31 +113,23 @@ export class BackupAPI {
|
||||
});
|
||||
}
|
||||
|
||||
public async getTransferArchive(
|
||||
abortSignal: AbortSignal
|
||||
): Promise<TransferArchiveType> {
|
||||
return this.#server.getTransferArchive({
|
||||
abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
public async downloadEphemeral({
|
||||
archive,
|
||||
downloadOffset,
|
||||
onProgress,
|
||||
abortSignal,
|
||||
}: DownloadOptionsType): Promise<Readable> {
|
||||
const response = await this.#server.getTransferArchive({
|
||||
abortSignal,
|
||||
});
|
||||
|
||||
if ('error' in response) {
|
||||
switch (response.error) {
|
||||
case 'RELINK_REQUESTED':
|
||||
throw new RelinkRequestedError();
|
||||
case 'CONTINUE_WITHOUT_UPLOAD':
|
||||
throw new ContinueWithoutSyncingError();
|
||||
default:
|
||||
throw missingCaseError(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
const { cdn, key } = response;
|
||||
|
||||
}: EphemeralDownloadOptionsType): Promise<Readable> {
|
||||
return this.#server.getEphemeralBackupStream({
|
||||
cdn,
|
||||
key,
|
||||
cdn: archive.cdn,
|
||||
key: archive.key,
|
||||
downloadOffset,
|
||||
onProgress,
|
||||
abortSignal,
|
||||
|
||||
@@ -17,5 +17,3 @@ export class BackupProcessingError extends Error {}
|
||||
export class BackupImportCanceledError extends Error {}
|
||||
|
||||
export class RelinkRequestedError extends Error {}
|
||||
|
||||
export class ContinueWithoutSyncingError extends Error {}
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
BackupDownloadFailedError,
|
||||
BackupImportCanceledError,
|
||||
BackupProcessingError,
|
||||
ContinueWithoutSyncingError,
|
||||
RelinkRequestedError,
|
||||
UnsupportedBackupVersion,
|
||||
} from './errors';
|
||||
@@ -227,6 +226,7 @@ export class BackupsService {
|
||||
|
||||
await window.storage.remove('backupDownloadPath');
|
||||
await window.storage.remove('backupEphemeralKey');
|
||||
await window.storage.remove('backupTransitArchive');
|
||||
await window.storage.put('isRestoredFromBackup', hasBackup);
|
||||
|
||||
// If the primary cancels sync on their end, then we can link without sync
|
||||
@@ -577,62 +577,77 @@ export class BackupsService {
|
||||
onProgress?.(InstallScreenBackupStep.Download, currentBytes, totalBytes);
|
||||
};
|
||||
|
||||
await ensureFile(downloadPath);
|
||||
if (controller.signal.aborted) {
|
||||
throw new BackupImportCanceledError();
|
||||
}
|
||||
|
||||
let stream: Readable;
|
||||
|
||||
try {
|
||||
await ensureFile(downloadPath);
|
||||
if (ephemeralKey == null) {
|
||||
stream = await this.api.download({
|
||||
downloadOffset,
|
||||
onProgress: onDownloadProgress,
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
} else {
|
||||
let archive = window.storage.get('backupTransitArchive');
|
||||
if (archive == null) {
|
||||
const response = await this.api.getTransferArchive(controller.signal);
|
||||
|
||||
if ('error' in response) {
|
||||
switch (response.error) {
|
||||
case 'RELINK_REQUESTED':
|
||||
throw new RelinkRequestedError();
|
||||
|
||||
// Primary decided to abort syncing process; continue on with no backup
|
||||
case 'CONTINUE_WITHOUT_UPLOAD':
|
||||
log.error(
|
||||
'backups.doDownloadAndImport: primary requested to continue without syncing'
|
||||
);
|
||||
return false;
|
||||
default:
|
||||
throw missingCaseError(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
archive = {
|
||||
cdn: response.cdn,
|
||||
key: response.key,
|
||||
};
|
||||
await window.storage.put('backupTransitArchive', archive);
|
||||
}
|
||||
|
||||
stream = await this.api.downloadEphemeral({
|
||||
archive,
|
||||
downloadOffset,
|
||||
onProgress: onDownloadProgress,
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
throw new BackupImportCanceledError();
|
||||
}
|
||||
|
||||
let stream: Readable;
|
||||
try {
|
||||
if (ephemeralKey == null) {
|
||||
stream = await this.api.download({
|
||||
downloadOffset,
|
||||
onProgress: onDownloadProgress,
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
} else {
|
||||
stream = await this.api.downloadEphemeral({
|
||||
downloadOffset,
|
||||
onProgress: onDownloadProgress,
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
throw new BackupImportCanceledError();
|
||||
}
|
||||
|
||||
// No backup on the server
|
||||
if (error instanceof HTTPError && error.code === 404) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Primary decided to abort syncing process; continue on with no backup
|
||||
if (error instanceof ContinueWithoutSyncingError) {
|
||||
log.error(
|
||||
'backups.doDownloadAndImport: primary requested to continue without syncing'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Primary wants to try link & sync again
|
||||
if (error instanceof RelinkRequestedError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
log.error(
|
||||
'backups.doDownloadAndImport: error downloading backup file',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
throw new BackupDownloadFailedError();
|
||||
// No backup on the server
|
||||
if (error instanceof HTTPError && error.code === 404) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (controller.signal.aborted) {
|
||||
throw new BackupImportCanceledError();
|
||||
}
|
||||
log.error(
|
||||
'backups.doDownloadAndImport: error downloading backup file',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
throw new BackupDownloadFailedError();
|
||||
}
|
||||
|
||||
if (controller.signal.aborted) {
|
||||
throw new BackupImportCanceledError();
|
||||
}
|
||||
|
||||
try {
|
||||
await pipeline(
|
||||
stream,
|
||||
createWriteStream(downloadPath, {
|
||||
|
||||
Vendored
+6
@@ -205,6 +205,12 @@ export type StorageAccessType = {
|
||||
// link-and-sync backup
|
||||
backupEphemeralKey: Uint8Array;
|
||||
|
||||
// If present - we are resuming the download of known transfer archive
|
||||
backupTransitArchive: {
|
||||
cdn: number;
|
||||
key: string;
|
||||
};
|
||||
|
||||
// If true Desktop message history was restored from backup
|
||||
isRestoredFromBackup: boolean;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user