diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 6b2a3cf5a..0bbfa9e1d 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -471,6 +471,8 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat __weak ProfileViewController *weakSelf = self; [OWSBackup.sharedManager checkCanImportBackup:^(BOOL value) { + OWSAssertIsOnMainThread(); + OWSLogInfo(@"has backup available for import? %d", value); if (value) { diff --git a/Signal/src/util/Backup/OWSBackup.h b/Signal/src/util/Backup/OWSBackup.h index 35cc8a2a8..d1515f824 100644 --- a/Signal/src/util/Backup/OWSBackup.h +++ b/Signal/src/util/Backup/OWSBackup.h @@ -26,6 +26,8 @@ NSString *NSStringForBackupImportState(OWSBackupState state); NSArray *MiscCollectionsToBackup(void); +NSError *OWSBackupErrorWithDescription(NSString *description); + @class AnyPromise; @class OWSBackupIO; @class TSAttachmentPointer; diff --git a/Signal/src/util/Backup/OWSBackup.m b/Signal/src/util/Backup/OWSBackup.m index 25001fa9b..055115f59 100644 --- a/Signal/src/util/Backup/OWSBackup.m +++ b/Signal/src/util/Backup/OWSBackup.m @@ -65,6 +65,17 @@ NSArray *MiscCollectionsToBackup(void) ]; } +typedef NS_ENUM(NSInteger, OWSBackupErrorCode) { + OWSBackupErrorCodeAssertionFailure = 0, +}; + +NSError *OWSBackupErrorWithDescription(NSString *description) +{ + return [NSError errorWithDomain:@"OWSBackupErrorDomain" + code:OWSBackupErrorCodeAssertionFailure + userInfo:@{ NSLocalizedDescriptionKey : description }]; +} + // TODO: Observe Reachability. @interface OWSBackup () @@ -475,17 +486,10 @@ NSArray *MiscCollectionsToBackup(void) [[OWSBackupAPI checkCloudKitAccessObjc] .thenInBackground(^{ - [OWSBackupAPI checkForManifestInCloudWithRecipientId:recipientId - success:^(BOOL value) { - dispatch_async(dispatch_get_main_queue(), ^{ - success(value); - }); - } - failure:^(NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(error); - }); - }]; + return [OWSBackupAPI checkForManifestInCloudObjcWithRecipientId:recipientId]; + }) + .then(^(NSNumber *value) { + success(value.boolValue); }) .catch(^(NSError *error) { failure(error); diff --git a/Signal/src/util/Backup/OWSBackupAPI.swift b/Signal/src/util/Backup/OWSBackupAPI.swift index e7161f863..4c0403ced 100644 --- a/Signal/src/util/Backup/OWSBackupAPI.swift +++ b/Signal/src/util/Backup/OWSBackupAPI.swift @@ -412,17 +412,23 @@ import PromiseKit } @objc - public class func checkForManifestInCloud(recipientId: String, - success: @escaping (Bool) -> Void, - failure: @escaping (Error) -> Void) { + public class func checkForManifestInCloudObjc(recipientId: String) -> AnyPromise { + return AnyPromise(checkForManifestInCloud(recipientId: recipientId)) + } + + public class func checkForManifestInCloud(recipientId: String) -> Promise { + let (promise, resolver) = Promise.pending() let recordName = recordNameForManifest(recipientId: recipientId) checkForFileInCloud(recordName: recordName, remainingRetries: maxRetries, success: { (record) in - success(record != nil) + resolver.fulfill(record != nil) }, - failure: failure) + failure: { (error) in + resolver.reject(error) + }) + return promise } @objc @@ -675,26 +681,24 @@ import PromiseKit public class func checkCloudKitAccess() -> Promise { let (promise, resolver) = Promise.pending() CKContainer.default().accountStatus(completionHandler: { (accountStatus, error) in - DispatchQueue.main.async { - if let error = error { - Logger.error("Unknown error: \(String(describing: error)).") - resolver.reject(error) - return - } - switch accountStatus { - case .couldNotDetermine: - Logger.error("could not determine CloudKit account status: \(String(describing: error)).") - resolver.reject(BackupError.couldNotDetermineAccountStatus) - case .noAccount: - Logger.error("no CloudKit account.") - resolver.reject(BackupError.noAccount) - case .restricted: - Logger.error("restricted CloudKit account.") - resolver.reject(BackupError.restrictedAccountStatus) - case .available: - Logger.verbose("CloudKit access okay.") - resolver.fulfill(()) - } + if let error = error { + Logger.error("Unknown error: \(String(describing: error)).") + resolver.reject(error) + return + } + switch accountStatus { + case .couldNotDetermine: + Logger.error("could not determine CloudKit account status: \(String(describing: error)).") + resolver.reject(BackupError.couldNotDetermineAccountStatus) + case .noAccount: + Logger.error("no CloudKit account.") + resolver.reject(BackupError.noAccount) + case .restricted: + Logger.error("restricted CloudKit account.") + resolver.reject(BackupError.restrictedAccountStatus) + case .available: + Logger.verbose("CloudKit access okay.") + resolver.fulfill(()) } }) return promise diff --git a/Signal/src/util/Backup/OWSBackupExportJob.m b/Signal/src/util/Backup/OWSBackupExportJob.m index 61313a17c..682dc5b9d 100644 --- a/Signal/src/util/Backup/OWSBackupExportJob.m +++ b/Signal/src/util/Backup/OWSBackupExportJob.m @@ -366,66 +366,46 @@ NS_ASSUME_NONNULL_BEGIN progress:nil]; __weak OWSBackupExportJob *weakSelf = self; - [self configureExportWithCompletion:^(BOOL configureExportSuccess) { - if (!configureExportSuccess) { - [self - failWithErrorDescription:NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT", - @"Error indicating the backup export could not export the user's data.")]; - return; - } + [[self configureExport] + .thenInBackground(^{ + return [self fetchAllRecords]; + }) + .thenInBackground(^{ + [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_EXPORT", + @"Indicates that the backup export data is being exported.") + progress:nil]; - if (self.isComplete) { - return; - } - [self fetchAllRecordsWithCompletion:^(BOOL tryToFetchManifestSuccess) { - if (!tryToFetchManifestSuccess) { - [self failWithErrorDescription: - NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT", - @"Error indicating the backup export could not export the user's data.")]; - return; - } + return [self exportDatabase]; + }) + .thenInBackground(^{ + return [self saveToCloud]; + }) + .thenInBackground(^{ + return [self cleanUp]; + }) + .thenInBackground(^{ + [self succeed]; + }) + .catch(^(NSError *error) { + OWSFailDebug(@"Backup export failed with error: %@.", error); - if (self.isComplete) { - return; - } - [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_EXPORT", - @"Indicates that the backup export data is being exported.") - progress:nil]; - if (![self exportDatabase]) { - [self failWithErrorDescription: - NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT", - @"Error indicating the backup export could not export the user's data.")]; - return; - } - if (self.isComplete) { - return; - } - [self saveToCloudWithCompletion:^(NSError *_Nullable saveError) { - if (saveError) { - [weakSelf failWithError:saveError]; - return; - } - [self cleanUpWithCompletion:^(NSError *_Nullable cleanUpError) { - if (cleanUpError) { - [weakSelf failWithError:cleanUpError]; - return; - } - [weakSelf succeed]; - }]; - }]; - }]; - }]; + [weakSelf failWithErrorDescription: + NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT", + @"Error indicating the backup export could not export the user's data.")]; + }) retainUntilComplete]; } -- (void)configureExportWithCompletion:(OWSBackupJobBoolCompletion)completion +- (AnyPromise *)configureExport { - OWSAssertDebug(completion); - OWSLogVerbose(@""); + if (self.isComplete) { + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; + } + if (![self ensureJobTempDir]) { OWSFailDebug(@"Could not create jobTempDirPath."); - return completion(NO); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not create jobTempDirPath.")]; } self.backupIO = [[OWSBackupIO alloc] initWithJobTempDirPath:self.jobTempDirPath]; @@ -437,55 +417,79 @@ NS_ASSUME_NONNULL_BEGIN // // We use an arbitrary request that requires authentication // to verify our account state. - TSRequest *currentSignedPreKey = [OWSRequestFactory currentSignedPreKeyRequest]; - [[TSNetworkManager sharedManager] makeRequest:currentSignedPreKey - success:^(NSURLSessionDataTask *task, NSDictionary *responseObject) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - completion(YES); - }); - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - // TODO: We may want to surface this in the UI. - OWSLogError(@"could not verify account status: %@.", error); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - completion(NO); - }); - }]; + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + TSRequest *currentSignedPreKey = [OWSRequestFactory currentSignedPreKeyRequest]; + [[TSNetworkManager sharedManager] makeRequest:currentSignedPreKey + success:^(NSURLSessionDataTask *task, NSDictionary *responseObject) { + resolve(@(1)); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + // TODO: We may want to surface this in the UI. + OWSLogError(@"could not verify account status: %@.", error); + resolve(error); + }]; + }]; + [promise retainUntilComplete]; + return promise; } -- (void)fetchAllRecordsWithCompletion:(OWSBackupJobBoolCompletion)completion +- (AnyPromise *)fetchAllRecords { - OWSAssertDebug(completion); + OWSLogVerbose(@""); if (self.isComplete) { - return; + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; } - OWSLogVerbose(@""); - __weak OWSBackupExportJob *weakSelf = self; - [OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId - success:^(NSArray *recordNames) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId + success:^(NSArray *recordNames) { OWSBackupExportJob *strongSelf = weakSelf; if (!strongSelf) { - return; + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); } if (strongSelf.isComplete) { - return; + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); } strongSelf.lastValidRecordNames = [NSSet setWithArray:recordNames]; - completion(YES); - }); + resolve(@(1)); + } + failure:^(NSError *error) { + resolve(error); + }]; + }]; + [promise retainUntilComplete]; + return promise; +} + +- (AnyPromise *)exportDatabase +{ + OWSAssertDebug(self.backupIO); + + OWSLogVerbose(@""); + + if (self.isComplete) { + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; + } + + __weak OWSBackupExportJob *weakSelf = self; + return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + OWSBackupExportJob *strongSelf = weakSelf; + if (!strongSelf) { + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); } - failure:^(NSError *error) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - completion(NO); - }); - }]; + + if (![strongSelf performExportDatabase]) { + NSError *error = OWSBackupErrorWithDescription(@"Backup export failed."); + return resolve(error); + } + + resolve(@(1)); + }]; } -- (BOOL)exportDatabase +- (BOOL)performExportDatabase { OWSAssertDebug(self.backupIO); @@ -687,12 +691,14 @@ NS_ASSUME_NONNULL_BEGIN return YES; } -- (void)saveToCloudWithCompletion:(OWSBackupJobCompletion)completion +- (AnyPromise *)saveToCloud { - OWSAssertDebug(completion); - OWSLogVerbose(@""); + if (self.isComplete) { + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; + } + self.savedDatabaseItems = [NSMutableArray new]; self.savedAttachmentItems = [NSMutableArray new]; @@ -728,20 +734,6 @@ NS_ASSUME_NONNULL_BEGIN } OWSLogInfo(@"exporting %@: count: %zd, bytes: %llu.", @"all items", totalFileCount, totalFileSize); - [self saveNextFileToCloudWithCompletion:completion]; -} - -// This method uploads one file (the "next" file) each time it -// is called. Each successful file upload re-invokes this method -// until the last (the manifest file). -- (void)saveNextFileToCloudWithCompletion:(OWSBackupJobCompletion)completion -{ - OWSAssertDebug(completion); - - if (self.isComplete) { - return; - } - // Add one for the manifest NSUInteger unsavedCount = (self.unsavedDatabaseItems.count + self.unsavedAttachmentExports.count + 1); NSUInteger savedCount = (self.savedDatabaseItems.count + self.savedAttachmentItems.count); @@ -751,65 +743,79 @@ NS_ASSUME_NONNULL_BEGIN @"Indicates that the backup export data is being uploaded.") progress:@(progress)]; - if ([self saveNextDatabaseFileToCloudWithCompletion:completion]) { - return; - } - if ([self saveNextAttachmentFileToCloudWithCompletion:completion]) { - return; - } - [self saveManifestFileToCloudWithCompletion:completion]; + // Save attachment files _before_ anything else, since they + // are the only reusable backup records. + return [self saveAttachmentFilesToCloud] + .thenInBackground(^{ + return [self saveDatabaseFilesToCloud]; + }) + .thenInBackground(^{ + return [self saveManifestFileToCloud]; + }); } // This method returns YES IFF "work was done and there might be more work to do". -- (BOOL)saveNextDatabaseFileToCloudWithCompletion:(OWSBackupJobCompletion)completion +- (AnyPromise *)saveDatabaseFilesToCloud { - OWSAssertDebug(completion); - __weak OWSBackupExportJob *weakSelf = self; - if (self.unsavedDatabaseItems.count < 1) { - return NO; - } - // Pop next item from queue, preserving ordering. - OWSBackupExportItem *item = self.unsavedDatabaseItems.firstObject; - [self.unsavedDatabaseItems removeObjectAtIndex:0]; + AnyPromise *promise = [AnyPromise promiseWithValue:@(1)]; - OWSAssertDebug(item.encryptedItem.filePath.length > 0); + // We need to preserve ordering of database shards. + for (OWSBackupExportItem *item in self.unsavedDatabaseItems) { + OWSAssertDebug(item.encryptedItem.filePath.length > 0); - [OWSBackupAPI saveEphemeralDatabaseFileToCloudWithRecipientId:self.recipientId - fileUrl:[NSURL fileURLWithPath:item.encryptedItem.filePath] - success:^(NSString *recordName) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - item.recordName = recordName; - [weakSelf.savedDatabaseItems addObject:item]; - [weakSelf saveNextFileToCloudWithCompletion:completion]; - }); - } - failure:^(NSError *error) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // Database files are critical so any error uploading them is unrecoverable. - OWSLogVerbose(@"error while saving file: %@", item.encryptedItem.filePath); - completion(error); - }); - }]; - return YES; + promise = promise.thenInBackground(^{ + return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + if (self.isComplete) { + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); + } + + [OWSBackupAPI saveEphemeralDatabaseFileToCloudWithRecipientId:self.recipientId + fileUrl:[NSURL fileURLWithPath:item.encryptedItem.filePath] + success:^(NSString *recordName) { + OWSBackupExportJob *strongSelf = weakSelf; + if (!strongSelf) { + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); + } + item.recordName = recordName; + [strongSelf.savedDatabaseItems addObject:item]; + resolve(@(1)); + } + failure:^(NSError *error) { + // Database files are critical so any error uploading them is unrecoverable. + OWSLogVerbose(@"error while saving file: %@", item.encryptedItem.filePath); + resolve(error); + }]; + }]; + }); + } + [self.unsavedDatabaseItems removeAllObjects]; + return promise; } // This method returns YES IFF "work was done and there might be more work to do". -- (BOOL)saveNextAttachmentFileToCloudWithCompletion:(OWSBackupJobCompletion)completion +- (AnyPromise *)saveAttachmentFilesToCloud { - OWSAssertDebug(completion); + AnyPromise *promise = [AnyPromise promiseWithValue:@(1)]; - __weak OWSBackupExportJob *weakSelf = self; - if (self.unsavedAttachmentExports.count < 1) { - return NO; + for (OWSAttachmentExport *attachmentExport in self.unsavedAttachmentExports) { + promise = promise.thenInBackground(^{ + return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + if (self.isComplete) { + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); + } + [self saveAttachmentFileToCloud:attachmentExport resolve:resolve]; + }]; + }); } + [self.unsavedAttachmentExports removeAllObjects]; + return promise; +} - // No need to preserve ordering of attachments. - OWSAttachmentExport *attachmentExport = self.unsavedAttachmentExports.lastObject; - [self.unsavedAttachmentExports removeLastObject]; +- (void)saveAttachmentFileToCloud:(OWSAttachmentExport *)attachmentExport resolve:(PMKResolver)resolve +{ + __weak OWSBackupExportJob *weakSelf = self; if (self.lastValidRecordNames) { // Wherever possible, we do incremental backups and re-use fragments of the last @@ -846,8 +852,7 @@ NS_ASSUME_NONNULL_BEGIN OWSLogVerbose(@"recycled attachment: %@ as %@", attachmentExport.attachmentFilePath, attachmentExport.relativeFilePath); - [self saveNextFileToCloudWithCompletion:completion]; - return YES; + return resolve(@(1)); } } @@ -856,8 +861,7 @@ NS_ASSUME_NONNULL_BEGIN // attachment to disk. if (![attachmentExport prepareForUpload]) { // Attachment files are non-critical so any error uploading them is recoverable. - [weakSelf saveNextFileToCloudWithCompletion:completion]; - return YES; + return resolve(@(1)); } OWSAssertDebug(attachmentExport.relativeFilePath.length > 0); OWSAssertDebug(attachmentExport.encryptedItem); @@ -877,96 +881,81 @@ NS_ASSUME_NONNULL_BEGIN return [NSURL fileURLWithPath:attachmentExport.encryptedItem.filePath]; } success:^(NSString *recordName) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - OWSBackupExportJob *strongSelf = weakSelf; - if (!strongSelf) { - return; - } + OWSBackupExportJob *strongSelf = weakSelf; + if (!strongSelf) { + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); + } - if (![attachmentExport cleanUp]) { - OWSLogError(@"couldn't clean up attachment export."); - // Attachment files are non-critical so any error uploading them is recoverable. - } + if (![attachmentExport cleanUp]) { + OWSLogError(@"couldn't clean up attachment export."); + // Attachment files are non-critical so any error uploading them is recoverable. + } - OWSBackupExportItem *exportItem = [OWSBackupExportItem new]; - exportItem.encryptedItem = attachmentExport.encryptedItem; - exportItem.recordName = recordName; - exportItem.attachmentExport = attachmentExport; - [strongSelf.savedAttachmentItems addObject:exportItem]; - - // Immediately save the record metadata to facilitate export resume. - OWSBackupFragment *backupFragment = [OWSBackupFragment new]; - backupFragment.recordName = recordName; - backupFragment.encryptionKey = exportItem.encryptedItem.encryptionKey; - backupFragment.relativeFilePath = attachmentExport.relativeFilePath; - backupFragment.attachmentId = attachmentExport.attachmentId; - backupFragment.uncompressedDataLength = exportItem.uncompressedDataLength; - [backupFragment save]; - - OWSLogVerbose(@"saved attachment: %@ as %@", - attachmentExport.attachmentFilePath, - attachmentExport.relativeFilePath); - [strongSelf saveNextFileToCloudWithCompletion:completion]; - }); + OWSBackupExportItem *exportItem = [OWSBackupExportItem new]; + exportItem.encryptedItem = attachmentExport.encryptedItem; + exportItem.recordName = recordName; + exportItem.attachmentExport = attachmentExport; + [strongSelf.savedAttachmentItems addObject:exportItem]; + + // Immediately save the record metadata to facilitate export resume. + OWSBackupFragment *backupFragment = [OWSBackupFragment new]; + backupFragment.recordName = recordName; + backupFragment.encryptionKey = exportItem.encryptedItem.encryptionKey; + backupFragment.relativeFilePath = attachmentExport.relativeFilePath; + backupFragment.attachmentId = attachmentExport.attachmentId; + backupFragment.uncompressedDataLength = exportItem.uncompressedDataLength; + [backupFragment save]; + + OWSLogVerbose( + @"saved attachment: %@ as %@", attachmentExport.attachmentFilePath, attachmentExport.relativeFilePath); + return resolve(@(1)); } failure:^(NSError *error) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (![attachmentExport cleanUp]) { - OWSLogError(@"couldn't clean up attachment export."); - // Attachment files are non-critical so any error uploading them is recoverable. - } - + if (![attachmentExport cleanUp]) { + OWSLogError(@"couldn't clean up attachment export."); // Attachment files are non-critical so any error uploading them is recoverable. - [weakSelf saveNextFileToCloudWithCompletion:completion]; - }); - }]; + } - return YES; + // Attachment files are non-critical so any error uploading them is recoverable. + return resolve(@(1)); + }]; } -- (void)saveManifestFileToCloudWithCompletion:(OWSBackupJobCompletion)completion +- (AnyPromise *)saveManifestFileToCloud { - OWSAssertDebug(completion); + if (self.isComplete) { + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; + } OWSBackupEncryptedItem *_Nullable encryptedItem = [self writeManifestFile]; if (!encryptedItem) { - completion(OWSErrorWithCodeDescription(OWSErrorCodeExportBackupFailed, - NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT", - @"Error indicating the backup export could not export the user's data."))); - return; + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not generate manifest.")]; } OWSBackupExportItem *exportItem = [OWSBackupExportItem new]; exportItem.encryptedItem = encryptedItem; __weak OWSBackupExportJob *weakSelf = self; - - [OWSBackupAPI upsertManifestFileToCloudWithRecipientId:self.recipientId - fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath] - success:^(NSString *recordName) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [OWSBackupAPI upsertManifestFileToCloudWithRecipientId:self.recipientId + fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath] + success:^(NSString *recordName) { OWSBackupExportJob *strongSelf = weakSelf; if (!strongSelf) { - return; + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); } exportItem.recordName = recordName; strongSelf.manifestItem = exportItem; // All files have been saved to the cloud. - completion(nil); - }); - } - failure:^(NSError *error) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + resolve(@(1)); + } + failure:^(NSError *error) { // The manifest file is critical so any error uploading them is unrecoverable. - completion(error); - }); - }]; + resolve(error); + }]; + }]; } - (nullable OWSBackupEncryptedItem *)writeManifestFile @@ -1020,13 +1009,11 @@ NS_ASSUME_NONNULL_BEGIN return result; } -- (void)cleanUpWithCompletion:(OWSBackupJobCompletion)completion +- (AnyPromise *)cleanUp { - OWSAssertDebug(completion); - if (self.isComplete) { // Job was aborted. - return completion(nil); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; } OWSLogVerbose(@""); @@ -1062,7 +1049,7 @@ NS_ASSUME_NONNULL_BEGIN [self cleanUpMetadataCacheWithActiveRecordNames:activeRecordNames]; - [self cleanUpCloudWithActiveRecordNames:activeRecordNames completion:completion]; + return [self cleanUpCloudWithActiveRecordNames:activeRecordNames]; } - (void)cleanUpMetadataCacheWithActiveRecordNames:(NSSet *)activeRecordNames @@ -1086,22 +1073,23 @@ NS_ASSUME_NONNULL_BEGIN }]; } -- (void)cleanUpCloudWithActiveRecordNames:(NSSet *)activeRecordNames - completion:(OWSBackupJobCompletion)completion +- (AnyPromise *)cleanUpCloudWithActiveRecordNames:(NSSet *)activeRecordNames { OWSAssertDebug(activeRecordNames.count > 0); - OWSAssertDebug(completion); if (self.isComplete) { // Job was aborted. - return completion(nil); + return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")]; } __weak OWSBackupExportJob *weakSelf = self; - [OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId - success:^(NSArray *recordNames) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId + success:^(NSArray *recordNames) { + OWSBackupExportJob *strongSelf = weakSelf; + if (!strongSelf) { + return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active.")); + } NSMutableSet *obsoleteRecordNames = [NSMutableSet new]; [obsoleteRecordNames addObjectsFromArray:recordNames]; [obsoleteRecordNames minusSet:activeRecordNames]; @@ -1113,16 +1101,16 @@ NS_ASSUME_NONNULL_BEGIN [weakSelf deleteRecordsFromCloud:[obsoleteRecordNames.allObjects mutableCopy] deletedCount:0 - completion:completion]; - }); - } - 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:^(NSError *_Nullable error) { + // Cloud cleanup is non-critical so any error is recoverable. + resolve(@(1)); + }]; + } + failure:^(NSError *error) { // Cloud cleanup is non-critical so any error is recoverable. - completion(nil); - }); - }]; + resolve(@(1)); + }]; + }]; } - (void)deleteRecordsFromCloud:(NSMutableArray *)obsoleteRecordNames @@ -1160,21 +1148,15 @@ NS_ASSUME_NONNULL_BEGIN __weak OWSBackupExportJob *weakSelf = self; [OWSBackupAPI deleteRecordsFromCloudWithRecordNames:batchRecordNames success:^{ - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [weakSelf deleteRecordsFromCloud:obsoleteRecordNames - deletedCount:deletedCount + batchRecordNames.count - completion:completion]; - }); + [weakSelf deleteRecordsFromCloud:obsoleteRecordNames + deletedCount:deletedCount + batchRecordNames.count + completion:completion]; } failure:^(NSError *error) { - // Ensure that we continue to work off the main thread. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // Cloud cleanup is non-critical so any error is recoverable. - [weakSelf deleteRecordsFromCloud:obsoleteRecordNames - deletedCount:deletedCount + batchRecordNames.count - completion:completion]; - }); + // Cloud cleanup is non-critical so any error is recoverable. + [weakSelf deleteRecordsFromCloud:obsoleteRecordNames + deletedCount:deletedCount + batchRecordNames.count + completion:completion]; }]; }