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..00cd59432 100644 --- a/Signal/src/util/Backup/OWSBackup.m +++ b/Signal/src/util/Backup/OWSBackup.m @@ -754,7 +754,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description) OWSFailDebug(@"Could not load database view."); return; } - + [ext enumerateKeysInGroup:TSLazyRestoreAttachmentsGroup usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) { [attachmentIds addObject:key]; @@ -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 93ecdb806..aa309577a 100644 --- a/Signal/src/util/Backup/OWSBackupAPI.swift +++ b/Signal/src/util/Backup/OWSBackupAPI.swift @@ -62,14 +62,14 @@ import PromiseKit // We wouldn't want to overwrite previous images until the entire backup export is // complete. @objc - public class func saveEphemeralDatabaseFileToCloudObjc(recipientId: String, - fileUrl: URL) -> AnyPromise { - return AnyPromise(saveEphemeralDatabaseFileToCloud(recipientId: recipientId, - fileUrl: fileUrl)) + public class func saveEphemeralFileToCloudObjc(recipientId: String, + fileUrl: URL) -> AnyPromise { + return AnyPromise(saveEphemeralFileToCloud(recipientId: recipientId, + fileUrl: fileUrl)) } - public class func saveEphemeralDatabaseFileToCloud(recipientId: String, - fileUrl: URL) -> Promise { + public class func saveEphemeralFileToCloud(recipientId: String, + fileUrl: URL) -> Promise { let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeralFile-\(NSUUID().uuidString)" return saveFileToCloud(fileUrl: fileUrl, recordName: recordName, @@ -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/OWSBackupExportJob.m b/Signal/src/util/Backup/OWSBackupExportJob.m index 6ff6ed111..5571b4ee0 100644 --- a/Signal/src/util/Backup/OWSBackupExportJob.m +++ b/Signal/src/util/Backup/OWSBackupExportJob.m @@ -308,6 +308,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSMutableArray *savedAttachmentItems; +@property (nonatomic, nullable) OWSBackupExportItem *localProfileAvatarItem; + @property (nonatomic, nullable) OWSBackupExportItem *manifestItem; // If we are replacing an existing backup, we use some of its contents for continuity. @@ -335,6 +337,11 @@ NS_ASSUME_NONNULL_BEGIN return AppEnvironment.shared.backup; } +- (OWSProfileManager *)profileManager +{ + return [OWSProfileManager sharedManager]; +} + #pragma mark - - (void)start @@ -457,7 +464,7 @@ NS_ASSUME_NONNULL_BEGIN NSError *error = OWSBackupErrorWithDescription(@"Backup export failed."); return resolve(error); } - + resolve(@(1)); }]; } @@ -722,6 +729,9 @@ NS_ASSUME_NONNULL_BEGIN .thenInBackground(^{ return [self saveDatabaseFilesToCloud]; }) + .thenInBackground(^{ + return [self saveLocalProfileAvatarToCloud]; + }) .thenInBackground(^{ return [self saveManifestFileToCloud]; }); @@ -745,7 +755,7 @@ NS_ASSUME_NONNULL_BEGIN } return [OWSBackupAPI - saveEphemeralDatabaseFileToCloudObjcWithRecipientId:self.recipientId + saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId fileUrl:[NSURL fileURLWithPath:item.encryptedItem .filePath]]; }) @@ -876,6 +886,36 @@ NS_ASSUME_NONNULL_BEGIN }); } +- (AnyPromise *)saveLocalProfileAvatarToCloud +{ + if (self.isComplete) { + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; + } + + NSData *_Nullable localProfileAvatarData = self.profileManager.localProfileAvatarData; + if (localProfileAvatarData.length < 1) { + // No profile avatar to backup. + return [AnyPromise promiseWithValue:@(1)]; + } + OWSBackupEncryptedItem *_Nullable encryptedItem = + [self.backupIO encryptDataAsTempFile:localProfileAvatarData encryptionKey:self.delegate.backupEncryptionKey]; + if (!encryptedItem) { + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not encrypt local profile avatar.")]; + } + + OWSBackupExportItem *exportItem = [OWSBackupExportItem new]; + exportItem.encryptedItem = encryptedItem; + + return [OWSBackupAPI saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId + fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath]] + .thenInBackground(^(NSString *recordName) { + exportItem.recordName = recordName; + self.localProfileAvatarItem = exportItem; + + return [AnyPromise promiseWithValue:@(1)]; + }); +} + - (AnyPromise *)saveManifestFileToCloud { if (self.isComplete) { @@ -907,10 +947,19 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(self.jobTempDirPath.length > 0); OWSAssertDebug(self.backupIO); - NSDictionary *json = @{ + NSMutableDictionary *json = [@{ kOWSBackup_ManifestKey_DatabaseFiles : [self jsonForItems:self.savedDatabaseItems], kOWSBackup_ManifestKey_AttachmentFiles : [self jsonForItems:self.savedAttachmentItems], - }; + } mutableCopy]; + + NSString *_Nullable localProfileName = self.profileManager.localProfileName; + if (localProfileName.length > 0) { + json[kOWSBackup_ManifestKey_LocalProfileName] = localProfileName; + } + + if (self.localProfileAvatarItem) { + json[kOWSBackup_ManifestKey_LocalProfileAvatar] = [self jsonForItems:@[ self.localProfileAvatarItem ]]; + } OWSLogVerbose(@"json: %@", json); @@ -1010,7 +1059,7 @@ NS_ASSUME_NONNULL_BEGIN NSMutableSet *obsoleteRecordNames = [NSMutableSet new]; [obsoleteRecordNames addObjectsFromArray:[transaction allKeysInCollection:[OWSBackupFragment collection]]]; [obsoleteRecordNames minusSet:activeRecordNames]; - + [transaction removeObjectsForKeys:obsoleteRecordNames.allObjects inCollection:[OWSBackupFragment collection]]; }]; } diff --git a/Signal/src/util/Backup/OWSBackupImportJob.m b/Signal/src/util/Backup/OWSBackupImportJob.m index 736fbab31..7c0c76adf 100644 --- a/Signal/src/util/Backup/OWSBackupImportJob.m +++ b/Signal/src/util/Backup/OWSBackupImportJob.m @@ -27,8 +27,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe @property (nonatomic) OWSBackupIO *backupIO; -@property (nonatomic) NSArray *databaseItems; -@property (nonatomic) NSArray *attachmentsItems; +@property (nonatomic) OWSBackupManifestContents *manifest; @end @@ -66,6 +65,20 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe #pragma mark - +- (NSArray *)databaseItems +{ + OWSAssertDebug(self.manifest); + + return self.manifest.databaseItems; +} + +- (NSArray *)attachmentsItems +{ + OWSAssertDebug(self.manifest); + + return self.manifest.attachmentsItems; +} + - (void)startAsync { OWSAssertIsOnMainThread(); @@ -76,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]; } @@ -108,36 +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.databaseItems = manifest.databaseItems; - strongSelf.attachmentsItems = manifest.attachmentsItems; - [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: We probably want to remove this. [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) { @@ -146,62 +154,29 @@ 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]; + }); } - (BOOL)configureImport @@ -218,80 +193,132 @@ 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]; - // 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]; + // Skip redundant file download. + if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) { + [OWSFileSystem protectFileOrFolderAtPath:tempFilePath]; - // Skip redundant file download. - if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) { - [OWSFileSystem protectFileOrFolderAtPath:tempFilePath]; + item.downloadFilePath = tempFilePath; - item.downloadFilePath = tempFilePath; + continue; + } - [self downloadNextItemFromCloud:items recordCount:recordCount completion:completion]; - return; + 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) { @@ -335,22 +362,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. @@ -399,14 +427,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++; @@ -420,7 +448,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 @@ -428,7 +456,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 = @@ -437,13 +465,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; @@ -451,7 +479,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; @@ -459,7 +487,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; @@ -467,7 +495,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; @@ -478,13 +506,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]; @@ -496,8 +524,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) { @@ -507,15 +539,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]; @@ -524,11 +559,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 ac42def1d..1c7c61f73 100644 --- a/Signal/src/util/Backup/OWSBackupJob.h +++ b/Signal/src/util/Backup/OWSBackupJob.h @@ -14,7 +14,10 @@ extern NSString *const kOWSBackup_ManifestKey_EncryptionKey; extern NSString *const kOWSBackup_ManifestKey_RelativeFilePath; extern NSString *const kOWSBackup_ManifestKey_AttachmentId; 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; @@ -28,6 +31,8 @@ typedef void (^OWSBackupJobManifestFailure)(NSError *error); @property (nonatomic) NSArray *databaseItems; @property (nonatomic) NSArray *attachmentsItems; +@property (nonatomic, nullable) OWSBackupFragment *localProfileAvatarItem; +@property (nonatomic, nullable) NSString *localProfileName; @end @@ -80,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 94617cae7..7c4e0c072 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 @@ -17,6 +18,8 @@ NSString *const kOWSBackup_ManifestKey_EncryptionKey = @"encryption_key"; NSString *const kOWSBackup_ManifestKey_RelativeFilePath = @"relative_file_path"; NSString *const kOWSBackup_ManifestKey_AttachmentId = @"attachment_id"; NSString *const kOWSBackup_ManifestKey_DataSize = @"data_size"; +NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar = @"local_profile_avatar"; +NSString *const kOWSBackup_ManifestKey_LocalProfileName = @"local_profile_name"; NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; @@ -108,12 +111,12 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; return; } self.isComplete = YES; - + // There's a lot of asynchrony in these backup jobs; // ensure we only end up finishing these jobs once. OWSAssertDebug(!self.hasSucceeded); self.hasSucceeded = YES; - + [self.delegate backupJobDidSucceed:self]; }); } @@ -151,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(@""); @@ -205,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; @@ -213,30 +195,56 @@ 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); NSArray *_Nullable databaseItems = - [self parseItems:json key:kOWSBackup_ManifestKey_DatabaseFiles]; + [self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles]; if (!databaseItems) { - return failure(); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")]; } NSArray *_Nullable attachmentsItems = - [self parseItems:json key:kOWSBackup_ManifestKey_AttachmentFiles]; + [self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles]; if (!attachmentsItems) { - return failure(); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")]; + } + + NSArray *_Nullable localProfileAvatarItems; + if ([self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileAvatar]) { + localProfileAvatarItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_LocalProfileAvatar]; } + NSString *_Nullable localProfileName = [self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileName]; + OWSBackupManifestContents *contents = [OWSBackupManifestContents new]; contents.databaseItems = databaseItems; contents.attachmentsItems = attachmentsItems; + contents.localProfileAvatarItem = localProfileAvatarItems.firstObject; + if ([localProfileName isKindOfClass:[NSString class]]) { + contents.localProfileName = localProfileName; + } else { + OWSFailDebug(@"Invalid localProfileName: %@", [localProfileName class]); + } - return success(contents); + return [AnyPromise promiseWithValue:contents]; +} + +- (nullable id)parseManifestItem:(id)json key:(NSString *)key +{ + OWSAssertDebug(json); + OWSAssertDebug(key.length); + + if (![json isKindOfClass:[NSDictionary class]]) { + OWSFailDebug(@"manifest has invalid data."); + return nil; + } + id _Nullable value = json[key]; + return value; } -- (nullable NSArray *)parseItems:(id)json key:(NSString *)key +- (nullable NSArray *)parseManifestItems:(id)json key:(NSString *)key { OWSAssertDebug(json); OWSAssertDebug(key.length); diff --git a/SignalMessaging/profiles/OWSProfileManager.h b/SignalMessaging/profiles/OWSProfileManager.h index a35193ab9..0f1122220 100644 --- a/SignalMessaging/profiles/OWSProfileManager.h +++ b/SignalMessaging/profiles/OWSProfileManager.h @@ -37,6 +37,7 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter; - (BOOL)hasLocalProfile; - (nullable NSString *)localProfileName; - (nullable UIImage *)localProfileAvatarImage; +- (nullable NSData *)localProfileAvatarData; - (void)ensureLocalProfileCached; // This method is used to update the "local profile" state on the client diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index 87648e85c..0766e0e0e 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -217,6 +217,15 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); return [self loadProfileAvatarWithFilename:self.localUserProfile.avatarFileName]; } +- (nullable NSData *)localProfileAvatarData +{ + NSString *_Nullable filename = self.localUserProfile.avatarFileName; + if (filename.length < 1) { + return nil; + } + return [self loadProfileDataWithFilename:filename]; +} + - (void)updateLocalProfileName:(nullable NSString *)profileName avatarImage:(nullable UIImage *)avatarImage success:(void (^)(void))successBlockParameter