diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index cafb3f612..de4ba5fb7 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -4,10 +4,8 @@ BuildDetails - CarthageVersion - 0.31.2 OSXVersion - 10.14.1 + 10.13.6 WebRTCCommit ca71024b4993ba95e3e6b8d0758004cffc54ddaf M70 diff --git a/Signal/src/util/Backup/OWSBackup.m b/Signal/src/util/Backup/OWSBackup.m index 6e37a79f6..c99779a54 100644 --- a/Signal/src/util/Backup/OWSBackup.m +++ b/Signal/src/util/Backup/OWSBackup.m @@ -763,22 +763,20 @@ NSError *OWSBackupErrorWithDescription(NSString *description) return attachmentIds; } -- (void)lazyRestoreAttachment:(TSAttachmentPointer *)attachment - backupIO:(OWSBackupIO *)backupIO - completion:(OWSBackupBoolBlock)completion +- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO { OWSAssertDebug(attachment); OWSAssertDebug(backupIO); - OWSAssertDebug(completion); OWSBackupFragment *_Nullable lazyRestoreFragment = attachment.lazyRestoreFragment; if (!lazyRestoreFragment) { - OWSLogWarn(@"Attachment missing lazy restore metadata."); - return completion(NO); + OWSLogError(@"Attachment missing lazy restore metadata."); + return + [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment missing lazy restore metadata.")]; } if (lazyRestoreFragment.recordName.length < 1 || lazyRestoreFragment.encryptionKey.length < 1) { OWSLogError(@"Incomplete lazy restore metadata."); - return completion(NO); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Incomplete lazy restore metadata.")]; } // Use a predictable file path so that multiple "import backup" attempts @@ -787,40 +785,30 @@ NSError *OWSBackupErrorWithDescription(NSString *description) // TODO: This will also require imports using a predictable jobTempDirPath. NSString *tempFilePath = [backupIO generateTempFilePath]; - [OWSBackupAPI downloadFileFromCloudWithRecordName:lazyRestoreFragment.recordName - toFileUrl:[NSURL fileURLWithPath:tempFilePath] - success:^{ - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self lazyRestoreAttachment:attachment - backupIO:backupIO - encryptedFilePath:tempFilePath - encryptionKey:lazyRestoreFragment.encryptionKey - completion:completion]; - }); - } - failure:^(NSError *error) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - completion(NO); - }); - }]; + return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:lazyRestoreFragment.recordName + toFileUrl:[NSURL fileURLWithPath:tempFilePath]] + .thenInBackground(^{ + return [self lazyRestoreAttachment:attachment + backupIO:backupIO + encryptedFilePath:tempFilePath + encryptionKey:lazyRestoreFragment.encryptionKey]; + }); } -- (void)lazyRestoreAttachment:(TSAttachmentPointer *)attachmentPointer - backupIO:(OWSBackupIO *)backupIO - encryptedFilePath:(NSString *)encryptedFilePath - encryptionKey:(NSData *)encryptionKey - completion:(OWSBackupBoolBlock)completion +- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachmentPointer + backupIO:(OWSBackupIO *)backupIO + encryptedFilePath:(NSString *)encryptedFilePath + encryptionKey:(NSData *)encryptionKey { OWSAssertDebug(attachmentPointer); OWSAssertDebug(backupIO); OWSAssertDebug(encryptedFilePath.length > 0); OWSAssertDebug(encryptionKey.length > 0); - OWSAssertDebug(completion); NSData *_Nullable data = [NSData dataWithContentsOfFile:encryptedFilePath]; if (!data) { OWSLogError(@"Could not load encrypted file."); - return completion(NO); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load encrypted file.")]; } NSString *decryptedFilePath = [backupIO generateTempFilePath]; @@ -828,7 +816,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description) @autoreleasepool { if (![backupIO decryptFileAsFile:encryptedFilePath dstFilePath:decryptedFilePath encryptionKey:encryptionKey]) { OWSLogError(@"Could not load decrypt file."); - return completion(NO); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load decrypt file.")]; } } @@ -837,18 +825,20 @@ NSError *OWSBackupErrorWithDescription(NSString *description) NSString *attachmentFilePath = stream.originalFilePath; if (attachmentFilePath.length < 1) { OWSLogError(@"Attachment has invalid file path."); - return completion(NO); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment has invalid file path.")]; } NSString *attachmentDirPath = [attachmentFilePath stringByDeletingLastPathComponent]; if (![OWSFileSystem ensureDirectoryExists:attachmentDirPath]) { OWSLogError(@"Couldn't create directory for attachment file."); - return completion(NO); + return [AnyPromise + promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't create directory for attachment file.")]; } if (![OWSFileSystem deleteFileIfExists:attachmentFilePath]) { OWSFailDebug(@"Couldn't delete existing file at attachment path."); - return completion(NO); + return [AnyPromise + promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't delete existing file at attachment path.")]; } NSError *error; @@ -856,7 +846,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description) [NSFileManager.defaultManager moveItemAtPath:decryptedFilePath toPath:attachmentFilePath error:&error]; if (!success || error) { OWSLogError(@"Attachment file could not be restored: %@.", error); - return completion(NO); + return [AnyPromise promiseWithValue:error]; } [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { @@ -864,7 +854,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description) [stream saveWithTransaction:transaction]; }]; - completion(YES); + return [AnyPromise promiseWithValue:@(1)]; } #pragma mark - Notifications diff --git a/Signal/src/util/Backup/OWSBackupAPI.swift b/Signal/src/util/Backup/OWSBackupAPI.swift index e9faba619..adee7c98c 100644 --- a/Signal/src/util/Backup/OWSBackupAPI.swift +++ b/Signal/src/util/Backup/OWSBackupAPI.swift @@ -582,57 +582,57 @@ import PromiseKit // MARK: - Download @objc - public class func downloadManifestFromCloud(recipientId: String, - success: @escaping (Data) -> Void, - failure: @escaping (Error) -> Void) { + public class func downloadManifestFromCloudObjc(recipientId: String) -> AnyPromise { + return AnyPromise(downloadManifestFromCloud(recipientId: recipientId)) + } + + public class func downloadManifestFromCloud(recipientId: String) -> Promise { let recordName = recordNameForManifest(recipientId: recipientId) - downloadDataFromCloud(recordName: recordName, - success: success, - failure: failure) + return downloadDataFromCloud(recordName: recordName) } @objc - public class func downloadDataFromCloud(recordName: String, - success: @escaping (Data) -> Void, - failure: @escaping (Error) -> Void) { - - downloadFromCloud(recordName: recordName, - remainingRetries: maxRetries, - success: { (asset) in - DispatchQueue.global().async { - do { - let data = try Data(contentsOf: asset.fileURL) - success(data) - } catch { - Logger.error("couldn't load asset file: \(error).") - failure(invalidServiceResponseError()) - } - } - }, - failure: failure) + public class func downloadDataFromCloudObjc(recordName: String) -> AnyPromise { + return AnyPromise(downloadDataFromCloud(recordName: recordName)) + } + + public class func downloadDataFromCloud(recordName: String) -> Promise { + + return downloadFromCloud(recordName: recordName, + remainingRetries: maxRetries) + .then { (asset) -> Promise in + do { + let data = try Data(contentsOf: asset.fileURL) + return Promise.value(data) + } catch { + Logger.error("couldn't load asset file: \(error).") + return Promise(error: invalidServiceResponseError()) + } + } } @objc + public class func downloadFileFromCloudObjc(recordName: String, + toFileUrl: URL) -> AnyPromise { + return AnyPromise(downloadFileFromCloud(recordName: recordName, + toFileUrl: toFileUrl)) + } + public class func downloadFileFromCloud(recordName: String, - toFileUrl: URL, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void) { - - downloadFromCloud(recordName: recordName, - remainingRetries: maxRetries, - success: { (asset) in - DispatchQueue.global().async { - do { - try FileManager.default.copyItem(at: asset.fileURL, to: toFileUrl) - success() - } catch { - Logger.error("couldn't copy asset file: \(error).") - failure(invalidServiceResponseError()) - } - } - }, - failure: failure) + toFileUrl: URL) -> Promise { + + return downloadFromCloud(recordName: recordName, + remainingRetries: maxRetries) + .then { (asset) -> Promise in + do { + try FileManager.default.copyItem(at: asset.fileURL, to: toFileUrl) + return Promise.value(()) + } catch { + Logger.error("couldn't copy asset file: \(error).") + return Promise(error: invalidServiceResponseError()) + } + } } // We return the CKAsset and not its fileUrl because @@ -641,9 +641,9 @@ import PromiseKit // defer cleanup by maintaining a strong reference to // the asset. private class func downloadFromCloud(recordName: String, - remainingRetries: Int, - success: @escaping (CKAsset) -> Void, - failure: @escaping (Error) -> Void) { + remainingRetries: Int) -> Promise { + + let (promise, resolver) = Promise.pending() let recordId = CKRecordID(recordName: recordName) let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ]) @@ -657,37 +657,45 @@ import PromiseKit case .success: guard let record = record else { Logger.error("missing fetching record.") - failure(invalidServiceResponseError()) + resolver.reject(invalidServiceResponseError()) return } guard let asset = record[payloadKey] as? CKAsset else { Logger.error("record missing payload.") - failure(invalidServiceResponseError()) + resolver.reject(invalidServiceResponseError()) return } - success(asset) + resolver.fulfill(asset) case .failureDoNotRetry(let outcomeError): - failure(outcomeError) + resolver.reject(outcomeError) case .failureRetryAfterDelay(let retryDelay): DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: { downloadFromCloud(recordName: recordName, - remainingRetries: remainingRetries - 1, - success: success, - failure: failure) + remainingRetries: remainingRetries - 1) + .done { (asset) in + resolver.fulfill(asset) + }.catch { (error) in + resolver.reject(error) + }.retainUntilComplete() }) case .failureRetryWithoutDelay: DispatchQueue.global().async { downloadFromCloud(recordName: recordName, - remainingRetries: remainingRetries - 1, - success: success, - failure: failure) + remainingRetries: remainingRetries - 1) + .done { (asset) in + resolver.fulfill(asset) + }.catch { (error) in + resolver.reject(error) + }.retainUntilComplete() } case .unknownItem: Logger.error("missing fetching record.") - failure(invalidServiceResponseError()) + resolver.reject(invalidServiceResponseError()) } } database().add(fetchOperation) + + return promise } // MARK: - Access diff --git a/Signal/src/util/Backup/OWSBackupImportJob.m b/Signal/src/util/Backup/OWSBackupImportJob.m index 35f817773..4fcd72fd7 100644 --- a/Signal/src/util/Backup/OWSBackupImportJob.m +++ b/Signal/src/util/Backup/OWSBackupImportJob.m @@ -89,15 +89,14 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe [self updateProgressWithDescription:nil progress:nil]; - __weak OWSBackupImportJob *weakSelf = self; [[self.backup ensureCloudKitAccess] .thenInBackground(^{ - [weakSelf start]; + [self start]; }) .catch(^(NSError *error) { - [weakSelf failWithErrorDescription: - NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT", - @"Error indicating the backup import could not import the user's data.")]; + [self failWithErrorDescription: + NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT", + @"Error indicating the backup import could not import the user's data.")]; }) retainUntilComplete]; } @@ -121,35 +120,32 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe @"Indicates that the backup import data is being imported.") progress:nil]; - __weak OWSBackupImportJob *weakSelf = self; - [weakSelf - downloadAndProcessManifestWithSuccess:^(OWSBackupManifestContents *manifest) { - OWSBackupImportJob *strongSelf = weakSelf; - if (!strongSelf) { - return; - } - if (self.isComplete) { - return; - } - OWSCAssertDebug(manifest.databaseItems.count > 0); - OWSCAssertDebug(manifest.attachmentsItems); - strongSelf.manifest = manifest; - [strongSelf downloadAndProcessImport]; - } - failure:^(NSError *manifestError) { - [weakSelf failWithError:manifestError]; - } - backupIO:self.backupIO]; + [[self downloadAndProcessManifestWithBackupIO:self.backupIO] + .thenInBackground(^(OWSBackupManifestContents *manifest) { + OWSCAssertDebug(manifest.databaseItems.count > 0); + OWSCAssertDebug(manifest.attachmentsItems); + + self.manifest = manifest; + + return [self downloadAndProcessImport]; + }) + .catchInBackground(^(NSError *error) { + [self failWithError:error]; + }) retainUntilComplete]; } -- (void)downloadAndProcessImport +- (AnyPromise *)downloadAndProcessImport { OWSAssertDebug(self.databaseItems); OWSAssertDebug(self.attachmentsItems); NSMutableArray *allItems = [NSMutableArray new]; [allItems addObjectsFromArray:self.databaseItems]; + // TODO: [allItems addObjectsFromArray:self.attachmentsItems]; + if (self.manifest.localProfileAvatarItem) { + [allItems addObject:self.manifest.localProfileAvatarItem]; + } // Record metadata for all items, so that we can re-use them in incremental backups after the restore. [self.primaryStorage.newDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { @@ -158,62 +154,31 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe } }]; - __weak OWSBackupImportJob *weakSelf = self; - [weakSelf - downloadFilesFromCloud:allItems - completion:^(NSError *_Nullable fileDownloadError) { - if (fileDownloadError) { - [weakSelf failWithError:fileDownloadError]; - return; - } - - if (weakSelf.isComplete) { - return; - } - - [weakSelf restoreDatabaseWithCompletion:^(BOOL restoreDatabaseSuccess) { - if (!restoreDatabaseSuccess) { - [weakSelf - failWithErrorDescription:NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT", - @"Error indicating the backup import " - @"could not import the user's data.")]; - return; - } - - if (weakSelf.isComplete) { - return; - } - - [weakSelf ensureMigrationsWithCompletion:^(BOOL ensureMigrationsSuccess) { - if (!ensureMigrationsSuccess) { - [weakSelf failWithErrorDescription:NSLocalizedString( - @"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT", - @"Error indicating the backup import " - @"could not import the user's data.")]; - return; - } - - if (weakSelf.isComplete) { - return; - } - - [weakSelf restoreAttachmentFiles]; - - if (weakSelf.isComplete) { - return; - } - - [weakSelf.profileManager fetchLocalUsersProfile]; - - [weakSelf.tsAccountManager updateAccountAttributes]; - - // Kick off lazy restore. - [OWSBackupLazyRestoreJob runAsync]; - - [weakSelf succeed]; - }]; - }]; - }]; + return [self downloadFilesFromCloud:allItems] + .thenInBackground(^{ + return [self restoreDatabase]; + }) + .thenInBackground(^{ + return [self ensureMigrations]; + }) + .thenInBackground(^{ + return [self restoreLocalProfile]; + }) + .thenInBackground(^{ + return [self restoreAttachmentFiles]; + }) + .thenInBackground(^{ + // Kick off lazy restore. + [OWSBackupLazyRestoreJob runAsync]; + + [self.profileManager fetchLocalUsersProfile]; + + [self.tsAccountManager updateAccountAttributes]; + + [self succeed]; + + return [AnyPromise promiseWithValue:@(1)]; + }); } - (BOOL)configureImport @@ -230,80 +195,134 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe return YES; } -- (void)downloadFilesFromCloud:(NSMutableArray *)items - completion:(OWSBackupJobCompletion)completion +- (AnyPromise *)downloadFilesFromCloud:(NSMutableArray *)items { OWSAssertDebug(items.count > 0); - OWSAssertDebug(completion); OWSLogVerbose(@""); - [self downloadNextItemFromCloud:items recordCount:items.count completion:completion]; -} - -- (void)downloadNextItemFromCloud:(NSMutableArray *)items - recordCount:(NSUInteger)recordCount - completion:(OWSBackupJobCompletion)completion -{ - OWSAssertDebug(items); - OWSAssertDebug(completion); + NSUInteger recordCount = items.count; if (self.isComplete) { // Job was aborted. - return completion(nil); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")]; } if (items.count < 1) { // All downloads are complete; exit. - return completion(nil); + return [AnyPromise promiseWithValue:@(1)]; } - OWSBackupFragment *item = items.lastObject; - [items removeLastObject]; - CGFloat progress = (recordCount > 0 ? ((recordCount - items.count) / (CGFloat)recordCount) : 0.f); - [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_DOWNLOAD", - @"Indicates that the backup import data is being downloaded.") - progress:@(progress)]; + AnyPromise *promise = [AnyPromise promiseWithValue:@(1)]; + for (OWSBackupFragment *item in items) { + promise = promise.thenInBackground(^{ + CGFloat progress = (recordCount > 0 ? ((recordCount - items.count) / (CGFloat)recordCount) : 0.f); + [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_DOWNLOAD", + @"Indicates that the backup import data is being downloaded.") + progress:@(progress)]; - // TODO: Use a predictable file path so that multiple "import backup" attempts - // will leverage successful file downloads from previous attempts. - // - // TODO: This will also require imports using a predictable jobTempDirPath. - NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:item.recordName]; + return [AnyPromise promiseWithValue:@(1)]; + }); - // Skip redundant file download. - if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) { - [OWSFileSystem protectFileOrFolderAtPath:tempFilePath]; + // TODO: Use a predictable file path so that multiple "import backup" attempts + // will leverage successful file downloads from previous attempts. + // + // TODO: This will also require imports using a predictable jobTempDirPath. + NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:item.recordName]; - item.downloadFilePath = tempFilePath; + // Skip redundant file download. + if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) { + [OWSFileSystem protectFileOrFolderAtPath:tempFilePath]; - [self downloadNextItemFromCloud:items recordCount:recordCount completion:completion]; - return; + item.downloadFilePath = tempFilePath; + + continue; + } + + promise = promise.thenInBackground(^{ + return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:item.recordName + toFileUrl:[NSURL fileURLWithPath:tempFilePath]] + .thenInBackground(^{ + [OWSFileSystem protectFileOrFolderAtPath:tempFilePath]; + item.downloadFilePath = tempFilePath; + }); + }); } - __weak OWSBackupImportJob *weakSelf = self; - [OWSBackupAPI downloadFileFromCloudWithRecordName:item.recordName - toFileUrl:[NSURL fileURLWithPath:tempFilePath] - success:^{ - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [OWSFileSystem protectFileOrFolderAtPath:tempFilePath]; - item.downloadFilePath = tempFilePath; + return promise; +} + +- (AnyPromise *)restoreLocalProfile +{ + OWSLogVerbose(@": %zd", self.attachmentsItems.count); - [weakSelf downloadNextItemFromCloud:items recordCount:recordCount completion:completion]; - }); + if (self.isComplete) { + // Job was aborted. + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")]; + } + + NSString *_Nullable localProfileName = self.manifest.localProfileName; + UIImage *_Nullable localProfileAvatar = nil; + + if (self.manifest.localProfileAvatarItem) { + OWSBackupFragment *item = self.manifest.localProfileAvatarItem; + if (item.recordName.length < 1) { + OWSLogError(@"local profile avatar was not downloaded."); + // Ignore errors related to local profile. + return [AnyPromise promiseWithValue:@(1)]; + } + if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) { + OWSLogError(@"database snapshot missing size."); + // Ignore errors related to local profile. + return [AnyPromise promiseWithValue:@(1)]; + } + + @autoreleasepool { + NSData *_Nullable data = + [self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey]; + if (!data) { + OWSLogError(@"could not decrypt local profile avatar."); + // Ignore errors related to local profile. + return [AnyPromise promiseWithValue:@(1)]; + } + // TODO: Verify that we're not compressing the profile avatar data. + UIImage *_Nullable image = [UIImage imageWithData:data]; + if (!image) { + OWSLogError(@"could not decrypt local profile avatar."); + // Ignore errors related to local profile. + return [AnyPromise promiseWithValue:@(1)]; + } + localProfileAvatar = image; } - failure:^(NSError *error) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - completion(error); - }); + } + + if (localProfileName.length > 0 || localProfileAvatar) { + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [self.profileManager updateLocalProfileName:localProfileName + avatarImage:localProfileAvatar + success:^{ + resolve(@(1)); + } + failure:^{ + // Ignore errors related to local profile. + resolve(@(1)); + }]; }]; + return promise; + } else { + return [AnyPromise promiseWithValue:@(1)]; + } } -- (void)restoreAttachmentFiles +- (AnyPromise *)restoreAttachmentFiles { OWSLogVerbose(@": %zd", self.attachmentsItems.count); + if (self.isComplete) { + // Job was aborted. + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")]; + } + __block NSUInteger count = 0; YapDatabaseConnection *dbConnection = self.primaryStorage.newDatabaseConnection; [dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { @@ -347,22 +366,23 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe }]; OWSLogError(@"enqueued lazy restore of %zd files.", count); + + return [AnyPromise promiseWithValue:@(1)]; } -- (void)restoreDatabaseWithCompletion:(OWSBackupJobBoolCompletion)completion +- (AnyPromise *)restoreDatabase { - OWSAssertDebug(completion); - OWSLogVerbose(@""); if (self.isComplete) { - return completion(NO); + // Job was aborted. + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")]; } YapDatabaseConnection *_Nullable dbConnection = self.primaryStorage.newDatabaseConnection; if (!dbConnection) { OWSFailDebug(@"Could not create dbConnection."); - return completion(NO); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not create dbConnection.")]; } // Order matters here. @@ -411,14 +431,14 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe // Attachment-related errors are recoverable and can be ignored. // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) { OWSLogError(@"database snapshot missing size."); // Attachment-related errors are recoverable and can be ignored. // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } count++; @@ -432,7 +452,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe if (!compressedData) { // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } NSData *_Nullable uncompressedData = [self.backupIO decompressData:compressedData @@ -440,7 +460,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe if (!uncompressedData) { // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } NSError *error; SignalIOSProtoBackupSnapshot *_Nullable entities = @@ -449,13 +469,13 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe OWSLogError(@"could not parse proto: %@.", error); // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } if (!entities || entities.entity.count < 1) { OWSLogError(@"missing entities."); // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } for (SignalIOSProtoBackupSnapshotBackupEntity *entity in entities.entity) { NSData *_Nullable entityData = entity.entityData; @@ -463,7 +483,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe OWSLogError(@"missing entity data."); // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } NSString *_Nullable collection = entity.collection; @@ -471,7 +491,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe OWSLogError(@"missing collection."); // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } NSString *_Nullable key = entity.key; @@ -479,7 +499,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe OWSLogError(@"missing key."); // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } __block NSObject *object = nil; @@ -490,13 +510,13 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe OWSLogError(@"invalid decoded entity: %@.", [object class]); // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } } @catch (NSException *exception) { OWSLogError(@"could not decode entity."); // Database-related errors are unrecoverable. aborted = YES; - return completion(NO); + return; } [transaction setObject:object forKey:key inCollection:collection]; @@ -508,8 +528,12 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe } }]; - if (self.isComplete || aborted) { - return; + if (aborted) { + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import failed.")]; + } + if (self.isComplete) { + // Job was aborted. + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")]; } for (NSString *collection in restoredEntityCounts) { @@ -519,15 +543,18 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe [self.primaryStorage logFileSizes]; - completion(YES); + return [AnyPromise promiseWithValue:@(1)]; } -- (void)ensureMigrationsWithCompletion:(OWSBackupJobBoolCompletion)completion +- (AnyPromise *)ensureMigrations { - OWSAssertDebug(completion); - OWSLogVerbose(@""); + if (self.isComplete) { + // Job was aborted. + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")]; + } + [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_FINALIZING", @"Indicates that the backup import data is being finalized.") progress:nil]; @@ -536,11 +563,14 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe // It's okay that we do this in a separate transaction from the // restoration of backup contents. If some of migrations don't // complete, they'll be run the next time the app launches. - dispatch_async(dispatch_get_main_queue(), ^{ - [[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:^{ - completion(YES); - }]; - }); + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:^{ + resolve(@(1)); + }]; + }); + }]; + return promise; } @end diff --git a/Signal/src/util/Backup/OWSBackupJob.h b/Signal/src/util/Backup/OWSBackupJob.h index 70a70909b..1c7c61f73 100644 --- a/Signal/src/util/Backup/OWSBackupJob.h +++ b/Signal/src/util/Backup/OWSBackupJob.h @@ -17,6 +17,7 @@ extern NSString *const kOWSBackup_ManifestKey_DataSize; extern NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar; extern NSString *const kOWSBackup_ManifestKey_LocalProfileName; +@class AnyPromise; @class OWSBackupIO; @class OWSBackupJob; @class OWSBackupManifestContents; @@ -84,9 +85,7 @@ typedef void (^OWSBackupJobManifestFailure)(NSError *error); #pragma mark - Manifest -- (void)downloadAndProcessManifestWithSuccess:(OWSBackupJobManifestSuccess)success - failure:(OWSBackupJobManifestFailure)failure - backupIO:(OWSBackupIO *)backupIO; +- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO; @end diff --git a/Signal/src/util/Backup/OWSBackupJob.m b/Signal/src/util/Backup/OWSBackupJob.m index 4b8ffd95b..f2d1433fe 100644 --- a/Signal/src/util/Backup/OWSBackupJob.m +++ b/Signal/src/util/Backup/OWSBackupJob.m @@ -5,6 +5,7 @@ #import "OWSBackupJob.h" #import "OWSBackupIO.h" #import "Signal-Swift.h" +#import #import #import @@ -153,52 +154,31 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; #pragma mark - Manifest -- (void)downloadAndProcessManifestWithSuccess:(OWSBackupJobManifestSuccess)success - failure:(OWSBackupJobManifestFailure)failure - backupIO:(OWSBackupIO *)backupIO +- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO { - OWSAssertDebug(success); - OWSAssertDebug(failure); OWSAssertDebug(backupIO); OWSLogVerbose(@""); - __weak OWSBackupJob *weakSelf = self; - [OWSBackupAPI downloadManifestFromCloudWithRecipientId:self.recipientId - success:^(NSData *data) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [weakSelf - processManifest:data - success:success - failure:^{ - failure(OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed, - NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT", - @"Error indicating the backup import could not import the user's data."))); - } - backupIO:backupIO]; - }); - } - failure:^(NSError *error) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // The manifest file is critical so any error downloading it is unrecoverable. - OWSCFailDebug(@"Could not download manifest."); - failure(error); - }); - }]; + if (self.isComplete) { + // Job was aborted. + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")]; + } + + return + [OWSBackupAPI downloadManifestFromCloudObjcWithRecipientId:self.recipientId].thenInBackground(^(NSData *data) { + return [self processManifest:data backupIO:backupIO]; + }); } -- (void)processManifest:(NSData *)manifestDataEncrypted - success:(OWSBackupJobManifestSuccess)success - failure:(dispatch_block_t)failure - backupIO:(OWSBackupIO *)backupIO +- (AnyPromise *)processManifest:(NSData *)manifestDataEncrypted backupIO:(OWSBackupIO *)backupIO { OWSAssertDebug(manifestDataEncrypted.length > 0); - OWSAssertDebug(success); - OWSAssertDebug(failure); OWSAssertDebug(backupIO); if (self.isComplete) { - return; + // Job was aborted. + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")]; } OWSLogVerbose(@""); @@ -207,7 +187,7 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; [backupIO decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey]; if (!manifestDataDecrypted) { OWSFailDebug(@"Could not decrypt manifest."); - return failure(); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not decrypt manifest.")]; } NSError *error; @@ -215,7 +195,7 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; [NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error]; if (![json isKindOfClass:[NSDictionary class]]) { OWSFailDebug(@"Could not download manifest."); - return failure(); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not download manifest.")]; } OWSLogVerbose(@"json: %@", json); @@ -223,12 +203,12 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; NSArray *_Nullable databaseItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles]; if (!databaseItems) { - return failure(); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")]; } NSArray *_Nullable attachmentsItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles]; if (!attachmentsItems) { - return failure(); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")]; } NSArray *_Nullable localProfileAvatarItems; @@ -248,7 +228,7 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; OWSFailDebug(@"Invalid localProfileName: %@", [localProfileName class]); } - return success(contents); + return [AnyPromise promiseWithValue:contents]; } - (nullable id)parseManifestItem:(id)json key:(NSString *)key