Clean up cloud after successful backup export.
This commit is contained in:
parent
aa546a02df
commit
90c8f5483b
|
@ -139,6 +139,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.dbConnection setBool:value
|
||||
forKey:OWSBackup_IsBackupEnabledKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
OWSAssert(self.isBackupEnabled);
|
||||
|
||||
if (!value) {
|
||||
[self.dbConnection removeObjectForKey:OWSBackup_LastExportSuccessDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
[self.dbConnection removeObjectForKey:OWSBackup_LastExportFailureDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationNameBackupStateDidChange
|
||||
object:nil
|
||||
|
@ -162,12 +170,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NSDate *_Nullable lastExportSuccessDate = self.lastExportSuccessDate;
|
||||
NSDate *_Nullable lastExportFailureDate = self.lastExportFailureDate;
|
||||
// Wait N hours before retrying after a success.
|
||||
const NSTimeInterval kRetryAfterSuccess = 24 * kHourInterval;
|
||||
//
|
||||
// TODO: Use actual values in production.
|
||||
// const NSTimeInterval kRetryAfterSuccess = 24 * kHourInterval;
|
||||
const NSTimeInterval kRetryAfterSuccess = 0;
|
||||
if (lastExportSuccessDate && fabs(lastExportSuccessDate.timeIntervalSinceNow) < kRetryAfterSuccess) {
|
||||
return NO;
|
||||
}
|
||||
// Wait N hours before retrying after a failure.
|
||||
const NSTimeInterval kRetryAfterFailure = 6 * kHourInterval;
|
||||
//
|
||||
// TODO: Use actual values in production.
|
||||
// const NSTimeInterval kRetryAfterFailure = 6 * kHourInterval;
|
||||
const NSTimeInterval kRetryAfterFailure = 0;
|
||||
if (lastExportFailureDate && fabs(lastExportFailureDate.timeIntervalSinceNow) < kRetryAfterFailure) {
|
||||
return NO;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,13 @@ import SignalServiceKit
|
|||
import CloudKit
|
||||
|
||||
@objc public class OWSBackupAPI: NSObject {
|
||||
|
||||
// If we change the record types, we need to ensure indices
|
||||
// are configured properly in the CloudKit dashboard.
|
||||
static let signalBackupRecordType = "signalBackup"
|
||||
static let manifestRecordName = "manifest"
|
||||
static let payloadKey = "payload"
|
||||
|
||||
@objc
|
||||
public class func recordIdForTest() -> String {
|
||||
return "test-\(NSUUID().uuidString)"
|
||||
|
@ -18,7 +25,7 @@ import CloudKit
|
|||
failure: @escaping (Error) -> Swift.Void) {
|
||||
saveFileToCloud(fileUrl: fileUrl,
|
||||
recordName: NSUUID().uuidString,
|
||||
recordType: "test",
|
||||
recordType: signalBackupRecordType,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
@ -29,46 +36,41 @@ import CloudKit
|
|||
// complete.
|
||||
@objc
|
||||
public class func saveEphemeralDatabaseFileToCloud(fileUrl: URL,
|
||||
success: @escaping (String) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
success: @escaping (String) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
saveFileToCloud(fileUrl: fileUrl,
|
||||
recordName: NSUUID().uuidString,
|
||||
recordType: "ephemeralFile",
|
||||
recordType: signalBackupRecordType,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
// "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,
|
||||
fileUrlBlock: @escaping (Swift.Void) -> URL?,
|
||||
success: @escaping (String) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
fileUrlBlock: @escaping (Swift.Void) -> URL?,
|
||||
success: @escaping (String) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
saveFileOnceToCloud(recordName: "persistentFile-\(fileId)",
|
||||
recordType: "persistentFile",
|
||||
fileUrlBlock: fileUrlBlock,
|
||||
success: success,
|
||||
failure: failure)
|
||||
recordType: signalBackupRecordType,
|
||||
fileUrlBlock: fileUrlBlock,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
static let manifestRecordName = "manifest_"
|
||||
static let manifestRecordType = "manifest"
|
||||
static let payloadKey = "payload"
|
||||
|
||||
@objc
|
||||
public class func upsertAttachmentToCloud(fileUrl: URL,
|
||||
success: @escaping (String) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
// We want to use a well-known record id and type for manifest files.
|
||||
upsertFileToCloud(fileUrl: fileUrl,
|
||||
recordName: manifestRecordName,
|
||||
recordType: manifestRecordType,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
// @objc
|
||||
// public class func upsertAttachmentToCloud(fileUrl: URL,
|
||||
// success: @escaping (String) -> Swift.Void,
|
||||
// failure: @escaping (Error) -> Swift.Void) {
|
||||
// // We want to use a well-known record id and type for manifest files.
|
||||
// upsertFileToCloud(fileUrl: fileUrl,
|
||||
// recordName: manifestRecordName,
|
||||
// recordType: signalBackupRecordType,
|
||||
// success: success,
|
||||
// failure: failure)
|
||||
// }
|
||||
|
||||
@objc
|
||||
public class func upsertManifestFileToCloud(fileUrl: URL,
|
||||
|
@ -77,7 +79,7 @@ import CloudKit
|
|||
// We want to use a well-known record id and type for manifest files.
|
||||
upsertFileToCloud(fileUrl: fileUrl,
|
||||
recordName: manifestRecordName,
|
||||
recordType: manifestRecordType,
|
||||
recordType: signalBackupRecordType,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
@ -125,6 +127,28 @@ import CloudKit
|
|||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func deleteRecordFromCloud(recordName: String,
|
||||
success: @escaping (Swift.Void) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
|
||||
let recordID = CKRecordID(recordName: recordName)
|
||||
|
||||
let myContainer = CKContainer.default()
|
||||
let privateDatabase = myContainer.privateCloudDatabase
|
||||
privateDatabase.delete(withRecordID: recordID) {
|
||||
(record, error) in
|
||||
|
||||
if let error = error {
|
||||
Logger.error("\(self.logTag) error deleting record: \(error)")
|
||||
failure(error)
|
||||
} else {
|
||||
Logger.info("\(self.logTag) deleted record.")
|
||||
success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func upsertFileToCloud(fileUrl: URL,
|
||||
recordName: String,
|
||||
|
@ -177,10 +201,10 @@ import CloudKit
|
|||
|
||||
@objc
|
||||
public class func saveFileOnceToCloud(recordName: String,
|
||||
recordType: String,
|
||||
fileUrlBlock: @escaping (Swift.Void) -> URL?,
|
||||
success: @escaping (String) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
recordType: String,
|
||||
fileUrlBlock: @escaping (Swift.Void) -> URL?,
|
||||
success: @escaping (String) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
let recordId = CKRecordID(recordName: recordName)
|
||||
let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ])
|
||||
// Don't download the file; we're just using the fetch to check whether or
|
||||
|
@ -193,7 +217,7 @@ import CloudKit
|
|||
// No record found to update, saving new record.
|
||||
|
||||
guard let fileUrl = fileUrlBlock() else {
|
||||
Logger.error("\(self.logTag) error preparing file for upload: \(error).")
|
||||
Logger.error("\(self.logTag) error preparing file for upload.")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -219,6 +243,62 @@ import CloudKit
|
|||
privateDatabase.add(fetchOperation)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func fetchAllRecordNames(success: @escaping ([String]) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
|
||||
let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true))
|
||||
// Fetch the first page of results for this query.
|
||||
fetchAllRecordNamesStep(query: query,
|
||||
previousRecordNames: [String](),
|
||||
cursor: nil,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
private class func fetchAllRecordNamesStep(query: CKQuery,
|
||||
previousRecordNames: [String],
|
||||
cursor: CKQueryCursor?,
|
||||
success: @escaping ([String]) -> Swift.Void,
|
||||
failure: @escaping (Error) -> Swift.Void) {
|
||||
|
||||
var allRecordNames = previousRecordNames
|
||||
|
||||
let queryOperation = CKQueryOperation(query: query)
|
||||
// If this isn't the first page of results for this query, resume
|
||||
// where we left off.
|
||||
queryOperation.cursor = cursor
|
||||
// Don't download the file; we're just using the query to get a list of record names.
|
||||
queryOperation.desiredKeys = []
|
||||
queryOperation.recordFetchedBlock = { (record) in
|
||||
assert(record.recordID.recordName.count > 0)
|
||||
allRecordNames.append(record.recordID.recordName)
|
||||
}
|
||||
queryOperation.queryCompletionBlock = { (cursor, error) in
|
||||
if let error = error {
|
||||
Logger.error("\(self.logTag) error fetching all record names: \(error).")
|
||||
failure(error)
|
||||
return
|
||||
}
|
||||
if let cursor = cursor {
|
||||
Logger.verbose("\(self.logTag) fetching more record names \(allRecordNames.count).")
|
||||
// There are more pages of results, continue fetching.
|
||||
fetchAllRecordNamesStep(query: query,
|
||||
previousRecordNames: allRecordNames,
|
||||
cursor: cursor,
|
||||
success: success,
|
||||
failure: failure)
|
||||
return
|
||||
}
|
||||
Logger.info("\(self.logTag) fetched \(allRecordNames.count) record names.")
|
||||
success(allRecordNames)
|
||||
}
|
||||
|
||||
let myContainer = CKContainer.default()
|
||||
let privateDatabase = myContainer.privateCloudDatabase
|
||||
privateDatabase.add(queryOperation)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func checkCloudKitAccess(completion: @escaping (Bool) -> Swift.Void) {
|
||||
CKContainer.default().accountStatus(completionHandler: { (accountStatus, error) in
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#import "zlib.h"
|
||||
#import <Curve25519Kit/Randomness.h>
|
||||
#import <SSZipArchive/SSZipArchive.h>
|
||||
#import <SignalServiceKit/NSData+Base64.h>
|
||||
#import <SignalServiceKit/NSDate+OWS.h>
|
||||
#import <SignalServiceKit/OWSBackgroundTask.h>
|
||||
#import <SignalServiceKit/OWSBackupStorage.h>
|
||||
|
@ -78,6 +79,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
OWSFail(@"%@ attachment could not be encrypted: %@", self.logTag, self.attachmentFilePath);
|
||||
return;
|
||||
}
|
||||
self.tempFilePath = tempFilePath;
|
||||
}
|
||||
|
||||
- (nullable NSString *)encryptAsTempFile:(NSString *)srcFilePath
|
||||
|
@ -114,7 +116,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
|
||||
@property (nonatomic, nullable) OWSBackupStorage *backupStorage;
|
||||
|
||||
@property (nonatomic, nullable) NSData *databaseSalt;
|
||||
@property (nonatomic, nullable) NSData *databaseKeySpec;
|
||||
|
||||
@property (nonatomic, nullable) OWSBackgroundTask *backgroundTask;
|
||||
|
||||
|
@ -209,24 +211,32 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
[self saveToCloud:^(NSError *_Nullable error) {
|
||||
if (error) {
|
||||
[weakSelf failWithError:error];
|
||||
} else {
|
||||
[weakSelf succeed];
|
||||
[self saveToCloud:^(NSError *_Nullable saveError) {
|
||||
if (saveError) {
|
||||
[weakSelf failWithError:saveError];
|
||||
return;
|
||||
}
|
||||
[self cleanUpCloud:^(NSError *_Nullable cleanUpError) {
|
||||
if (cleanUpError) {
|
||||
[weakSelf failWithError:cleanUpError];
|
||||
return;
|
||||
}
|
||||
[weakSelf succeed];
|
||||
}];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)configureExport:(OWSBackupExportBoolCompletion)completion
|
||||
{
|
||||
OWSAssert(completion);
|
||||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
NSString *temporaryDirectory = NSTemporaryDirectory();
|
||||
self.exportDirPath = [temporaryDirectory stringByAppendingString:[NSUUID UUID].UUIDString];
|
||||
NSString *exportDatabaseDirPath = [self.exportDirPath stringByAppendingPathComponent:@"Database"];
|
||||
self.databaseSalt = [Randomness generateRandomBytes:(int)kSQLCipherSaltLength];
|
||||
self.databaseKeySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
|
||||
|
||||
if (![OWSFileSystem ensureDirectoryExists:self.exportDirPath]) {
|
||||
OWSProdLogAndFail(@"%@ Could not create exportDirPath.", self.logTag);
|
||||
|
@ -236,8 +246,8 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
OWSProdLogAndFail(@"%@ Could not create exportDatabaseDirPath.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
if (!self.databaseSalt) {
|
||||
OWSProdLogAndFail(@"%@ Could not create databaseSalt.", self.logTag);
|
||||
if (!self.databaseKeySpec) {
|
||||
OWSProdLogAndFail(@"%@ Could not create databaseKeySpec.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
__weak OWSBackupExport *weakSelf = self;
|
||||
|
@ -246,14 +256,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
if (!backupKey) {
|
||||
return (NSData *)nil;
|
||||
}
|
||||
NSData *_Nullable databaseSalt = weakSelf.databaseSalt;
|
||||
if (!databaseSalt) {
|
||||
return (NSData *)nil;
|
||||
}
|
||||
OWSCAssert(backupKey.length > 0);
|
||||
NSData *_Nullable keySpec =
|
||||
[YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:backupKey saltData:databaseSalt];
|
||||
return keySpec;
|
||||
return weakSelf.databaseKeySpec;
|
||||
};
|
||||
self.backupStorage =
|
||||
[[OWSBackupStorage alloc] initBackupStorageWithDatabaseDirPath:exportDatabaseDirPath keySpecBlock:keySpecBlock];
|
||||
|
@ -394,6 +397,8 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
|
||||
- (void)saveToCloud:(OWSBackupExportCompletion)completion
|
||||
{
|
||||
OWSAssert(completion);
|
||||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
self.databaseRecordMap = [NSMutableDictionary new];
|
||||
|
@ -404,6 +409,8 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
|
||||
- (void)saveNextFileToCloud:(OWSBackupExportCompletion)completion
|
||||
{
|
||||
OWSAssert(completion);
|
||||
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
|
@ -417,8 +424,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
// TODO: Security review.
|
||||
[OWSBackupAPI saveEphemeralDatabaseFileToCloudWithFileUrl:[NSURL fileURLWithPath:filePath]
|
||||
success:^(NSString *recordName) {
|
||||
// Ensure that we continue to perform the backup export
|
||||
// off the main thread.
|
||||
// Ensure that we continue to work off the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
OWSBackupExport *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
|
@ -450,14 +456,18 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
[OWSBackupAPI savePersistentFileOnceToCloudWithFileId:attachmentId
|
||||
fileUrlBlock:^{
|
||||
[attachmentExport prepareForUpload];
|
||||
if (attachmentExport.tempFilePath.length < 1 || attachmentExport.relativeFilePath.length < 1) {
|
||||
if (attachmentExport.tempFilePath.length < 1) {
|
||||
DDLogError(@"%@ attachment export missing temp file path", self.logTag);
|
||||
return (NSURL *)nil;
|
||||
}
|
||||
if (attachmentExport.relativeFilePath.length < 1) {
|
||||
DDLogError(@"%@ attachment export missing relative file path", self.logTag);
|
||||
return (NSURL *)nil;
|
||||
}
|
||||
return [NSURL fileURLWithPath:attachmentExport.tempFilePath];
|
||||
}
|
||||
success:^(NSString *recordName) {
|
||||
// Ensure that we continue to perform the backup export
|
||||
// off the main thread.
|
||||
// Ensure that we continue to work off the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
OWSBackupExport *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
|
@ -468,8 +478,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
});
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
// Ensure that we continue to perform the backup export
|
||||
// off the main thread.
|
||||
// Ensure that we continue to work off the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Attachment files are non-critical so any error uploading them is recoverable.
|
||||
[weakSelf saveNextFileToCloud:completion];
|
||||
|
@ -489,8 +498,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
|
||||
[OWSBackupAPI upsertManifestFileToCloudWithFileUrl:[NSURL fileURLWithPath:self.manifestFilePath]
|
||||
success:^(NSString *recordName) {
|
||||
// Ensure that we continue to perform the backup export
|
||||
// off the main thread.
|
||||
// Ensure that we continue to work off the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
OWSBackupExport *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
|
@ -533,10 +541,13 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
OWSAssert(self.databaseRecordMap.count > 0);
|
||||
OWSAssert(self.attachmentRecordMap);
|
||||
OWSAssert(self.exportDirPath.length > 0);
|
||||
OWSAssert(self.databaseKeySpec.length > 0);
|
||||
|
||||
NSDictionary *json = @{
|
||||
@"database_files" : self.databaseRecordMap,
|
||||
@"attachment_files" : self.attachmentRecordMap,
|
||||
// JSON doesn't support byte arrays.
|
||||
@"database_key_spec" : self.databaseKeySpec.base64EncodedString,
|
||||
};
|
||||
NSError *error;
|
||||
NSData *_Nullable jsonData =
|
||||
|
@ -554,11 +565,92 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (void)cleanUpCloud:(OWSBackupExportCompletion)completion
|
||||
{
|
||||
OWSAssert(completion);
|
||||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
// Now that our backup export has successfully completed,
|
||||
// we try to clean up the cloud. We can safely delete any
|
||||
// records not involved in this backup export.
|
||||
NSMutableSet<NSString *> *activeRecordNames = [NSMutableSet new];
|
||||
|
||||
OWSAssert(self.databaseRecordMap.count > 0);
|
||||
[activeRecordNames addObjectsFromArray:self.databaseRecordMap.allKeys];
|
||||
|
||||
OWSAssert(self.attachmentRecordMap);
|
||||
[activeRecordNames addObjectsFromArray:self.attachmentRecordMap.allKeys];
|
||||
|
||||
OWSAssert(self.manifestRecordName.length > 0);
|
||||
[activeRecordNames addObject:self.manifestRecordName];
|
||||
|
||||
__weak OWSBackupExport *weakSelf = self;
|
||||
[OWSBackupAPI fetchAllRecordNamesWithSuccess:^(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];
|
||||
[obsoleteRecordNames addObjectsFromArray:recordNames];
|
||||
[obsoleteRecordNames minusSet:activeRecordNames];
|
||||
|
||||
DDLogVerbose(@"%@ recordNames: %zd - activeRecordNames: %zd = obsoleteRecordNames: %zd",
|
||||
self.logTag,
|
||||
recordNames.count,
|
||||
activeRecordNames.count,
|
||||
obsoleteRecordNames.count);
|
||||
|
||||
[weakSelf deleteRecordsFromCloud:[obsoleteRecordNames.allObjects mutableCopy] completion:completion];
|
||||
});
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
// Ensure that we continue to work off the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Cloud cleanup is non-critical so any error is recoverable.
|
||||
completion(nil);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)deleteRecordsFromCloud:(NSMutableArray<NSString *> *)obsoleteRecordNames
|
||||
completion:(OWSBackupExportCompletion)completion
|
||||
{
|
||||
OWSAssert(obsoleteRecordNames);
|
||||
OWSAssert(completion);
|
||||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
if (obsoleteRecordNames.count < 1) {
|
||||
// No more records to delete; cleanup is complete.
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *recordName = obsoleteRecordNames.lastObject;
|
||||
[obsoleteRecordNames removeLastObject];
|
||||
|
||||
__weak OWSBackupExport *weakSelf = self;
|
||||
[OWSBackupAPI deleteRecordFromCloudWithRecordName:recordName
|
||||
success:^{
|
||||
// Ensure that we continue to work off the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[weakSelf deleteRecordsFromCloud:obsoleteRecordNames completion:completion];
|
||||
});
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
// Ensure that we continue to work off the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Cloud cleanup is non-critical so any error is recoverable.
|
||||
[weakSelf deleteRecordsFromCloud:obsoleteRecordNames completion:completion];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
// TODO:
|
||||
self.isComplete = YES;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue