Merge branch 'charlesmchen/backupLocalProfile'

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

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

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

@ -62,14 +62,14 @@ import PromiseKit
// We wouldn't want to overwrite previous images until the entire backup export is // We wouldn't want to overwrite previous images until the entire backup export is
// complete. // complete.
@objc @objc
public class func saveEphemeralDatabaseFileToCloudObjc(recipientId: String, public class func saveEphemeralFileToCloudObjc(recipientId: String,
fileUrl: URL) -> AnyPromise { fileUrl: URL) -> AnyPromise {
return AnyPromise(saveEphemeralDatabaseFileToCloud(recipientId: recipientId, return AnyPromise(saveEphemeralFileToCloud(recipientId: recipientId,
fileUrl: fileUrl)) fileUrl: fileUrl))
} }
public class func saveEphemeralDatabaseFileToCloud(recipientId: String, public class func saveEphemeralFileToCloud(recipientId: String,
fileUrl: URL) -> Promise<String> { fileUrl: URL) -> Promise<String> {
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeralFile-\(NSUUID().uuidString)" let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeralFile-\(NSUUID().uuidString)"
return saveFileToCloud(fileUrl: fileUrl, return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName, recordName: recordName,
@ -582,57 +582,57 @@ import PromiseKit
// MARK: - Download // MARK: - Download
@objc @objc
public class func downloadManifestFromCloud(recipientId: String, public class func downloadManifestFromCloudObjc(recipientId: String) -> AnyPromise {
success: @escaping (Data) -> Void, return AnyPromise(downloadManifestFromCloud(recipientId: recipientId))
failure: @escaping (Error) -> Void) { }
public class func downloadManifestFromCloud(recipientId: String) -> Promise<Data> {
let recordName = recordNameForManifest(recipientId: recipientId) let recordName = recordNameForManifest(recipientId: recipientId)
downloadDataFromCloud(recordName: recordName, return downloadDataFromCloud(recordName: recordName)
success: success,
failure: failure)
} }
@objc @objc
public class func downloadDataFromCloud(recordName: String, public class func downloadDataFromCloudObjc(recordName: String) -> AnyPromise {
success: @escaping (Data) -> Void, return AnyPromise(downloadDataFromCloud(recordName: recordName))
failure: @escaping (Error) -> Void) { }
downloadFromCloud(recordName: recordName, public class func downloadDataFromCloud(recordName: String) -> Promise<Data> {
remainingRetries: maxRetries,
success: { (asset) in return downloadFromCloud(recordName: recordName,
DispatchQueue.global().async { remainingRetries: maxRetries)
do { .then { (asset) -> Promise<Data> in
let data = try Data(contentsOf: asset.fileURL) do {
success(data) let data = try Data(contentsOf: asset.fileURL)
} catch { return Promise.value(data)
Logger.error("couldn't load asset file: \(error).") } catch {
failure(invalidServiceResponseError()) Logger.error("couldn't load asset file: \(error).")
} return Promise(error: invalidServiceResponseError())
} }
}, }
failure: failure)
} }
@objc @objc
public class func downloadFileFromCloudObjc(recordName: String,
toFileUrl: URL) -> AnyPromise {
return AnyPromise(downloadFileFromCloud(recordName: recordName,
toFileUrl: toFileUrl))
}
public class func downloadFileFromCloud(recordName: String, public class func downloadFileFromCloud(recordName: String,
toFileUrl: URL, toFileUrl: URL) -> Promise<Void> {
success: @escaping () -> Void,
failure: @escaping (Error) -> Void) { return downloadFromCloud(recordName: recordName,
remainingRetries: maxRetries)
downloadFromCloud(recordName: recordName, .then { (asset) -> Promise<Void> in
remainingRetries: maxRetries, do {
success: { (asset) in try FileManager.default.copyItem(at: asset.fileURL, to: toFileUrl)
DispatchQueue.global().async { return Promise.value(())
do { } catch {
try FileManager.default.copyItem(at: asset.fileURL, to: toFileUrl) Logger.error("couldn't copy asset file: \(error).")
success() return Promise(error: invalidServiceResponseError())
} catch { }
Logger.error("couldn't copy asset file: \(error).") }
failure(invalidServiceResponseError())
}
}
},
failure: failure)
} }
// We return the CKAsset and not its fileUrl because // We return the CKAsset and not its fileUrl because
@ -641,9 +641,9 @@ import PromiseKit
// defer cleanup by maintaining a strong reference to // defer cleanup by maintaining a strong reference to
// the asset. // the asset.
private class func downloadFromCloud(recordName: String, private class func downloadFromCloud(recordName: String,
remainingRetries: Int, remainingRetries: Int) -> Promise<CKAsset> {
success: @escaping (CKAsset) -> Void,
failure: @escaping (Error) -> Void) { let (promise, resolver) = Promise<CKAsset>.pending()
let recordId = CKRecordID(recordName: recordName) let recordId = CKRecordID(recordName: recordName)
let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ]) let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ])
@ -657,37 +657,45 @@ import PromiseKit
case .success: case .success:
guard let record = record else { guard let record = record else {
Logger.error("missing fetching record.") Logger.error("missing fetching record.")
failure(invalidServiceResponseError()) resolver.reject(invalidServiceResponseError())
return return
} }
guard let asset = record[payloadKey] as? CKAsset else { guard let asset = record[payloadKey] as? CKAsset else {
Logger.error("record missing payload.") Logger.error("record missing payload.")
failure(invalidServiceResponseError()) resolver.reject(invalidServiceResponseError())
return return
} }
success(asset) resolver.fulfill(asset)
case .failureDoNotRetry(let outcomeError): case .failureDoNotRetry(let outcomeError):
failure(outcomeError) resolver.reject(outcomeError)
case .failureRetryAfterDelay(let retryDelay): case .failureRetryAfterDelay(let retryDelay):
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: { DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
downloadFromCloud(recordName: recordName, downloadFromCloud(recordName: recordName,
remainingRetries: remainingRetries - 1, remainingRetries: remainingRetries - 1)
success: success, .done { (asset) in
failure: failure) resolver.fulfill(asset)
}.catch { (error) in
resolver.reject(error)
}.retainUntilComplete()
}) })
case .failureRetryWithoutDelay: case .failureRetryWithoutDelay:
DispatchQueue.global().async { DispatchQueue.global().async {
downloadFromCloud(recordName: recordName, downloadFromCloud(recordName: recordName,
remainingRetries: remainingRetries - 1, remainingRetries: remainingRetries - 1)
success: success, .done { (asset) in
failure: failure) resolver.fulfill(asset)
}.catch { (error) in
resolver.reject(error)
}.retainUntilComplete()
} }
case .unknownItem: case .unknownItem:
Logger.error("missing fetching record.") Logger.error("missing fetching record.")
failure(invalidServiceResponseError()) resolver.reject(invalidServiceResponseError())
} }
} }
database().add(fetchOperation) database().add(fetchOperation)
return promise
} }
// MARK: - Access // MARK: - Access

