From c86518e44cf053388b7e5045549ea2ef7a67f9ef Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Nov 2018 15:55:47 -0500 Subject: [PATCH 1/3] Support multiple backups in single iCloud account. --- .../ViewControllers/DebugUI/DebugUIBackup.m | 25 ++- Signal/src/util/Backup/OWSBackup.m | 92 ++++++++++-- Signal/src/util/Backup/OWSBackupAPI.swift | 142 +++++++++++++++--- Signal/src/util/Backup/OWSBackupExportJob.m | 32 +++- Signal/src/util/Backup/OWSBackupImportJob.m | 11 ++ Signal/src/util/Backup/OWSBackupJob.h | 8 +- Signal/src/util/Backup/OWSBackupJob.m | 14 +- 7 files changed, 264 insertions(+), 60 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m b/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m index 14565b820..97a77251e 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m @@ -14,6 +14,15 @@ NS_ASSUME_NONNULL_BEGIN @implementation DebugUIBackup +#pragma mark - Dependencies + ++ (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + #pragma mark - Factory Methods - (NSString *)name @@ -66,15 +75,17 @@ NS_ASSUME_NONNULL_BEGIN BOOL success = [data writeToFile:filePath atomically:YES]; OWSAssertDebug(success); + NSString *recipientId = self.tsAccountManager.localNumber; [OWSBackupAPI checkCloudKitAccessWithCompletion:^(BOOL hasAccess) { if (hasAccess) { - [OWSBackupAPI saveTestFileToCloudWithFileUrl:[NSURL fileURLWithPath:filePath] - success:^(NSString *recordName) { - // Do nothing, the API method will log for us. - } - failure:^(NSError *error){ - // Do nothing, the API method will log for us. - }]; + [OWSBackupAPI saveTestFileToCloudWithRecipientId:recipientId + fileUrl:[NSURL fileURLWithPath:filePath] + success:^(NSString *recordName) { + // Do nothing, the API method will log for us. + } + failure:^(NSError *error){ + // Do nothing, the API method will log for us. + }]; } }]; } diff --git a/Signal/src/util/Backup/OWSBackup.m b/Signal/src/util/Backup/OWSBackup.m index 550ffead3..308d0a4dd 100644 --- a/Signal/src/util/Backup/OWSBackup.m +++ b/Signal/src/util/Backup/OWSBackup.m @@ -18,6 +18,7 @@ NSString *const OWSPrimaryStorage_OWSBackupCollection = @"OWSPrimaryStorage_OWSB NSString *const OWSBackup_IsBackupEnabledKey = @"OWSBackup_IsBackupEnabledKey"; NSString *const OWSBackup_LastExportSuccessDateKey = @"OWSBackup_LastExportSuccessDateKey"; NSString *const OWSBackup_LastExportFailureDateKey = @"OWSBackup_LastExportFailureDateKey"; +NSString *const OWSBackupErrorDomain = @"OWSBackupErrorDomain"; NSString *NSStringForBackupExportState(OWSBackupState state) { @@ -168,6 +169,16 @@ NSString *NSStringForBackupImportState(OWSBackupState state) return; } + if (!self.tsAccountManager.isRegisteredAndReady) { + OWSLogError(@"Can't backup; not registered and ready."); + return; + } + NSString *_Nullable recipientId = self.tsAccountManager.localNumber; + if (recipientId.length < 1) { + OWSFailDebug(@"Can't backup; missing recipientId."); + return; + } + // In development, make sure there's no export or import in progress. [self.backupExportJob cancel]; self.backupExportJob = nil; @@ -176,7 +187,7 @@ NSString *NSStringForBackupImportState(OWSBackupState state) _backupExportState = OWSBackupState_InProgress; - self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self primaryStorage:self.primaryStorage]; + self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self recipientId:recipientId]; [self.backupExportJob startAsync]; [self postDidChangeNotification]; @@ -313,12 +324,22 @@ NSString *NSStringForBackupImportState(OWSBackupState state) return; } + if (!self.tsAccountManager.isRegisteredAndReady) { + OWSLogError(@"Can't backup; not registered and ready."); + return; + } + NSString *_Nullable recipientId = self.tsAccountManager.localNumber; + if (recipientId.length < 1) { + OWSFailDebug(@"Can't backup; missing recipientId."); + return; + } + // Start or abort a backup export if neccessary. if (!self.shouldHaveBackupExport && self.backupExportJob) { [self.backupExportJob cancel]; self.backupExportJob = nil; } else if (self.shouldHaveBackupExport && !self.backupExportJob) { - self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self primaryStorage:self.primaryStorage]; + self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self recipientId:recipientId]; [self.backupExportJob startAsync]; } @@ -358,8 +379,31 @@ NSString *NSStringForBackupImportState(OWSBackupState state) OWSLogInfo(@""); - [OWSBackupAPI - checkForManifestInCloudWithSuccess:^(BOOL value) { + void (^failWithUnexpectedError)(void) = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = + [NSError errorWithDomain:OWSBackupErrorDomain + code:1 + userInfo:@{ + NSLocalizedDescriptionKey : NSLocalizedString(@"BACKUP_UNEXPECTED_ERROR", + @"Error shown when backup fails due to an unexpected error.") + }]; + failure(error); + }); + }; + + if (!self.tsAccountManager.isRegisteredAndReady) { + OWSLogError(@"Can't backup; not registered and ready."); + return failWithUnexpectedError(); + } + NSString *_Nullable recipientId = self.tsAccountManager.localNumber; + if (recipientId.length < 1) { + OWSFailDebug(@"Can't backup; missing recipientId."); + return failWithUnexpectedError(); + } + + [OWSBackupAPI checkForManifestInCloudWithRecipientId:recipientId + success:^(BOOL value) { dispatch_async(dispatch_get_main_queue(), ^{ success(value); }); @@ -376,6 +420,16 @@ NSString *NSStringForBackupImportState(OWSBackupState state) OWSAssertIsOnMainThread(); OWSAssertDebug(!self.backupImportJob); + if (!self.tsAccountManager.isRegisteredAndReady) { + OWSLogError(@"Can't restore backup; not registered and ready."); + return; + } + NSString *_Nullable recipientId = self.tsAccountManager.localNumber; + if (recipientId.length < 1) { + OWSLogError(@"Can't restore backup; missing recipientId."); + return; + } + // In development, make sure there's no export or import in progress. [self.backupExportJob cancel]; self.backupExportJob = nil; @@ -384,7 +438,7 @@ NSString *NSStringForBackupImportState(OWSBackupState state) _backupImportState = OWSBackupState_InProgress; - self.backupImportJob = [[OWSBackupImportJob alloc] initWithDelegate:self primaryStorage:self.primaryStorage]; + self.backupImportJob = [[OWSBackupImportJob alloc] initWithDelegate:self recipientId:recipientId]; [self.backupImportJob startAsync]; [self postDidChangeNotification]; @@ -512,8 +566,18 @@ NSString *NSStringForBackupImportState(OWSBackupState state) OWSLogInfo(@""); - [OWSBackupAPI - fetchAllRecordNamesWithSuccess:^(NSArray *recordNames) { + if (!self.tsAccountManager.isRegisteredAndReady) { + OWSLogError(@"Can't interact with backup; not registered and ready."); + return; + } + NSString *_Nullable recipientId = self.tsAccountManager.localNumber; + if (recipientId.length < 1) { + OWSLogError(@"Can't interact with backup; missing recipientId."); + return; + } + + [OWSBackupAPI fetchAllRecordNamesWithRecipientId:recipientId + success:^(NSArray *recordNames) { for (NSString *recordName in [recordNames sortedArrayUsingSelector:@selector(compare:)]) { OWSLogInfo(@"\t %@", recordName); } @@ -530,8 +594,18 @@ NSString *NSStringForBackupImportState(OWSBackupState state) OWSLogInfo(@""); - [OWSBackupAPI - fetchAllRecordNamesWithSuccess:^(NSArray *recordNames) { + if (!self.tsAccountManager.isRegisteredAndReady) { + OWSLogError(@"Can't interact with backup; not registered and ready."); + return; + } + NSString *_Nullable recipientId = self.tsAccountManager.localNumber; + if (recipientId.length < 1) { + OWSLogError(@"Can't interact with backup; missing recipientId."); + return; + } + + [OWSBackupAPI fetchAllRecordNamesWithRecipientId:recipientId + success:^(NSArray *recordNames) { if (recordNames.count < 1) { OWSLogInfo(@"No CloudKit records found to clear."); return; diff --git a/Signal/src/util/Backup/OWSBackupAPI.swift b/Signal/src/util/Backup/OWSBackupAPI.swift index a32dfe5c6..8138f05d0 100644 --- a/Signal/src/util/Backup/OWSBackupAPI.swift +++ b/Signal/src/util/Backup/OWSBackupAPI.swift @@ -19,7 +19,6 @@ import CloudKit // // TODO: Change the record types when we ship to production. static let signalBackupRecordType = "signalBackup" - static let manifestRecordName = "manifest" static let payloadKey = "payload" static let maxRetries = 5 @@ -42,11 +41,13 @@ import CloudKit // MARK: - Upload @objc - public class func saveTestFileToCloud(fileUrl: URL, + public class func saveTestFileToCloud(recipientId: String, + fileUrl: URL, success: @escaping (String) -> Void, failure: @escaping (Error) -> Void) { + let recordName = "\(recordNamePrefix(forRecipientId: recipientId))test-\(NSUUID().uuidString)" saveFileToCloud(fileUrl: fileUrl, - recordName: NSUUID().uuidString, + recordName: recordName, recordType: signalBackupRecordType, success: success, failure: failure) @@ -57,11 +58,13 @@ import CloudKit // We wouldn't want to overwrite previous images until the entire backup export is // complete. @objc - public class func saveEphemeralDatabaseFileToCloud(fileUrl: URL, + public class func saveEphemeralDatabaseFileToCloud(recipientId: String, + fileUrl: URL, success: @escaping (String) -> Void, failure: @escaping (Error) -> Void) { + let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeralFile-\(NSUUID().uuidString)" saveFileToCloud(fileUrl: fileUrl, - recordName: "ephemeralFile-\(NSUUID().uuidString)", + recordName: recordName, recordType: signalBackupRecordType, success: success, failure: failure) @@ -71,19 +74,65 @@ import CloudKit // once. For example, attachment files should only be uploaded once. Subsequent // backups can reuse the same record. @objc - public class func recordNameForPersistentFile(fileId: String) -> String { - return "persistentFile-\(fileId)" + public class func recordNameForPersistentFile(recipientId: String, + fileId: String) -> String { + return "\(recordNamePrefix(forRecipientId: recipientId))persistentFile-\(fileId)" } // "Persistent" files may be shared between backup export; they should only be saved // once. For example, attachment files should only be uploaded once. Subsequent // backups can reuse the same record. @objc - public class func savePersistentFileOnceToCloud(fileId: String, + public class func recordNameForManifest(recipientId: String) -> String { + return "\(recordNamePrefix(forRecipientId: recipientId))manifest" + } + + private class func recordNamePrefix(forRecipientId recipientId: String) -> String { + return "\(recipientId)-" + } + + private class func recipientId(forRecordName recordName: String) -> String? { + let recipientIds = self.recipientIds(forRecordNames: [recordName]) + guard let recipientId = recipientIds.first else { + return nil + } + return recipientId + } + + private class func recipientIds(forRecordNames recordNames: [String]) -> [String] { + let regex: NSRegularExpression + do { + regex = try NSRegularExpression(pattern: "(\\+[0-9]+)\\-") + } catch { + Logger.error("couldn't compile regex: \(error)") + return [] + } + var recipientIds = [String]() + for recordName in recordNames { + guard let match = regex.firstMatch(in: recordName, options: [], range: NSRange(location: 0, length: recordName.count)) else { + continue + } + guard match.range.location == 0 else { + // Match must be at start of string. + continue + } + let recipientId = (recordName as NSString).substring(with: match.range) as String + recipientIds.append(recipientId) + } + return recipientIds + } + + // "Persistent" files may be shared between backup export; they should only be saved + // once. For example, attachment files should only be uploaded once. Subsequent + // backups can reuse the same record. + @objc + public class func savePersistentFileOnceToCloud(recipientId: String, + fileId: String, fileUrlBlock: @escaping () -> URL?, success: @escaping (String) -> Void, failure: @escaping (Error) -> Void) { - saveFileOnceToCloud(recordName: recordNameForPersistentFile(fileId: fileId), + let recordName = recordNameForPersistentFile(recipientId: recipientId, fileId: fileId) + saveFileOnceToCloud(recordName: recordName, recordType: signalBackupRecordType, fileUrlBlock: fileUrlBlock, success: success, @@ -91,12 +140,14 @@ import CloudKit } @objc - public class func upsertManifestFileToCloud(fileUrl: URL, + public class func upsertManifestFileToCloud(recipientId: String, + fileUrl: URL, success: @escaping (String) -> Void, failure: @escaping (Error) -> Void) { // We want to use a well-known record id and type for manifest files. + let recordName = recordNameForManifest(recipientId: recipientId) upsertFileToCloud(fileUrl: fileUrl, - recordName: manifestRecordName, + recordName: recordName, recordType: signalBackupRecordType, success: success, failure: failure) @@ -348,10 +399,12 @@ import CloudKit } @objc - public class func checkForManifestInCloud(success: @escaping (Bool) -> Void, + public class func checkForManifestInCloud(recipientId: String, + success: @escaping (Bool) -> Void, failure: @escaping (Error) -> Void) { - checkForFileInCloud(recordName: manifestRecordName, + let recordName = recordNameForManifest(recipientId: recipientId) + checkForFileInCloud(recordName: recordName, remainingRetries: maxRetries, success: { (record) in success(record != nil) @@ -360,12 +413,14 @@ import CloudKit } @objc - public class func fetchAllRecordNames(success: @escaping ([String]) -> Void, + public class func fetchAllRecordNames(recipientId: String, + success: @escaping ([String]) -> Void, failure: @escaping (Error) -> Void) { let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true)) // Fetch the first page of results for this query. - fetchAllRecordNamesStep(query: query, + fetchAllRecordNamesStep(recipientId: recipientId, + query: query, previousRecordNames: [String](), cursor: nil, remainingRetries: maxRetries, @@ -373,7 +428,30 @@ import CloudKit failure: failure) } - private class func fetchAllRecordNamesStep(query: CKQuery, + @objc + public class func fetchAllBackupRecipientIds(success: @escaping ([String]) -> Void, + failure: @escaping (Error) -> Void) { + + let processResults = { (recordNames: [String]) in + DispatchQueue.global().async { + let recipientIds = self.recipientIds(forRecordNames: recordNames) + success(recipientIds) + } + } + + let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true)) + // Fetch the first page of results for this query. + fetchAllRecordNamesStep(recipientId: nil, + query: query, + previousRecordNames: [String](), + cursor: nil, + remainingRetries: maxRetries, + success: processResults, + failure: failure) + } + + private class func fetchAllRecordNamesStep(recipientId: String?, + query: CKQuery, previousRecordNames: [String], cursor: CKQueryCursor?, remainingRetries: Int, @@ -390,7 +468,18 @@ import CloudKit queryOperation.desiredKeys = [] queryOperation.recordFetchedBlock = { (record) in assert(record.recordID.recordName.count > 0) - allRecordNames.append(record.recordID.recordName) + + let recordName = record.recordID.recordName + + if let recipientId = recipientId { + let prefix = recordNamePrefix(forRecipientId: recipientId) + guard recordName.hasPrefix(prefix) else { + Logger.info("Ignoring record: \(recordName)") + return + } + } + + allRecordNames.append(recordName) } queryOperation.queryCompletionBlock = { (cursor, error) in @@ -402,7 +491,8 @@ import CloudKit if let cursor = cursor { Logger.verbose("fetching more record names \(allRecordNames.count).") // There are more pages of results, continue fetching. - fetchAllRecordNamesStep(query: query, + fetchAllRecordNamesStep(recipientId: recipientId, + query: query, previousRecordNames: allRecordNames, cursor: cursor, remainingRetries: maxRetries, @@ -416,7 +506,8 @@ import CloudKit failure(outcomeError) case .failureRetryAfterDelay(let retryDelay): DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: { - fetchAllRecordNamesStep(query: query, + fetchAllRecordNamesStep(recipientId: recipientId, + query: query, previousRecordNames: allRecordNames, cursor: cursor, remainingRetries: remainingRetries - 1, @@ -425,7 +516,8 @@ import CloudKit }) case .failureRetryWithoutDelay: DispatchQueue.global().async { - fetchAllRecordNamesStep(query: query, + fetchAllRecordNamesStep(recipientId: recipientId, + query: query, previousRecordNames: allRecordNames, cursor: cursor, remainingRetries: remainingRetries - 1, @@ -443,10 +535,12 @@ import CloudKit // MARK: - Download @objc - public class func downloadManifestFromCloud( - success: @escaping (Data) -> Void, - failure: @escaping (Error) -> Void) { - downloadDataFromCloud(recordName: manifestRecordName, + public class func downloadManifestFromCloud(recipientId: String, + success: @escaping (Data) -> Void, + failure: @escaping (Error) -> Void) { + + let recordName = recordNameForManifest(recipientId: recipientId) + downloadDataFromCloud(recordName: recordName, success: success, failure: failure) } diff --git a/Signal/src/util/Backup/OWSBackupExportJob.m b/Signal/src/util/Backup/OWSBackupExportJob.m index 9a052dfd5..4706b4e3a 100644 --- a/Signal/src/util/Backup/OWSBackupExportJob.m +++ b/Signal/src/util/Backup/OWSBackupExportJob.m @@ -310,6 +310,17 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSBackupExportJob +#pragma mark - Dependencies + +- (OWSPrimaryStorage *)primaryStorage +{ + OWSAssertDebug(SSKEnvironment.shared.primaryStorage); + + return SSKEnvironment.shared.primaryStorage; +} + +#pragma mark - + - (void)startAsync { OWSAssertIsOnMainThread(); @@ -439,8 +450,8 @@ NS_ASSUME_NONNULL_BEGIN OWSLogVerbose(@""); __weak OWSBackupExportJob *weakSelf = self; - [OWSBackupAPI - fetchAllRecordNamesWithSuccess:^(NSArray *recordNames) { + [OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId + success:^(NSArray *recordNames) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ OWSBackupExportJob *strongSelf = weakSelf; if (!strongSelf) { @@ -719,7 +730,8 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(item.encryptedItem.filePath.length > 0); - [OWSBackupAPI saveEphemeralDatabaseFileToCloudWithFileUrl:[NSURL fileURLWithPath:item.encryptedItem.filePath] + [OWSBackupAPI saveEphemeralDatabaseFileToCloudWithRecipientId:self.recipientId + fileUrl:[NSURL fileURLWithPath:item.encryptedItem.filePath] success:^(NSString *recordName) { // Ensure that we continue to work off the main thread. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -765,7 +777,9 @@ NS_ASSUME_NONNULL_BEGIN // * That we already know the metadata for this fragment (from a previous backup // or restore). // * That this record does in fact exist in our CloudKit database. - NSString *lastRecordName = [OWSBackupAPI recordNameForPersistentFileWithFileId:attachmentExport.attachmentId]; + NSString *lastRecordName = + [OWSBackupAPI recordNameForPersistentFileWithRecipientId:self.recipientId + fileId:attachmentExport.attachmentId]; OWSBackupFragment *_Nullable lastBackupFragment = [OWSBackupFragment fetchObjectWithUniqueID:lastRecordName]; if (lastBackupFragment && [self.lastValidRecordNames containsObject:lastRecordName]) { OWSAssertDebug(lastBackupFragment.encryptionKey.length > 0); @@ -803,7 +817,8 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(attachmentExport.encryptedItem); } - [OWSBackupAPI savePersistentFileOnceToCloudWithFileId:attachmentExport.attachmentId + [OWSBackupAPI savePersistentFileOnceToCloudWithRecipientId:self.recipientId + fileId:attachmentExport.attachmentId fileUrlBlock:^{ if (attachmentExport.encryptedItem.filePath.length < 1) { OWSLogError(@"attachment export missing temp file path"); @@ -882,7 +897,8 @@ NS_ASSUME_NONNULL_BEGIN __weak OWSBackupExportJob *weakSelf = self; - [OWSBackupAPI upsertManifestFileToCloudWithFileUrl:[NSURL fileURLWithPath:encryptedItem.filePath] + [OWSBackupAPI upsertManifestFileToCloudWithRecipientId:self.recipientId + fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath] success:^(NSString *recordName) { // Ensure that we continue to work off the main thread. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -1036,8 +1052,8 @@ NS_ASSUME_NONNULL_BEGIN } __weak OWSBackupExportJob *weakSelf = self; - [OWSBackupAPI - fetchAllRecordNamesWithSuccess:^(NSArray *recordNames) { + [OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId + success:^(NSArray *recordNames) { // Ensure that we continue to work off the main thread. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSMutableSet *obsoleteRecordNames = [NSMutableSet new]; diff --git a/Signal/src/util/Backup/OWSBackupImportJob.m b/Signal/src/util/Backup/OWSBackupImportJob.m index 64080ea8b..9f602fff9 100644 --- a/Signal/src/util/Backup/OWSBackupImportJob.m +++ b/Signal/src/util/Backup/OWSBackupImportJob.m @@ -35,6 +35,17 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe @implementation OWSBackupImportJob +#pragma mark - Dependencies + +- (OWSPrimaryStorage *)primaryStorage +{ + OWSAssertDebug(SSKEnvironment.shared.primaryStorage); + + return SSKEnvironment.shared.primaryStorage; +} + +#pragma mark - + - (void)startAsync { OWSAssertIsOnMainThread(); diff --git a/Signal/src/util/Backup/OWSBackupJob.h b/Signal/src/util/Backup/OWSBackupJob.h index 663ed9e5a..ac42def1d 100644 --- a/Signal/src/util/Backup/OWSBackupJob.h +++ b/Signal/src/util/Backup/OWSBackupJob.h @@ -53,22 +53,20 @@ typedef void (^OWSBackupJobManifestFailure)(NSError *error); #pragma mark - -@class OWSPrimaryStorage; - @interface OWSBackupJob : NSObject @property (nonatomic, weak, readonly) id delegate; +@property (nonatomic, readonly) NSString *recipientId; + // Indicates that the backup succeeded, failed or was cancelled. @property (atomic, readonly) BOOL isComplete; -@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; - @property (nonatomic, readonly) NSString *jobTempDirPath; - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithDelegate:(id)delegate primaryStorage:(OWSPrimaryStorage *)primaryStorage; +- (instancetype)initWithDelegate:(id)delegate recipientId:(NSString *)recipientId; #pragma mark - Private diff --git a/Signal/src/util/Backup/OWSBackupJob.m b/Signal/src/util/Backup/OWSBackupJob.m index 57eb009b6..94617cae7 100644 --- a/Signal/src/util/Backup/OWSBackupJob.m +++ b/Signal/src/util/Backup/OWSBackupJob.m @@ -30,11 +30,11 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; @property (nonatomic, weak) id delegate; +@property (nonatomic) NSString *recipientId; + @property (atomic) BOOL isComplete; @property (atomic) BOOL hasSucceeded; -@property (nonatomic) OWSPrimaryStorage *primaryStorage; - @property (nonatomic) NSString *jobTempDirPath; @end @@ -43,7 +43,7 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; @implementation OWSBackupJob -- (instancetype)initWithDelegate:(id)delegate primaryStorage:(OWSPrimaryStorage *)primaryStorage +- (instancetype)initWithDelegate:(id)delegate recipientId:(NSString *)recipientId { self = [super init]; @@ -51,11 +51,11 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; return self; } - OWSAssertDebug(primaryStorage); + OWSAssertDebug(recipientId.length > 0); OWSAssertDebug([OWSStorage isStorageReady]); self.delegate = delegate; - self.primaryStorage = primaryStorage; + self.recipientId = recipientId; return self; } @@ -162,8 +162,8 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; OWSLogVerbose(@""); __weak OWSBackupJob *weakSelf = self; - [OWSBackupAPI - downloadManifestFromCloudWithSuccess:^(NSData *data) { + [OWSBackupAPI downloadManifestFromCloudWithRecipientId:self.recipientId + success:^(NSData *data) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [weakSelf processManifest:data From e23773ed2c94aa06c44da9b074a2d8f46158f2e7 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Nov 2018 16:05:09 -0500 Subject: [PATCH 2/3] Support multiple backups in single iCloud account. --- Signal/src/util/Backup/OWSBackup.h | 3 ++ Signal/src/util/Backup/OWSBackup.m | 19 +++++++ Signal/src/util/Backup/OWSBackupAPI.swift | 66 ++++++++++++++++------- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/Signal/src/util/Backup/OWSBackup.h b/Signal/src/util/Backup/OWSBackup.h index 058e9a355..459c7ec54 100644 --- a/Signal/src/util/Backup/OWSBackup.h +++ b/Signal/src/util/Backup/OWSBackup.h @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *const NSNotificationNameBackupStateDidChange; typedef void (^OWSBackupBoolBlock)(BOOL value); +typedef void (^OWSBackupStringListBlock)(NSArray *value); typedef void (^OWSBackupErrorBlock)(NSError *error); typedef NS_ENUM(NSUInteger, OWSBackupState) { @@ -66,6 +67,8 @@ NSString *NSStringForBackupImportState(OWSBackupState state); @property (nonatomic, readonly, nullable) NSString *backupImportDescription; @property (nonatomic, readonly, nullable) NSNumber *backupImportProgress; +- (void)allRecipientIdsWithManifestsInCloud:(OWSBackupStringListBlock)success failure:(OWSBackupErrorBlock)failure; + - (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure; // TODO: After a successful import, we should enable backup and diff --git a/Signal/src/util/Backup/OWSBackup.m b/Signal/src/util/Backup/OWSBackup.m index 308d0a4dd..ad90e4233 100644 --- a/Signal/src/util/Backup/OWSBackup.m +++ b/Signal/src/util/Backup/OWSBackup.m @@ -373,6 +373,25 @@ NSString *NSStringForBackupImportState(OWSBackupState state) #pragma mark - Backup Import +- (void)allRecipientIdsWithManifestsInCloud:(OWSBackupStringListBlock)success failure:(OWSBackupErrorBlock)failure +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + [OWSBackupAPI + allRecipientIdsWithManifestsInCloudWithSuccess:^(NSArray *recipientIds) { + dispatch_async(dispatch_get_main_queue(), ^{ + success(recipientIds); + }); + } + failure:^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + }]; +} + - (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure { OWSAssertIsOnMainThread(); diff --git a/Signal/src/util/Backup/OWSBackupAPI.swift b/Signal/src/util/Backup/OWSBackupAPI.swift index 8138f05d0..121680c5a 100644 --- a/Signal/src/util/Backup/OWSBackupAPI.swift +++ b/Signal/src/util/Backup/OWSBackupAPI.swift @@ -19,6 +19,7 @@ import CloudKit // // TODO: Change the record types when we ship to production. static let signalBackupRecordType = "signalBackup" + static let manifestRecordNameSuffix = "manifest" static let payloadKey = "payload" static let maxRetries = 5 @@ -84,11 +85,15 @@ import CloudKit // backups can reuse the same record. @objc public class func recordNameForManifest(recipientId: String) -> String { - return "\(recordNamePrefix(forRecipientId: recipientId))manifest" + return "\(recordNamePrefix(forRecipientId: recipientId))\(manifestRecordNameSuffix)" + } + + private class func isManifest(recordName: String) -> Bool { + return recordName.hasSuffix(manifestRecordNameSuffix) } private class func recordNamePrefix(forRecipientId recipientId: String) -> String { - return "\(recipientId)-" + return "\(recipientId)-" } private class func recipientId(forRecordName recordName: String) -> String? { @@ -413,43 +418,68 @@ import CloudKit } @objc - public class func fetchAllRecordNames(recipientId: String, - success: @escaping ([String]) -> Void, - failure: @escaping (Error) -> Void) { + public class func allRecipientIdsWithManifestsInCloud(success: @escaping ([String]) -> Void, + failure: @escaping (Error) -> Void) { + + let processResults = { (recordNames: [String]) in + DispatchQueue.global().async { + let manifestRecordNames = recordNames.filter({ (recordName) -> Bool in + self.isManifest(recordName: recordName) + }) + let recipientIds = self.recipientIds(forRecordNames: manifestRecordNames) + success(recipientIds) + } + } let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true)) // Fetch the first page of results for this query. - fetchAllRecordNamesStep(recipientId: recipientId, + fetchAllRecordNamesStep(recipientId: nil, query: query, previousRecordNames: [String](), cursor: nil, remainingRetries: maxRetries, - success: success, + success: processResults, failure: failure) } @objc - public class func fetchAllBackupRecipientIds(success: @escaping ([String]) -> Void, - failure: @escaping (Error) -> Void) { - - let processResults = { (recordNames: [String]) in - DispatchQueue.global().async { - let recipientIds = self.recipientIds(forRecordNames: recordNames) - success(recipientIds) - } - } + public class func fetchAllRecordNames(recipientId: String, + success: @escaping ([String]) -> Void, + failure: @escaping (Error) -> Void) { let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true)) // Fetch the first page of results for this query. - fetchAllRecordNamesStep(recipientId: nil, + fetchAllRecordNamesStep(recipientId: recipientId, query: query, previousRecordNames: [String](), cursor: nil, remainingRetries: maxRetries, - success: processResults, + success: success, failure: failure) } +// @objc +// public class func fetchAllBackupRecipientIds(success: @escaping ([String]) -> Void, +// failure: @escaping (Error) -> Void) { +// +// let processResults = { (recordNames: [String]) in +// DispatchQueue.global().async { +// let recipientIds = self.recipientIds(forRecordNames: recordNames) +// success(recipientIds) +// } +// } +// +// let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true)) +// // Fetch the first page of results for this query. +// fetchAllRecordNamesStep(recipientId: nil, +// query: query, +// previousRecordNames: [String](), +// cursor: nil, +// remainingRetries: maxRetries, +// success: processResults, +// failure: failure) +// } + private class func fetchAllRecordNamesStep(recipientId: String?, query: CKQuery, previousRecordNames: [String], From 1c012e9a23205f393e20555d707bb4abf958021c Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Nov 2018 09:43:32 -0500 Subject: [PATCH 3/3] Respond to CR. --- .../ViewControllers/DebugUI/DebugUIBackup.m | 17 ++++++ Signal/src/util/Backup/OWSBackup.m | 2 +- Signal/src/util/Backup/OWSBackupAPI.swift | 53 +++++++------------ 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m b/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m index 97a77251e..74e775cce 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m @@ -45,6 +45,10 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIBackup logBackupRecords]; }]]; + [items addObject:[OWSTableItem itemWithTitle:@"Log CloudKit backup manifests" + actionBlock:^{ + [DebugUIBackup logBackupManifests]; + }]]; [items addObject:[OWSTableItem itemWithTitle:@"Restore CloudKit backup" actionBlock:^{ [DebugUIBackup tryToImportBackup]; @@ -110,6 +114,19 @@ NS_ASSUME_NONNULL_BEGIN [OWSBackup.sharedManager logBackupRecords]; } ++ (void)logBackupManifests +{ + OWSLogInfo(@"logBackupManifests."); + + [OWSBackup.sharedManager + allRecipientIdsWithManifestsInCloud:^(NSArray *recipientIds) { + OWSLogInfo(@"recipientIds: %@", recipientIds); + } + failure:^(NSError *error) { + OWSLogError(@"error: %@", error); + }]; +} + + (void)tryToImportBackup { OWSLogInfo(@"tryToImportBackup."); diff --git a/Signal/src/util/Backup/OWSBackup.m b/Signal/src/util/Backup/OWSBackup.m index ad90e4233..c08ce28de 100644 --- a/Signal/src/util/Backup/OWSBackup.m +++ b/Signal/src/util/Backup/OWSBackup.m @@ -170,7 +170,7 @@ NSString *NSStringForBackupImportState(OWSBackupState state) } if (!self.tsAccountManager.isRegisteredAndReady) { - OWSLogError(@"Can't backup; not registered and ready."); + OWSFailDebug(@"Can't backup; not registered and ready."); return; } NSString *_Nullable recipientId = self.tsAccountManager.localNumber; diff --git a/Signal/src/util/Backup/OWSBackupAPI.swift b/Signal/src/util/Backup/OWSBackupAPI.swift index 121680c5a..73531cd44 100644 --- a/Signal/src/util/Backup/OWSBackupAPI.swift +++ b/Signal/src/util/Backup/OWSBackupAPI.swift @@ -93,7 +93,7 @@ import CloudKit } private class func recordNamePrefix(forRecipientId recipientId: String) -> String { - return "\(recipientId)-" + return "\(recipientId)-" } private class func recipientId(forRecordName recordName: String) -> String? { @@ -104,24 +104,31 @@ import CloudKit return recipientId } + private static var recordNamePrefixRegex = { + return try! NSRegularExpression(pattern: "^(\\+[0-9]+)\\-") + }() + private class func recipientIds(forRecordNames recordNames: [String]) -> [String] { - let regex: NSRegularExpression - do { - regex = try NSRegularExpression(pattern: "(\\+[0-9]+)\\-") - } catch { - Logger.error("couldn't compile regex: \(error)") - return [] - } var recipientIds = [String]() for recordName in recordNames { - guard let match = regex.firstMatch(in: recordName, options: [], range: NSRange(location: 0, length: recordName.count)) else { + let regex = recordNamePrefixRegex + guard let match: NSTextCheckingResult = regex.firstMatch(in: recordName, options: [], range: NSRange(location: 0, length: recordName.count)) else { + Logger.warn("no match: \(recordName)") + continue + } + guard match.numberOfRanges > 0 else { + // Match must include first group. + Logger.warn("invalid match: \(recordName)") continue } - guard match.range.location == 0 else { - // Match must be at start of string. + let firstRange = match.range(at: 1) + guard firstRange.location == 0, + firstRange.length > 0 else { + // Match must be at start of string and non-empty. + Logger.warn("invalid match: \(recordName) \(firstRange)") continue } - let recipientId = (recordName as NSString).substring(with: match.range) as String + let recipientId = (recordName as NSString).substring(with: firstRange) as String recipientIds.append(recipientId) } return recipientIds @@ -458,28 +465,6 @@ import CloudKit failure: failure) } -// @objc -// public class func fetchAllBackupRecipientIds(success: @escaping ([String]) -> Void, -// failure: @escaping (Error) -> Void) { -// -// let processResults = { (recordNames: [String]) in -// DispatchQueue.global().async { -// let recipientIds = self.recipientIds(forRecordNames: recordNames) -// success(recipientIds) -// } -// } -// -// let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true)) -// // Fetch the first page of results for this query. -// fetchAllRecordNamesStep(recipientId: nil, -// query: query, -// previousRecordNames: [String](), -// cursor: nil, -// remainingRetries: maxRetries, -// success: processResults, -// failure: failure) -// } - private class func fetchAllRecordNamesStep(recipientId: String?, query: CKQuery, previousRecordNames: [String],