Support multiple backups in single iCloud account.

This commit is contained in:
Matthew Chen 2018-11-26 15:55:47 -05:00
parent df25301d57
commit c86518e44c
7 changed files with 264 additions and 60 deletions

View File

@ -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.
}];
}
}];
}

View File

@ -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<NSString *> *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<NSString *> *recordNames) {
for (NSString *recordName in [recordNames sortedArrayUsingSelector:@selector(compare:)]) {
OWSLogInfo(@"\t %@", recordName);
}
@ -530,8 +594,18 @@ NSString *NSStringForBackupImportState(OWSBackupState state)
OWSLogInfo(@"");
[OWSBackupAPI
fetchAllRecordNamesWithSuccess:^(NSArray<NSString *> *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<NSString *> *recordNames) {
if (recordNames.count < 1) {
OWSLogInfo(@"No CloudKit records found to clear.");
return;

View File

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

View File

@ -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<NSString *> *recordNames) {
[OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId
success:^(NSArray<NSString *> *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<NSString *> *recordNames) {
[OWSBackupAPI fetchAllRecordNamesWithRecipientId:self.recipientId
success:^(NSArray<NSString *> *recordNames) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableSet<NSString *> *obsoleteRecordNames = [NSMutableSet new];

View File

@ -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();

View File

@ -53,22 +53,20 @@ typedef void (^OWSBackupJobManifestFailure)(NSError *error);
#pragma mark -
@class OWSPrimaryStorage;
@interface OWSBackupJob : NSObject
@property (nonatomic, weak, readonly) id<OWSBackupJobDelegate> 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<OWSBackupJobDelegate>)delegate primaryStorage:(OWSPrimaryStorage *)primaryStorage;
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)delegate recipientId:(NSString *)recipientId;
#pragma mark - Private

View File

@ -30,11 +30,11 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
@property (nonatomic, weak) id<OWSBackupJobDelegate> 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<OWSBackupJobDelegate>)delegate primaryStorage:(OWSPrimaryStorage *)primaryStorage
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)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