@ -308,6 +308,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) NSMutableArray<OWSBackupExportItem *> *savedAttachmentItems; @property (nonatomic) NSMutableArray<OWSBackupExportItem *> *savedAttachmentItems;
@property (nonatomic, nullable) OWSBackupExportItem *localProfileAvatarItem;
@property (nonatomic, nullable) OWSBackupExportItem *manifestItem; @property (nonatomic, nullable) OWSBackupExportItem *manifestItem;
// If we are replacing an existing backup, we use some of its contents for continuity. // 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; return AppEnvironment.shared.backup;
} }
- (OWSProfileManager *)profileManager
{
return [OWSProfileManager sharedManager];
}
#pragma mark - #pragma mark -
- (void)start - (void)start
@ -457,7 +464,7 @@ NS_ASSUME_NONNULL_BEGIN
NSError *error = OWSBackupErrorWithDescription(@"Backup export failed."); NSError *error = OWSBackupErrorWithDescription(@"Backup export failed.");
return resolve(error); return resolve(error);
} }
resolve(@(1)); resolve(@(1));
}]; }];
} }
@ -722,6 +729,9 @@ NS_ASSUME_NONNULL_BEGIN
.thenInBackground(^{ .thenInBackground(^{
return [self saveDatabaseFilesToCloud]; return [self saveDatabaseFilesToCloud];
}) })
.thenInBackground(^{
return [self saveLocalProfileAvatarToCloud];
})
.thenInBackground(^{ .thenInBackground(^{
return [self saveManifestFileToCloud]; return [self saveManifestFileToCloud];
}); });
@ -745,7 +755,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
return [OWSBackupAPI return [OWSBackupAPI
saveEphemeralDatabaseFileToCloudObjcWithRecipientId:self.recipientId saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:item.encryptedItem fileUrl:[NSURL fileURLWithPath:item.encryptedItem
.filePath]]; .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 - (AnyPromise *)saveManifestFileToCloud
{ {
if (self.isComplete) { if (self.isComplete) {
@ -907,10 +947,19 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(self.jobTempDirPath.length > 0); OWSAssertDebug(self.jobTempDirPath.length > 0);
OWSAssertDebug(self.backupIO); OWSAssertDebug(self.backupIO);
NSDictionary *json = @{ NSMutableDictionary *json = [@{
kOWSBackup_ManifestKey_DatabaseFiles : [self jsonForItems:self.savedDatabaseItems], kOWSBackup_ManifestKey_DatabaseFiles : [self jsonForItems:self.savedDatabaseItems],
kOWSBackup_ManifestKey_AttachmentFiles : [self jsonForItems:self.savedAttachmentItems], 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); OWSLogVerbose(@"json: %@", json);
@ -1010,7 +1059,7 @@ NS_ASSUME_NONNULL_BEGIN
NSMutableSet<NSString *> *obsoleteRecordNames = [NSMutableSet new]; NSMutableSet<NSString *> *obsoleteRecordNames = [NSMutableSet new];
[obsoleteRecordNames addObjectsFromArray:[transaction allKeysInCollection:[OWSBackupFragment collection]]]; [obsoleteRecordNames addObjectsFromArray:[transaction allKeysInCollection:[OWSBackupFragment collection]]];
[obsoleteRecordNames minusSet:activeRecordNames]; [obsoleteRecordNames minusSet:activeRecordNames];
[transaction removeObjectsForKeys:obsoleteRecordNames.allObjects inCollection:[OWSBackupFragment collection]]; [transaction removeObjectsForKeys:obsoleteRecordNames.allObjects inCollection:[OWSBackupFragment collection]];
}]; }];
} }

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

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

@ -5,6 +5,7 @@
#import "OWSBackupJob.h" #import "OWSBackupJob.h"
#import "OWSBackupIO.h" #import "OWSBackupIO.h"
#import "Signal-Swift.h" #import "Signal-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/Randomness.h> #import <SignalCoreKit/Randomness.h>
#import <YapDatabase/YapDatabaseCryptoUtils.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_RelativeFilePath = @"relative_file_path";
NSString *const kOWSBackup_ManifestKey_AttachmentId = @"attachment_id"; NSString *const kOWSBackup_ManifestKey_AttachmentId = @"attachment_id";
NSString *const kOWSBackup_ManifestKey_DataSize = @"data_size"; 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"; NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
@ -108,12 +111,12 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
return; return;
} }
self.isComplete = YES; self.isComplete = YES;
// There's a lot of asynchrony in these backup jobs; // There's a lot of asynchrony in these backup jobs;
// ensure we only end up finishing these jobs once. // ensure we only end up finishing these jobs once.
OWSAssertDebug(!self.hasSucceeded); OWSAssertDebug(!self.hasSucceeded);
self.hasSucceeded = YES; self.hasSucceeded = YES;
[self.delegate backupJobDidSucceed:self]; [self.delegate backupJobDidSucceed:self];
}); });
} }
@ -151,52 +154,31 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
#pragma mark - Manifest #pragma mark - Manifest
- (void)downloadAndProcessManifestWithSuccess:(OWSBackupJobManifestSuccess)success - (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO
failure:(OWSBackupJobManifestFailure)failure
backupIO:(OWSBackupIO *)backupIO
{ {
OWSAssertDebug(success);
OWSAssertDebug(failure);
OWSAssertDebug(backupIO); OWSAssertDebug(backupIO);
OWSLogVerbose(@""); OWSLogVerbose(@"");
__weak OWSBackupJob *weakSelf = self; if (self.isComplete) {
[OWSBackupAPI downloadManifestFromCloudWithRecipientId:self.recipientId // Job was aborted.
success:^(NSData *data) { return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ }
[weakSelf
processManifest:data return
success:success [OWSBackupAPI downloadManifestFromCloudObjcWithRecipientId:self.recipientId].thenInBackground(^(NSData *data) {
failure:^{ return [self processManifest:data backupIO:backupIO];
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);
});
}];
} }
- (void)processManifest:(NSData *)manifestDataEncrypted - (AnyPromise *)processManifest:(NSData *)manifestDataEncrypted backupIO:(OWSBackupIO *)backupIO
success:(OWSBackupJobManifestSuccess)success
failure:(dispatch_block_t)failure
backupIO:(OWSBackupIO *)backupIO
{ {
OWSAssertDebug(manifestDataEncrypted.length > 0); OWSAssertDebug(manifestDataEncrypted.length > 0);
OWSAssertDebug(success);
OWSAssertDebug(failure);
OWSAssertDebug(backupIO); OWSAssertDebug(backupIO);
if (self.isComplete) { if (self.isComplete) {
return; // Job was aborted.
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
} }
OWSLogVerbose(@""); OWSLogVerbose(@"");
@ -205,7 +187,7 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
[backupIO decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey]; [backupIO decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey];
if (!manifestDataDecrypted) { if (!manifestDataDecrypted) {
OWSFailDebug(@"Could not decrypt manifest."); OWSFailDebug(@"Could not decrypt manifest.");
return failure(); return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not decrypt manifest.")];
} }
NSError *error; NSError *error;
@ -213,30 +195,56 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
[NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error]; [NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error];
if (![json isKindOfClass:[NSDictionary class]]) { if (![json isKindOfClass:[NSDictionary class]]) {
OWSFailDebug(@"Could not download manifest."); OWSFailDebug(@"Could not download manifest.");
return failure(); return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not download manifest.")];
} }
OWSLogVerbose(@"json: %@", json); OWSLogVerbose(@"json: %@", json);
NSArray<OWSBackupFragment *> *_Nullable databaseItems = NSArray<OWSBackupFragment *> *_Nullable databaseItems =
[self parseItems:json key:kOWSBackup_ManifestKey_DatabaseFiles]; [self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
if (!databaseItems) { if (!databaseItems) {
return failure(); return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")];
} }
NSArray<OWSBackupFragment *> *_Nullable attachmentsItems = NSArray<OWSBackupFragment *> *_Nullable attachmentsItems =
[self parseItems:json key:kOWSBackup_ManifestKey_AttachmentFiles]; [self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
if (!attachmentsItems) { 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]; OWSBackupManifestContents *contents = [OWSBackupManifestContents new];
contents.databaseItems = databaseItems; contents.databaseItems = databaseItems;
contents.attachmentsItems = attachmentsItems; 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(json);
OWSAssertDebug(key.length); OWSAssertDebug(key.length);

@ -37,6 +37,7 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
- (BOOL)hasLocalProfile; - (BOOL)hasLocalProfile;
- (nullable NSString *)localProfileName; - (nullable NSString *)localProfileName;
- (nullable UIImage *)localProfileAvatarImage; - (nullable UIImage *)localProfileAvatarImage;
- (nullable NSData *)localProfileAvatarData;
- (void)ensureLocalProfileCached; - (void)ensureLocalProfileCached;
// This method is used to update the "local profile" state on the client // 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]; 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 - (void)updateLocalProfileName:(nullable NSString *)profileName
avatarImage:(nullable UIImage *)avatarImage avatarImage:(nullable UIImage *)avatarImage
success:(void (^)(void))successBlockParameter success:(void (^)(void))successBlockParameter

Loading…
Cancel
Save