Backup local profile.
This commit is contained in:
parent
d6ca969c62
commit
3acfa707d9
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue