Convert backup logic to use promises.

This commit is contained in:
Matthew Chen 2018-11-27 16:12:46 -05:00
parent af477d3bf3
commit 44d0ad34f5
3 changed files with 221 additions and 212 deletions

View File

@ -88,20 +88,10 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(success);
NSString *recipientId = self.tsAccountManager.localNumber;
[[self.backup checkCloudKitAccess]
.then(^{
[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.
}];
})
.catch(^(NSError *error){
// Do nothing, the API method will log for us.
}) retainUntilComplete];
[[self.backup checkCloudKitAccess].then(^{
return
[OWSBackupAPI saveTestFileToCloudObjcWithRecipientId:recipientId fileUrl:[NSURL fileURLWithPath:filePath]];
}) retainUntilComplete];
}
+ (void)checkForBackup

View File

@ -43,16 +43,18 @@ import PromiseKit
// MARK: - Upload
@objc
public class func saveTestFileToCloudObjc(recipientId: String,
fileUrl: URL) -> AnyPromise {
return AnyPromise(saveTestFileToCloud(recipientId: recipientId,
fileUrl: fileUrl))
}
public class func saveTestFileToCloud(recipientId: String,
fileUrl: URL,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
fileUrl: URL) -> Promise<String> {
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))test-\(NSUUID().uuidString)"
saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType,
success: success,
failure: failure)
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType)
}
// "Ephemeral" files are specific to this backup export and will always need to
@ -60,16 +62,18 @@ import PromiseKit
// We wouldn't want to overwrite previous images until the entire backup export is
// complete.
@objc
public class func saveEphemeralDatabaseFileToCloudObjc(recipientId: String,
fileUrl: URL) -> AnyPromise {
return AnyPromise(saveEphemeralDatabaseFileToCloud(recipientId: recipientId,
fileUrl: fileUrl))
}
public class func saveEphemeralDatabaseFileToCloud(recipientId: String,
fileUrl: URL,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
fileUrl: URL) -> Promise<String> {
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeralFile-\(NSUUID().uuidString)"
saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType,
success: success,
failure: failure)
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType)
}
// "Persistent" files may be shared between backup export; they should only be saved
@ -125,9 +129,9 @@ import PromiseKit
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
// Match must be at start of string and non-empty.
Logger.warn("invalid match: \(recordName) \(firstRange)")
continue
}
let recipientId = (recordName as NSString).substring(with: firstRange) as String
recipientIds.append(recipientId)
@ -139,57 +143,75 @@ import PromiseKit
// once. For example, attachment files should only be uploaded once. Subsequent
// backups can reuse the same record.
@objc
public class func savePersistentFileOnceToCloudObjc(recipientId: String,
fileId: String,
fileUrlBlock: @escaping () -> URL?) -> AnyPromise {
return AnyPromise(savePersistentFileOnceToCloud(recipientId: recipientId,
fileId: fileId,
fileUrlBlock: fileUrlBlock))
}
public class func savePersistentFileOnceToCloud(recipientId: String,
fileId: String,
fileUrlBlock: @escaping () -> URL?,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
fileUrlBlock: @escaping () -> URL?) -> Promise<String> {
let recordName = recordNameForPersistentFile(recipientId: recipientId, fileId: fileId)
saveFileOnceToCloud(recordName: recordName,
recordType: signalBackupRecordType,
fileUrlBlock: fileUrlBlock,
success: success,
failure: failure)
return saveFileOnceToCloud(recordName: recordName,
recordType: signalBackupRecordType,
fileUrlBlock: fileUrlBlock)
}
@objc
public class func upsertManifestFileToCloudObjc(recipientId: String,
fileUrl: URL) -> AnyPromise {
return AnyPromise(upsertManifestFileToCloud(recipientId: recipientId,
fileUrl: fileUrl))
}
public class func upsertManifestFileToCloud(recipientId: String,
fileUrl: URL,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
fileUrl: URL) -> Promise<String> {
// We want to use a well-known record id and type for manifest files.
let recordName = recordNameForManifest(recipientId: recipientId)
upsertFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType,
success: success,
failure: failure)
return upsertFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType)
}
@objc
public class func saveFileToCloudObjc(fileUrl: URL,
recordName: String,
recordType: String) -> AnyPromise {
return AnyPromise(saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: recordType))
}
public class func saveFileToCloud(fileUrl: URL,
recordName: String,
recordType: String,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
recordType: String) -> Promise<String> {
let recordID = CKRecordID(recordName: recordName)
let record = CKRecord(recordType: recordType, recordID: recordID)
let asset = CKAsset(fileURL: fileUrl)
record[payloadKey] = asset
saveRecordToCloud(record: record,
success: success,
failure: failure)
return saveRecordToCloud(record: record)
}
@objc
public class func saveRecordToCloud(record: CKRecord,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
public class func saveRecordToCloudObjc(record: CKRecord) -> AnyPromise {
return AnyPromise(saveRecordToCloud(record: record))
}
public class func saveRecordToCloud(record: CKRecord) -> Promise<String> {
let (promise, resolver) = Promise<String>.pending()
saveRecordToCloud(record: record,
remainingRetries: maxRetries,
success: success,
failure: failure)
success: { (recordName) in
resolver.fulfill(recordName)
},
failure: { (error) in
resolver.reject(error)
})
return promise.retainUntilComplete()
}
private class func saveRecordToCloud(record: CKRecord,
@ -201,8 +223,8 @@ import PromiseKit
saveOperation.modifyRecordsCompletionBlock = { (records, recordIds, error) in
let outcome = outcomeForCloudKitError(error: error,
remainingRetries: remainingRetries,
label: "Save Record")
remainingRetries: remainingRetries,
label: "Save Record")
switch outcome {
case .success:
let recordName = record.recordID.recordName
@ -245,32 +267,33 @@ import PromiseKit
// * A "save once" creates a new record if none exists and
// does nothing if there is an existing record.
@objc
public class func upsertFileToCloudObjc(fileUrl: URL,
recordName: String,
recordType: String) -> AnyPromise {
return AnyPromise(upsertFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: recordType))
}
public class func upsertFileToCloud(fileUrl: URL,
recordName: String,
recordType: String,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
recordType: String) -> Promise<String> {
checkForFileInCloud(recordName: recordName,
remainingRetries: maxRetries,
success: { (record) in
if let record = record {
// Record found, updating existing record.
let asset = CKAsset(fileURL: fileUrl)
record[payloadKey] = asset
saveRecordToCloud(record: record,
success: success,
failure: failure)
} else {
// No record found, saving new record.
saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: recordType,
success: success,
failure: failure)
}
},
failure: failure)
return checkForFileInCloud(recordName: recordName,
remainingRetries: maxRetries)
.then { (record: CKRecord?) -> Promise<String> in
if let record = record {
// Record found, updating existing record.
let asset = CKAsset(fileURL: fileUrl)
record[payloadKey] = asset
return saveRecordToCloud(record: record)
}
// No record found, saving new record.
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: recordType)
}
}
// Compare:
@ -279,54 +302,55 @@ import PromiseKit
// * A "save once" creates a new record if none exists and
// does nothing if there is an existing record.
@objc
public class func saveFileOnceToCloudObjc(recordName: String,
recordType: String,
fileUrlBlock: @escaping () -> URL?) -> AnyPromise {
return AnyPromise(saveFileOnceToCloud(recordName: recordName,
recordType: recordType,
fileUrlBlock: fileUrlBlock))
}
public class func saveFileOnceToCloud(recordName: String,
recordType: String,
fileUrlBlock: @escaping () -> URL?,
success: @escaping (String) -> Void,
failure: @escaping (Error) -> Void) {
fileUrlBlock: @escaping () -> URL?) -> Promise<String> {
checkForFileInCloud(recordName: recordName,
remainingRetries: maxRetries,
success: { (record) in
if record != nil {
// Record found, skipping save.
success(recordName)
} else {
// No record found, saving new record.
guard let fileUrl = fileUrlBlock() else {
Logger.error("error preparing file for upload.")
failure(OWSErrorWithCodeDescription(.exportBackupError,
NSLocalizedString("BACKUP_EXPORT_ERROR_SAVE_FILE_TO_CLOUD_FAILED",
comment: "Error indicating the backup export failed to save a file to the cloud.")))
return
}
return checkForFileInCloud(recordName: recordName,
remainingRetries: maxRetries)
.then { (record: CKRecord?) -> Promise<String> in
if record != nil {
// Record found, skipping save.
return Promise.value(recordName)
}
// No record found, saving new record.
guard let fileUrl = fileUrlBlock() else {
Logger.error("error preparing file for upload.")
return Promise(error: OWSErrorWithCodeDescription(.exportBackupError,
NSLocalizedString("BACKUP_EXPORT_ERROR_SAVE_FILE_TO_CLOUD_FAILED",
comment: "Error indicating the backup export failed to save a file to the cloud.")))
}
saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: recordType,
success: success,
failure: failure)
}
},
failure: failure)
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: recordType)
}
}
// MARK: - Delete
@objc
public class func deleteRecordsFromCloud(recordNames: [String],
success: @escaping () -> Void,
failure: @escaping (Error) -> Void) {
success: @escaping () -> Void,
failure: @escaping (Error) -> Void) {
deleteRecordsFromCloud(recordNames: recordNames,
remainingRetries: maxRetries,
success: success,
failure: failure)
remainingRetries: maxRetries,
success: success,
failure: failure)
}
private class func deleteRecordsFromCloud(recordNames: [String],
remainingRetries: Int,
success: @escaping () -> Void,
failure: @escaping (Error) -> Void) {
remainingRetries: Int,
success: @escaping () -> Void,
failure: @escaping (Error) -> Void) {
let recordIDs = recordNames.map { CKRecordID(recordName: $0) }
let deleteOperation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDs)
@ -343,16 +367,16 @@ import PromiseKit
case .failureRetryAfterDelay(let retryDelay):
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
deleteRecordsFromCloud(recordNames: recordNames,
remainingRetries: remainingRetries - 1,
success: success,
failure: failure)
remainingRetries: remainingRetries - 1,
success: success,
failure: failure)
})
case .failureRetryWithoutDelay:
DispatchQueue.global().async {
deleteRecordsFromCloud(recordNames: recordNames,
remainingRetries: remainingRetries - 1,
success: success,
failure: failure)
remainingRetries: remainingRetries - 1,
success: success,
failure: failure)
}
case .unknownItem:
owsFailDebug("unexpected CloudKit response.")
@ -365,9 +389,10 @@ import PromiseKit
// MARK: - Exists?
private class func checkForFileInCloud(recordName: String,
remainingRetries: Int,
success: @escaping (CKRecord?) -> Void,
failure: @escaping (Error) -> Void) {
remainingRetries: Int) -> Promise<CKRecord?> {
let (promise, resolver) = Promise<CKRecord?>.pending()
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
@ -376,39 +401,46 @@ import PromiseKit
fetchOperation.perRecordCompletionBlock = { (record, recordId, error) in
let outcome = outcomeForCloudKitError(error: error,
remainingRetries: remainingRetries,
label: "Check for Record")
remainingRetries: remainingRetries,
label: "Check for Record")
switch outcome {
case .success:
guard let record = record else {
owsFailDebug("missing fetching record.")
failure(invalidServiceResponseError())
resolver.reject(invalidServiceResponseError())
return
}
// Record found.
success(record)
resolver.fulfill(record)
case .failureDoNotRetry(let outcomeError):
failure(outcomeError)
resolver.reject(outcomeError)
case .failureRetryAfterDelay(let retryDelay):
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
checkForFileInCloud(recordName: recordName,
remainingRetries: remainingRetries - 1,
success: success,
failure: failure)
remainingRetries: remainingRetries - 1)
.done { (record) in
resolver.fulfill(record)
}.catch { (error) in
resolver.reject(error)
}
})
case .failureRetryWithoutDelay:
DispatchQueue.global().async {
checkForFileInCloud(recordName: recordName,
remainingRetries: remainingRetries - 1,
success: success,
failure: failure)
remainingRetries: remainingRetries - 1)
.done { (record) in
resolver.fulfill(record)
}.catch { (error) in
resolver.reject(error)
}
}
case .unknownItem:
// Record not found.
success(nil)
resolver.fulfill(nil)
}
}
database().add(fetchOperation)
return promise.retainUntilComplete()
}
@objc
@ -418,17 +450,12 @@ import PromiseKit
public class func checkForManifestInCloud(recipientId: String) -> Promise<Bool> {
let (promise, resolver) = Promise<Bool>.pending()
let recordName = recordNameForManifest(recipientId: recipientId)
checkForFileInCloud(recordName: recordName,
remainingRetries: maxRetries,
success: { (record) in
resolver.fulfill(record != nil)
},
failure: { (error) in
resolver.reject(error)
})
return promise
return checkForFileInCloud(recordName: recordName,
remainingRetries: maxRetries)
.map { (record) in
return record != nil
}
}
@objc
@ -506,8 +533,8 @@ import PromiseKit
queryOperation.queryCompletionBlock = { (cursor, error) in
let outcome = outcomeForCloudKitError(error: error,
remainingRetries: remainingRetries,
label: "Fetch All Records")
remainingRetries: remainingRetries,
label: "Fetch All Records")
switch outcome {
case .success:
if let cursor = cursor {
@ -626,8 +653,8 @@ import PromiseKit
fetchOperation.perRecordCompletionBlock = { (record, recordId, error) in
let outcome = outcomeForCloudKitError(error: error,
remainingRetries: remainingRetries,
label: "Download Record")
remainingRetries: remainingRetries,
label: "Download Record")
switch outcome {
case .success:
guard let record = record else {
@ -734,8 +761,8 @@ import PromiseKit
}
private class func outcomeForCloudKitError(error: Error?,
remainingRetries: Int,
label: String) -> APIOutcome {
remainingRetries: Int,
label: String) -> APIOutcome {
if let error = error as? CKError {
if error.code == CKError.unknownItem {
// This is not always an error for our purposes.

View File

@ -755,20 +755,19 @@ NS_ASSUME_NONNULL_BEGIN
if (self.isComplete) {
return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active."));
}
[OWSBackupAPI saveEphemeralDatabaseFileToCloudWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:item.encryptedItem.filePath]
success:^(NSString *recordName) {
item.recordName = recordName;
[self.savedDatabaseItems addObject:item];
resolve(@(1));
}
failure:^(NSError *error) {
// Database files are critical so any error uploading them is unrecoverable.
OWSLogVerbose(@"error while saving file: %@", item.encryptedItem.filePath);
resolve(error);
}];
}];
resolve(@(1));
}]
.thenInBackground(^{
return [OWSBackupAPI
saveEphemeralDatabaseFileToCloudObjcWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:item.encryptedItem
.filePath]];
})
.thenInBackground(^(NSString *recordName) {
item.recordName = recordName;
[self.savedDatabaseItems addObject:item];
return [AnyPromise promiseWithValue:@(1)];
});
});
}
[self.unsavedDatabaseItems removeAllObjects];
@ -782,19 +781,17 @@ NS_ASSUME_NONNULL_BEGIN
for (OWSAttachmentExport *attachmentExport in self.unsavedAttachmentExports) {
promise = promise.thenInBackground(^{
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
if (self.isComplete) {
return resolve(OWSBackupErrorWithDescription(@"Backup export no longer active."));
}
[self saveAttachmentFileToCloud:attachmentExport resolve:resolve];
}];
if (self.isComplete) {
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")];
}
return [self saveAttachmentFileToCloud:attachmentExport];
});
}
[self.unsavedAttachmentExports removeAllObjects];
return promise;
}
- (void)saveAttachmentFileToCloud:(OWSAttachmentExport *)attachmentExport resolve:(PMKResolver)resolve
- (AnyPromise *)saveAttachmentFileToCloud:(OWSAttachmentExport *)attachmentExport
{
if (self.lastValidRecordNames) {
// Wherever possible, we do incremental backups and re-use fragments of the last
@ -831,7 +828,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSLogVerbose(@"recycled attachment: %@ as %@",
attachmentExport.attachmentFilePath,
attachmentExport.relativeFilePath);
return resolve(@(1));
return [AnyPromise promiseWithValue:@(1)];
}
}
@ -840,26 +837,27 @@ NS_ASSUME_NONNULL_BEGIN
// attachment to disk.
if (![attachmentExport prepareForUpload]) {
// Attachment files are non-critical so any error uploading them is recoverable.
return resolve(@(1));
return [AnyPromise promiseWithValue:@(1)];
}
OWSAssertDebug(attachmentExport.relativeFilePath.length > 0);
OWSAssertDebug(attachmentExport.encryptedItem);
}
[OWSBackupAPI savePersistentFileOnceToCloudWithRecipientId:self.recipientId
fileId:attachmentExport.attachmentId
fileUrlBlock:^{
if (attachmentExport.encryptedItem.filePath.length < 1) {
OWSLogError(@"attachment export missing temp file path");
return (NSURL *)nil;
}
if (attachmentExport.relativeFilePath.length < 1) {
OWSLogError(@"attachment export missing relative file path");
return (NSURL *)nil;
}
return [NSURL fileURLWithPath:attachmentExport.encryptedItem.filePath];
}
success:^(NSString *recordName) {
return [OWSBackupAPI
savePersistentFileOnceToCloudObjcWithRecipientId:self.recipientId
fileId:attachmentExport.attachmentId
fileUrlBlock:^{
if (attachmentExport.encryptedItem.filePath.length < 1) {
OWSLogError(@"attachment export missing temp file path");
return (NSURL *)nil;
}
if (attachmentExport.relativeFilePath.length < 1) {
OWSLogError(@"attachment export missing relative file path");
return (NSURL *)nil;
}
return [NSURL fileURLWithPath:attachmentExport.encryptedItem.filePath];
}]
.thenInBackground(^(NSString *recordName) {
if (![attachmentExport cleanUp]) {
OWSLogError(@"couldn't clean up attachment export.");
// Attachment files are non-critical so any error uploading them is recoverable.
@ -882,17 +880,17 @@ NS_ASSUME_NONNULL_BEGIN
OWSLogVerbose(
@"saved attachment: %@ as %@", attachmentExport.attachmentFilePath, attachmentExport.relativeFilePath);
return resolve(@(1));
}
failure:^(NSError *error) {
return [AnyPromise promiseWithValue:@(1)];
})
.catchInBackground(^{
if (![attachmentExport cleanUp]) {
OWSLogError(@"couldn't clean up attachment export.");
// Attachment files are non-critical so any error uploading them is recoverable.
}
// Attachment files are non-critical so any error uploading them is recoverable.
return resolve(@(1));
}];
return [AnyPromise promiseWithValue:@(1)];
});
}
- (AnyPromise *)saveManifestFileToCloud
@ -909,21 +907,15 @@ NS_ASSUME_NONNULL_BEGIN
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = encryptedItem;
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[OWSBackupAPI upsertManifestFileToCloudWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath]
success:^(NSString *recordName) {
exportItem.recordName = recordName;
self.manifestItem = exportItem;
return [OWSBackupAPI upsertManifestFileToCloudObjcWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath]]
.thenInBackground(^(NSString *recordName) {
exportItem.recordName = recordName;
self.manifestItem = exportItem;
// All files have been saved to the cloud.
resolve(@(1));
}
failure:^(NSError *error) {
// The manifest file is critical so any error uploading them is unrecoverable.
resolve(error);
}];
}];
// All files have been saved to the cloud.
return [AnyPromise promiseWithValue:@(1)];
});
}
- (nullable OWSBackupEncryptedItem *)writeManifestFile