Merge branch 'charlesmchen/backupLocalProfile'

pull/1/head
Matthew Chen 6 years ago
commit 08cd514e65

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

@ -754,7 +754,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description)
OWSFailDebug(@"Could not load database view.");
return;
}
[ext enumerateKeysInGroup:TSLazyRestoreAttachmentsGroup
usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) {
[attachmentIds addObject:key];
@ -763,22 +763,20 @@ NSError *OWSBackupErrorWithDescription(NSString *description)
return attachmentIds;
}
- (void)lazyRestoreAttachment:(TSAttachmentPointer *)attachment
backupIO:(OWSBackupIO *)backupIO
completion:(OWSBackupBoolBlock)completion
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO
{
OWSAssertDebug(attachment);
OWSAssertDebug(backupIO);
OWSAssertDebug(completion);
OWSBackupFragment *_Nullable lazyRestoreFragment = attachment.lazyRestoreFragment;
if (!lazyRestoreFragment) {
OWSLogWarn(@"Attachment missing lazy restore metadata.");
return completion(NO);
OWSLogError(@"Attachment missing lazy restore metadata.");
return
[AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment missing lazy restore metadata.")];
}
if (lazyRestoreFragment.recordName.length < 1 || lazyRestoreFragment.encryptionKey.length < 1) {
OWSLogError(@"Incomplete lazy restore metadata.");
return completion(NO);
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Incomplete lazy restore metadata.")];
}
// Use a predictable file path so that multiple "import backup" attempts
@ -787,40 +785,30 @@ NSError *OWSBackupErrorWithDescription(NSString *description)
// TODO: This will also require imports using a predictable jobTempDirPath.
NSString *tempFilePath = [backupIO generateTempFilePath];
[OWSBackupAPI downloadFileFromCloudWithRecordName:lazyRestoreFragment.recordName
toFileUrl:[NSURL fileURLWithPath:tempFilePath]
success:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self lazyRestoreAttachment:attachment
backupIO:backupIO
encryptedFilePath:tempFilePath
encryptionKey:lazyRestoreFragment.encryptionKey
completion:completion];
});
}
failure:^(NSError *error) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(NO);
});
}];
return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:lazyRestoreFragment.recordName
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
.thenInBackground(^{
return [self lazyRestoreAttachment:attachment
backupIO:backupIO
encryptedFilePath:tempFilePath
encryptionKey:lazyRestoreFragment.encryptionKey];
});
}
- (void)lazyRestoreAttachment:(TSAttachmentPointer *)attachmentPointer
backupIO:(OWSBackupIO *)backupIO
encryptedFilePath:(NSString *)encryptedFilePath
encryptionKey:(NSData *)encryptionKey
completion:(OWSBackupBoolBlock)completion
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachmentPointer
backupIO:(OWSBackupIO *)backupIO
encryptedFilePath:(NSString *)encryptedFilePath
encryptionKey:(NSData *)encryptionKey
{
OWSAssertDebug(attachmentPointer);
OWSAssertDebug(backupIO);
OWSAssertDebug(encryptedFilePath.length > 0);
OWSAssertDebug(encryptionKey.length > 0);
OWSAssertDebug(completion);
NSData *_Nullable data = [NSData dataWithContentsOfFile:encryptedFilePath];
if (!data) {
OWSLogError(@"Could not load encrypted file.");
return completion(NO);
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load encrypted file.")];
}
NSString *decryptedFilePath = [backupIO generateTempFilePath];
@ -828,7 +816,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description)
@autoreleasepool {
if (![backupIO decryptFileAsFile:encryptedFilePath dstFilePath:decryptedFilePath encryptionKey:encryptionKey]) {
OWSLogError(@"Could not load decrypt file.");
return completion(NO);
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load decrypt file.")];
}
}
@ -837,18 +825,20 @@ NSError *OWSBackupErrorWithDescription(NSString *description)
NSString *attachmentFilePath = stream.originalFilePath;
if (attachmentFilePath.length < 1) {
OWSLogError(@"Attachment has invalid file path.");
return completion(NO);
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment has invalid file path.")];
}
NSString *attachmentDirPath = [attachmentFilePath stringByDeletingLastPathComponent];
if (![OWSFileSystem ensureDirectoryExists:attachmentDirPath]) {
OWSLogError(@"Couldn't create directory for attachment file.");
return completion(NO);
return [AnyPromise
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't create directory for attachment file.")];
}
if (![OWSFileSystem deleteFileIfExists:attachmentFilePath]) {
OWSFailDebug(@"Couldn't delete existing file at attachment path.");
return completion(NO);
return [AnyPromise
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't delete existing file at attachment path.")];
}
NSError *error;
@ -856,7 +846,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description)
[NSFileManager.defaultManager moveItemAtPath:decryptedFilePath toPath:attachmentFilePath error:&error];
if (!success || error) {
OWSLogError(@"Attachment file could not be restored: %@.", error);
return completion(NO);
return [AnyPromise promiseWithValue:error];
}
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -864,7 +854,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description)
[stream saveWithTransaction:transaction];
}];
completion(YES);
return [AnyPromise promiseWithValue:@(1)];
}
#pragma mark - Notifications

@ -62,14 +62,14 @@ import PromiseKit
// We wouldn't want to overwrite previous images until the entire backup export is
// complete.
@objc
public class func saveEphemeralDatabaseFileToCloudObjc(recipientId: String,
fileUrl: URL) -> AnyPromise {
return AnyPromise(saveEphemeralDatabaseFileToCloud(recipientId: recipientId,
fileUrl: fileUrl))
public class func saveEphemeralFileToCloudObjc(recipientId: String,
fileUrl: URL) -> AnyPromise {
return AnyPromise(saveEphemeralFileToCloud(recipientId: recipientId,
fileUrl: fileUrl))
}
public class func saveEphemeralDatabaseFileToCloud(recipientId: String,
fileUrl: URL) -> Promise<String> {
public class func saveEphemeralFileToCloud(recipientId: String,
fileUrl: URL) -> Promise<String> {
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeralFile-\(NSUUID().uuidString)"
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
@ -582,57 +582,57 @@ import PromiseKit
// MARK: - Download
@objc
public class func downloadManifestFromCloud(recipientId: String,
success: @escaping (Data) -> Void,
failure: @escaping (Error) -> Void) {
public class func downloadManifestFromCloudObjc(recipientId: String) -> AnyPromise {
return AnyPromise(downloadManifestFromCloud(recipientId: recipientId))
}
public class func downloadManifestFromCloud(recipientId: String) -> Promise<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) {
downloadFromCloud(recordName: recordName,
remainingRetries: maxRetries,
success: { (asset) in
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: asset.fileURL)
success(data)
} catch {
Logger.error("couldn't load asset file: \(error).")
failure(invalidServiceResponseError())
}
}
},
failure: failure)
public class func downloadDataFromCloudObjc(recordName: String) -> AnyPromise {
return AnyPromise(downloadDataFromCloud(recordName: recordName))
}
public class func downloadDataFromCloud(recordName: String) -> Promise<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) {
downloadFromCloud(recordName: recordName,
remainingRetries: maxRetries,
success: { (asset) in
DispatchQueue.global().async {
do {
try FileManager.default.copyItem(at: asset.fileURL, to: toFileUrl)
success()
} catch {
Logger.error("couldn't copy asset file: \(error).")
failure(invalidServiceResponseError())
}
}
},
failure: failure)
toFileUrl: URL) -> Promise<Void> {
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

@ -308,6 +308,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) NSMutableArray<OWSBackupExportItem *> *savedAttachmentItems;
@property (nonatomic, nullable) OWSBackupExportItem *localProfileAvatarItem;
@property (nonatomic, nullable) OWSBackupExportItem *manifestItem;
// If we are replacing an existing backup, we use some of its contents for continuity.
@ -335,6 +337,11 @@ NS_ASSUME_NONNULL_BEGIN
return AppEnvironment.shared.backup;
}
- (OWSProfileManager *)profileManager
{
return [OWSProfileManager sharedManager];
}
#pragma mark -
- (void)start
@ -457,7 +464,7 @@ NS_ASSUME_NONNULL_BEGIN
NSError *error = OWSBackupErrorWithDescription(@"Backup export failed.");
return resolve(error);
}
resolve(@(1));
}];
}
@ -722,6 +729,9 @@ NS_ASSUME_NONNULL_BEGIN
.thenInBackground(^{
return [self saveDatabaseFilesToCloud];
})
.thenInBackground(^{
return [self saveLocalProfileAvatarToCloud];
})
.thenInBackground(^{
return [self saveManifestFileToCloud];
});
@ -745,7 +755,7 @@ NS_ASSUME_NONNULL_BEGIN
}
return [OWSBackupAPI
saveEphemeralDatabaseFileToCloudObjcWithRecipientId:self.recipientId
saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:item.encryptedItem
.filePath]];
})
@ -876,6 +886,36 @@ NS_ASSUME_NONNULL_BEGIN
});
}
- (AnyPromise *)saveLocalProfileAvatarToCloud
{
if (self.isComplete) {
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")];
}
NSData *_Nullable localProfileAvatarData = self.profileManager.localProfileAvatarData;
if (localProfileAvatarData.length < 1) {
// No profile avatar to backup.
return [AnyPromise promiseWithValue:@(1)];
}
OWSBackupEncryptedItem *_Nullable encryptedItem =
[self.backupIO encryptDataAsTempFile:localProfileAvatarData encryptionKey:self.delegate.backupEncryptionKey];
if (!encryptedItem) {
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not encrypt local profile avatar.")];
}
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = encryptedItem;
return [OWSBackupAPI saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath]]
.thenInBackground(^(NSString *recordName) {
exportItem.recordName = recordName;
self.localProfileAvatarItem = exportItem;
return [AnyPromise promiseWithValue:@(1)];
});
}
- (AnyPromise *)saveManifestFileToCloud
{
if (self.isComplete) {
@ -907,10 +947,19 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(self.jobTempDirPath.length > 0);
OWSAssertDebug(self.backupIO);
NSDictionary *json = @{
NSMutableDictionary *json = [@{
kOWSBackup_ManifestKey_DatabaseFiles : [self jsonForItems:self.savedDatabaseItems],
kOWSBackup_ManifestKey_AttachmentFiles : [self jsonForItems:self.savedAttachmentItems],
};
} mutableCopy];
NSString *_Nullable localProfileName = self.profileManager.localProfileName;
if (localProfileName.length > 0) {
json[kOWSBackup_ManifestKey_LocalProfileName] = localProfileName;
}
if (self.localProfileAvatarItem) {
json[kOWSBackup_ManifestKey_LocalProfileAvatar] = [self jsonForItems:@[ self.localProfileAvatarItem ]];
}
OWSLogVerbose(@"json: %@", json);
@ -1010,7 +1059,7 @@ NS_ASSUME_NONNULL_BEGIN
NSMutableSet<NSString *> *obsoleteRecordNames = [NSMutableSet new];
[obsoleteRecordNames addObjectsFromArray:[transaction allKeysInCollection:[OWSBackupFragment collection]]];
[obsoleteRecordNames minusSet:activeRecordNames];
[transaction removeObjectsForKeys:obsoleteRecordNames.allObjects inCollection:[OWSBackupFragment collection]];
}];
}

