Backup local profile.

This commit is contained in:
Matthew Chen 2018-11-28 15:49:45 -05:00
parent d6ca969c62
commit 3acfa707d9
6 changed files with 299 additions and 294 deletions

View File

@ -4,10 +4,8 @@
<dict>
<key>BuildDetails</key>
<dict>
<key>CarthageVersion</key>
<string>0.31.2</string>
<key>OSXVersion</key>
<string>10.14.1</string>
<string>10.13.6</string>
<key>WebRTCCommit</key>
<string>ca71024b4993ba95e3e6b8d0758004cffc54ddaf M70</string>
</dict>

View File

@ -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

View File

@ -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<Data> {
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) {
public class func downloadDataFromCloudObjc(recordName: String) -> AnyPromise {
return AnyPromise(downloadDataFromCloud(recordName: recordName))
}
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 downloadDataFromCloud(recordName: String) -> Promise<Data> {
return downloadFromCloud(recordName: recordName,
remainingRetries: maxRetries)
.then { (asset) -> Promise<Data> 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) {
toFileUrl: URL) -> Promise<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)
return downloadFromCloud(recordName: recordName,
remainingRetries: maxRetries)
.then { (asset) -> Promise<Void> 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<CKAsset> {
let (promise, resolver) = Promise<CKAsset>.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

View File

@ -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<OWSBackupFragment *> *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;
}
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];
if (weakSelf.isComplete) {
return;
}
[self.profileManager fetchLocalUsersProfile];
[self.tsAccountManager updateAccountAttributes];
[self succeed];
[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 [AnyPromise promiseWithValue:@(1)];
});
}
- (BOOL)configureImport
@ -230,80 +195,134 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
return YES;
}
- (void)downloadFilesFromCloud:(NSMutableArray<OWSBackupFragment *> *)items
completion:(OWSBackupJobCompletion)completion
- (AnyPromise *)downloadFilesFromCloud:(NSMutableArray<OWSBackupFragment *> *)items
{
OWSAssertDebug(items.count > 0);
OWSAssertDebug(completion);
OWSLogVerbose(@"");
[self downloadNextItemFromCloud:items recordCount:items.count completion:completion];
}
- (void)downloadNextItemFromCloud:(NSMutableArray<OWSBackupFragment *> *)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);
}
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)];
// 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];
item.downloadFilePath = tempFilePath;
[self downloadNextItemFromCloud:items recordCount:recordCount completion:completion];
return;
return [AnyPromise promiseWithValue:@(1)];
}
__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;
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)];
[weakSelf downloadNextItemFromCloud:items recordCount:recordCount completion:completion];
});
return [AnyPromise promiseWithValue:@(1)];
});
// 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];
item.downloadFilePath = tempFilePath;
continue;
}
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);
});
}];
promise = promise.thenInBackground(^{
return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:item.recordName
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
.thenInBackground(^{
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
item.downloadFilePath = tempFilePath;
});
});
}
return promise;
}
- (void)restoreAttachmentFiles
- (AnyPromise *)restoreLocalProfile
{
OWSLogVerbose(@": %zd", self.attachmentsItems.count);
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;
}
}
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)];
}
}
- (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

View File

@ -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

View File

@ -5,6 +5,7 @@
#import "OWSBackupJob.h"
#import "OWSBackupIO.h"
#import "Signal-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/Randomness.h>
#import <YapDatabase/YapDatabaseCryptoUtils.h>
@ -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<OWSBackupFragment *> *_Nullable databaseItems =
[self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
if (!databaseItems) {
return failure();
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")];
}
NSArray<OWSBackupFragment *> *_Nullable attachmentsItems =
[self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
if (!attachmentsItems) {
return failure();
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")];
}
NSArray<OWSBackupFragment *> *_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