@ -27,8 +27,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
@property (nonatomic) OWSBackupIO *backupIO;
@property (nonatomic) NSArray<OWSBackupFragment *> *databaseItems;
@property (nonatomic) NSArray<OWSBackupFragment *> *attachmentsItems;
@property (nonatomic) OWSBackupManifestContents *manifest;
@end
@ -66,6 +65,20 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
#pragma mark -
- (NSArray<OWSBackupFragment *> *)databaseItems
{
OWSAssertDebug(self.manifest);
return self.manifest.databaseItems;
}
- (NSArray<OWSBackupFragment *> *)attachmentsItems
{
OWSAssertDebug(self.manifest);
return self.manifest.attachmentsItems;
}
- (void)startAsync
{
OWSAssertIsOnMainThread();
@ -76,15 +89,14 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
[self updateProgressWithDescription:nil progress:nil];
__weak OWSBackupImportJob *weakSelf = self;
[[self.backup ensureCloudKitAccess]
.thenInBackground(^{
[weakSelf start];
[self start];
})
.catch(^(NSError *error) {
[weakSelf failWithErrorDescription:
NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
@"Error indicating the backup import could not import the user's data.")];
[self failWithErrorDescription:
NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
@"Error indicating the backup import could not import the user's data.")];
}) retainUntilComplete];
}
@ -108,36 +120,32 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
@"Indicates that the backup import data is being imported.")
progress:nil];
__weak OWSBackupImportJob *weakSelf = self;
[weakSelf
downloadAndProcessManifestWithSuccess:^(OWSBackupManifestContents *manifest) {
OWSBackupImportJob *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (self.isComplete) {
return;
}
OWSCAssertDebug(manifest.databaseItems.count > 0);
OWSCAssertDebug(manifest.attachmentsItems);
strongSelf.databaseItems = manifest.databaseItems;
strongSelf.attachmentsItems = manifest.attachmentsItems;
[strongSelf downloadAndProcessImport];
}
failure:^(NSError *manifestError) {
[weakSelf failWithError:manifestError];
}
backupIO:self.backupIO];
[[self downloadAndProcessManifestWithBackupIO:self.backupIO]
.thenInBackground(^(OWSBackupManifestContents *manifest) {
OWSCAssertDebug(manifest.databaseItems.count > 0);
OWSCAssertDebug(manifest.attachmentsItems);
self.manifest = manifest;
return [self downloadAndProcessImport];
})
.catchInBackground(^(NSError *error) {
[self failWithError:error];
}) retainUntilComplete];
}
- (void)downloadAndProcessImport
- (AnyPromise *)downloadAndProcessImport
{
OWSAssertDebug(self.databaseItems);
OWSAssertDebug(self.attachmentsItems);
NSMutableArray<OWSBackupFragment *> *allItems = [NSMutableArray new];
[allItems addObjectsFromArray:self.databaseItems];
// TODO: We probably want to remove this.
[allItems addObjectsFromArray:self.attachmentsItems];
if (self.manifest.localProfileAvatarItem) {
[allItems addObject:self.manifest.localProfileAvatarItem];
}
// Record metadata for all items, so that we can re-use them in incremental backups after the restore.
[self.primaryStorage.newDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -146,62 +154,29 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
}
}];
__weak OWSBackupImportJob *weakSelf = self;
[weakSelf
downloadFilesFromCloud:allItems
completion:^(NSError *_Nullable fileDownloadError) {
if (fileDownloadError) {
[weakSelf failWithError:fileDownloadError];
return;
}
if (weakSelf.isComplete) {
return;
}
[weakSelf restoreDatabaseWithCompletion:^(BOOL restoreDatabaseSuccess) {
if (!restoreDatabaseSuccess) {
[weakSelf
failWithErrorDescription:NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
@"Error indicating the backup import "
@"could not import the user's data.")];
return;
}
if (weakSelf.isComplete) {
return;
}
[weakSelf ensureMigrationsWithCompletion:^(BOOL ensureMigrationsSuccess) {
if (!ensureMigrationsSuccess) {
[weakSelf failWithErrorDescription:NSLocalizedString(
@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
@"Error indicating the backup import "
@"could not import the user's data.")];
return;
}
if (weakSelf.isComplete) {
return;
}
[weakSelf restoreAttachmentFiles];
if (weakSelf.isComplete) {
return;
}
[weakSelf.profileManager fetchLocalUsersProfile];
[weakSelf.tsAccountManager updateAccountAttributes];
// Kick off lazy restore.
[OWSBackupLazyRestoreJob runAsync];
[weakSelf succeed];
}];
}];
}];
return [self downloadFilesFromCloud:allItems]
.thenInBackground(^{
return [self restoreDatabase];
})
.thenInBackground(^{
return [self ensureMigrations];
})
.thenInBackground(^{
return [self restoreLocalProfile];
})
.thenInBackground(^{
return [self restoreAttachmentFiles];
})
.thenInBackground(^{
// Kick off lazy restore.
[OWSBackupLazyRestoreJob runAsync];
[self.profileManager fetchLocalUsersProfile];
[self.tsAccountManager updateAccountAttributes];
[self succeed];
});
}
- (BOOL)configureImport
@ -218,80 +193,132 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
return YES;
}
- (void)downloadFilesFromCloud:(NSMutableArray<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);
return [AnyPromise promiseWithValue:@(1)];
}
OWSBackupFragment *item = items.lastObject;
[items removeLastObject];
CGFloat progress = (recordCount > 0 ? ((recordCount - items.count) / (CGFloat)recordCount) : 0.f);
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_DOWNLOAD",
@"Indicates that the backup import data is being downloaded.")
progress:@(progress)];
AnyPromise *promise = [AnyPromise promiseWithValue:@(1)];
for (OWSBackupFragment *item in items) {
promise = promise.thenInBackground(^{
CGFloat progress = (recordCount > 0 ? ((recordCount - items.count) / (CGFloat)recordCount) : 0.f);
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_DOWNLOAD",
@"Indicates that the backup import data is being downloaded.")
progress:@(progress)];
});
// TODO: Use a predictable file path so that multiple "import backup" attempts
// will leverage successful file downloads from previous attempts.
//
// TODO: This will also require imports using a predictable jobTempDirPath.
NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:item.recordName];
// TODO: Use a predictable file path so that multiple "import backup" attempts
// will leverage successful file downloads from previous attempts.
//
// TODO: This will also require imports using a predictable jobTempDirPath.
NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:item.recordName];
// Skip redundant file download.
if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) {
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
// Skip redundant file download.
if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) {
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
item.downloadFilePath = tempFilePath;
item.downloadFilePath = tempFilePath;
continue;
}
[self downloadNextItemFromCloud:items recordCount:recordCount completion:completion];
return;
promise = promise.thenInBackground(^{
return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:item.recordName
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
.thenInBackground(^{
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
item.downloadFilePath = tempFilePath;
});
});
}
__weak OWSBackupImportJob *weakSelf = self;
[OWSBackupAPI downloadFileFromCloudWithRecordName:item.recordName
toFileUrl:[NSURL fileURLWithPath:tempFilePath]
success:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
item.downloadFilePath = tempFilePath;
return promise;
}
- (AnyPromise *)restoreLocalProfile
{
OWSLogVerbose(@": %zd", self.attachmentsItems.count);
[weakSelf downloadNextItemFromCloud:items recordCount:recordCount completion:completion];
});
if (self.isComplete) {
// Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
}
NSString *_Nullable localProfileName = self.manifest.localProfileName;
UIImage *_Nullable localProfileAvatar = nil;
if (self.manifest.localProfileAvatarItem) {
OWSBackupFragment *item = self.manifest.localProfileAvatarItem;
if (item.recordName.length < 1) {
OWSLogError(@"local profile avatar was not downloaded.");
// Ignore errors related to local profile.
return [AnyPromise promiseWithValue:@(1)];
}
if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) {
OWSLogError(@"database snapshot missing size.");
// Ignore errors related to local profile.
return [AnyPromise promiseWithValue:@(1)];
}
@autoreleasepool {
NSData *_Nullable data =
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
if (!data) {
OWSLogError(@"could not decrypt local profile avatar.");
// Ignore errors related to local profile.
return [AnyPromise promiseWithValue:@(1)];
}
// TODO: Verify that we're not compressing the profile avatar data.
UIImage *_Nullable image = [UIImage imageWithData:data];
if (!image) {
OWSLogError(@"could not decrypt local profile avatar.");
// Ignore errors related to local profile.
return [AnyPromise promiseWithValue:@(1)];
}
localProfileAvatar = image;
}
failure:^(NSError *error) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(error);
});
}
if (localProfileName.length > 0 || localProfileAvatar) {
AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self.profileManager updateLocalProfileName:localProfileName
avatarImage:localProfileAvatar
success:^{
resolve(@(1));
}
failure:^{
// Ignore errors related to local profile.
resolve(@(1));
}];
}];
return promise;
} else {
return [AnyPromise promiseWithValue:@(1)];
}
}
- (void)restoreAttachmentFiles
- (AnyPromise *)restoreAttachmentFiles
{
OWSLogVerbose(@": %zd", self.attachmentsItems.count);
if (self.isComplete) {
// Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
}
__block NSUInteger count = 0;
YapDatabaseConnection *dbConnection = self.primaryStorage.newDatabaseConnection;
[dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -335,22 +362,23 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
}];
OWSLogError(@"enqueued lazy restore of %zd files.", count);
return [AnyPromise promiseWithValue:@(1)];
}
- (void)restoreDatabaseWithCompletion:(OWSBackupJobBoolCompletion)completion
- (AnyPromise *)restoreDatabase
{
OWSAssertDebug(completion);
OWSLogVerbose(@"");
if (self.isComplete) {
return completion(NO);
// Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
}
YapDatabaseConnection *_Nullable dbConnection = self.primaryStorage.newDatabaseConnection;
if (!dbConnection) {
OWSFailDebug(@"Could not create dbConnection.");
return completion(NO);
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not create dbConnection.")];
}
// Order matters here.
@ -399,14 +427,14 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
// Attachment-related errors are recoverable and can be ignored.
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) {
OWSLogError(@"database snapshot missing size.");
// Attachment-related errors are recoverable and can be ignored.
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
count++;
@ -420,7 +448,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
if (!compressedData) {
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
NSData *_Nullable uncompressedData =
[self.backupIO decompressData:compressedData
@ -428,7 +456,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
if (!uncompressedData) {
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
NSError *error;
SignalIOSProtoBackupSnapshot *_Nullable entities =
@ -437,13 +465,13 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSLogError(@"could not parse proto: %@.", error);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
if (!entities || entities.entity.count < 1) {
OWSLogError(@"missing entities.");
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
for (SignalIOSProtoBackupSnapshotBackupEntity *entity in entities.entity) {
NSData *_Nullable entityData = entity.entityData;
@ -451,7 +479,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSLogError(@"missing entity data.");
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
NSString *_Nullable collection = entity.collection;
@ -459,7 +487,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSLogError(@"missing collection.");
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
NSString *_Nullable key = entity.key;
@ -467,7 +495,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSLogError(@"missing key.");
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
__block NSObject *object = nil;
@ -478,13 +506,13 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSLogError(@"invalid decoded entity: %@.", [object class]);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
} @catch (NSException *exception) {
OWSLogError(@"could not decode entity.");
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
return;
}
[transaction setObject:object forKey:key inCollection:collection];
@ -496,8 +524,12 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
}
}];
if (self.isComplete || aborted) {
return;
if (aborted) {
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import failed.")];
}
if (self.isComplete) {
// Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
}
for (NSString *collection in restoredEntityCounts) {
@ -507,15 +539,18 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
[self.primaryStorage logFileSizes];
completion(YES);
return [AnyPromise promiseWithValue:@(1)];
}
- (void)ensureMigrationsWithCompletion:(OWSBackupJobBoolCompletion)completion
- (AnyPromise *)ensureMigrations
{
OWSAssertDebug(completion);
OWSLogVerbose(@"");
if (self.isComplete) {
// Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
}
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_FINALIZING",
@"Indicates that the backup import data is being finalized.")
progress:nil];
@ -524,11 +559,14 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
// It's okay that we do this in a separate transaction from the
// restoration of backup contents. If some of migrations don't
// complete, they'll be run the next time the app launches.
dispatch_async(dispatch_get_main_queue(), ^{
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:^{
completion(YES);
}];
});
AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
dispatch_async(dispatch_get_main_queue(), ^{
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:^{
resolve(@(1));
}];
});
}];
return promise;
}
@end

@ -14,7 +14,10 @@ extern NSString *const kOWSBackup_ManifestKey_EncryptionKey;
extern NSString *const kOWSBackup_ManifestKey_RelativeFilePath;
extern NSString *const kOWSBackup_ManifestKey_AttachmentId;
extern NSString *const kOWSBackup_ManifestKey_DataSize;
extern NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar;
extern NSString *const kOWSBackup_ManifestKey_LocalProfileName;
@class AnyPromise;
@class OWSBackupIO;
@class OWSBackupJob;
@class OWSBackupManifestContents;
@ -28,6 +31,8 @@ typedef void (^OWSBackupJobManifestFailure)(NSError *error);
@property (nonatomic) NSArray<OWSBackupFragment *> *databaseItems;
@property (nonatomic) NSArray<OWSBackupFragment *> *attachmentsItems;
@property (nonatomic, nullable) OWSBackupFragment *localProfileAvatarItem;
@property (nonatomic, nullable) NSString *localProfileName;
@end
@ -80,9 +85,7 @@ typedef void (^OWSBackupJobManifestFailure)(NSError *error);
#pragma mark - Manifest
- (void)downloadAndProcessManifestWithSuccess:(OWSBackupJobManifestSuccess)success
failure:(OWSBackupJobManifestFailure)failure
backupIO:(OWSBackupIO *)backupIO;
- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO;
@end

@ -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>
@ -17,6 +18,8 @@ NSString *const kOWSBackup_ManifestKey_EncryptionKey = @"encryption_key";
NSString *const kOWSBackup_ManifestKey_RelativeFilePath = @"relative_file_path";
NSString *const kOWSBackup_ManifestKey_AttachmentId = @"attachment_id";
NSString *const kOWSBackup_ManifestKey_DataSize = @"data_size";
NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar = @"local_profile_avatar";
NSString *const kOWSBackup_ManifestKey_LocalProfileName = @"local_profile_name";
NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
@ -108,12 +111,12 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
return;
}
self.isComplete = YES;
// There's a lot of asynchrony in these backup jobs;
// ensure we only end up finishing these jobs once.
OWSAssertDebug(!self.hasSucceeded);
self.hasSucceeded = YES;
[self.delegate backupJobDidSucceed:self];
});
}
@ -151,52 +154,31 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
#pragma mark - Manifest
- (void)downloadAndProcessManifestWithSuccess:(OWSBackupJobManifestSuccess)success
failure:(OWSBackupJobManifestFailure)failure
backupIO:(OWSBackupIO *)backupIO
- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO
{
OWSAssertDebug(success);
OWSAssertDebug(failure);
OWSAssertDebug(backupIO);
OWSLogVerbose(@"");
__weak OWSBackupJob *weakSelf = self;
[OWSBackupAPI downloadManifestFromCloudWithRecipientId:self.recipientId
success:^(NSData *data) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf
processManifest:data
success:success
failure:^{
failure(OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed,
NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
@"Error indicating the backup import could not import the user's data.")));
}
backupIO:backupIO];
});
}
failure:^(NSError *error) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// The manifest file is critical so any error downloading it is unrecoverable.
OWSCFailDebug(@"Could not download manifest.");
failure(error);
});
}];
if (self.isComplete) {
// Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
}
return
[OWSBackupAPI downloadManifestFromCloudObjcWithRecipientId:self.recipientId].thenInBackground(^(NSData *data) {
return [self processManifest:data backupIO:backupIO];
});
}
- (void)processManifest:(NSData *)manifestDataEncrypted
success:(OWSBackupJobManifestSuccess)success
failure:(dispatch_block_t)failure
backupIO:(OWSBackupIO *)backupIO
- (AnyPromise *)processManifest:(NSData *)manifestDataEncrypted backupIO:(OWSBackupIO *)backupIO
{
OWSAssertDebug(manifestDataEncrypted.length > 0);
OWSAssertDebug(success);
OWSAssertDebug(failure);
OWSAssertDebug(backupIO);
if (self.isComplete) {
return;
// Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
}
OWSLogVerbose(@"");
@ -205,7 +187,7 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
[backupIO decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey];
if (!manifestDataDecrypted) {
OWSFailDebug(@"Could not decrypt manifest.");
return failure();
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not decrypt manifest.")];
}
NSError *error;
@ -213,30 +195,56 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
[NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error];
if (![json isKindOfClass:[NSDictionary class]]) {
OWSFailDebug(@"Could not download manifest.");
return failure();
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not download manifest.")];
}
OWSLogVerbose(@"json: %@", json);
NSArray<OWSBackupFragment *> *_Nullable databaseItems =
[self parseItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
[self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
if (!databaseItems) {
return failure();
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")];
}
NSArray<OWSBackupFragment *> *_Nullable attachmentsItems =
[self parseItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
[self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
if (!attachmentsItems) {
return failure();
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")];
}
NSArray<OWSBackupFragment *> *_Nullable localProfileAvatarItems;
if ([self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileAvatar]) {
localProfileAvatarItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_LocalProfileAvatar];
}
NSString *_Nullable localProfileName = [self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileName];
OWSBackupManifestContents *contents = [OWSBackupManifestContents new];
contents.databaseItems = databaseItems;
contents.attachmentsItems = attachmentsItems;
contents.localProfileAvatarItem = localProfileAvatarItems.firstObject;
if ([localProfileName isKindOfClass:[NSString class]]) {
contents.localProfileName = localProfileName;
} else {
OWSFailDebug(@"Invalid localProfileName: %@", [localProfileName class]);
}
return success(contents);
return [AnyPromise promiseWithValue:contents];
}
- (nullable id)parseManifestItem:(id)json key:(NSString *)key
{
OWSAssertDebug(json);
OWSAssertDebug(key.length);
if (![json isKindOfClass:[NSDictionary class]]) {
OWSFailDebug(@"manifest has invalid data.");
return nil;
}
id _Nullable value = json[key];
return value;
}
- (nullable NSArray<OWSBackupFragment *> *)parseItems:(id)json key:(NSString *)key
- (nullable NSArray<OWSBackupFragment *> *)parseManifestItems:(id)json key:(NSString *)key
{
OWSAssertDebug(json);
OWSAssertDebug(key.length);

@ -37,6 +37,7 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
- (BOOL)hasLocalProfile;
- (nullable NSString *)localProfileName;
- (nullable UIImage *)localProfileAvatarImage;
- (nullable NSData *)localProfileAvatarData;
- (void)ensureLocalProfileCached;
// This method is used to update the "local profile" state on the client

@ -217,6 +217,15 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
return [self loadProfileAvatarWithFilename:self.localUserProfile.avatarFileName];
}
- (nullable NSData *)localProfileAvatarData
{
NSString *_Nullable filename = self.localUserProfile.avatarFileName;
if (filename.length < 1) {
return nil;
}
return [self loadProfileDataWithFilename:filename];
}
- (void)updateLocalProfileName:(nullable NSString *)profileName
avatarImage:(nullable UIImage *)avatarImage
success:(void (^)(void))successBlockParameter

Loading…
Cancel
Save