Merge branch 'charlesmchen/batchBackupTransfers'

pull/1/head
Matthew Chen 6 years ago
commit f7e0448f1b

@ -81,6 +81,18 @@ NS_ASSUME_NONNULL_BEGIN
actionBlock:^{
[AppEnvironment.shared.backupLazyRestore runIfNecessary];
}]];
[items addObject:[OWSTableItem itemWithTitle:@"Upload 100 CK records"
actionBlock:^{
[DebugUIBackup uploadCKBatch:100];
}]];
[items addObject:[OWSTableItem itemWithTitle:@"Upload 1,000 CK records"
actionBlock:^{
[DebugUIBackup uploadCKBatch:1000];
}]];
[items addObject:[OWSTableItem itemWithTitle:@"Upload 10,000 CK records"
actionBlock:^{
[DebugUIBackup uploadCKBatch:10000];
}]];
return [OWSTableSection sectionWithTitle:self.name items:items];
}
@ -96,9 +108,11 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(success);
NSString *recipientId = self.tsAccountManager.localNumber;
[[self.backup ensureCloudKitAccess].then(^{
return
[OWSBackupAPI saveTestFileToCloudObjcWithRecipientId:recipientId fileUrl:[NSURL fileURLWithPath:filePath]];
NSString *recordName = [OWSBackupAPI recordNameForTestFileWithRecipientId:recipientId];
CKRecord *record = [OWSBackupAPI recordForFileUrl:[NSURL fileURLWithPath:filePath] recordName:recordName];
[[self.backup ensureCloudKitAccess].thenInBackground(^{
return [OWSBackupAPI saveRecordsToCloudObjcWithRecords:@[ record ]];
}) retainUntilComplete];
}
@ -219,6 +233,26 @@ NS_ASSUME_NONNULL_BEGIN
[self.backup logBackupMetadataCache:OWSPrimaryStorage.sharedManager.newDatabaseConnection];
}
+ (void)uploadCKBatch:(NSUInteger)count
{
NSMutableArray<CKRecord *> *records = [NSMutableArray new];
for (NSUInteger i = 0; i < count; i++) {
NSData *_Nullable data = [Randomness generateRandomBytes:32];
OWSAssertDebug(data);
NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:@"pdf"];
BOOL success = [data writeToFile:filePath atomically:YES];
OWSAssertDebug(success);
NSString *recipientId = self.tsAccountManager.localNumber;
NSString *recordName = [OWSBackupAPI recordNameForTestFileWithRecipientId:recipientId];
CKRecord *record = [OWSBackupAPI recordForFileUrl:[NSURL fileURLWithPath:filePath] recordName:recordName];
[records addObject:record];
}
[[OWSBackupAPI saveRecordsToCloudObjcWithRecords:records].thenInBackground(^{
OWSLogVerbose(@"success.");
}) retainUntilComplete];
}
@end
NS_ASSUME_NONNULL_END

@ -24,10 +24,6 @@ import PromiseKit
static let payloadKey = "payload"
static let maxRetries = 5
private class func recordIdForTest() -> String {
return "test-\(NSUUID().uuidString)"
}
private class func database() -> CKDatabase {
let myContainer = CKContainer.default()
let privateDatabase = myContainer.privateCloudDatabase
@ -43,18 +39,8 @@ 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) -> Promise<String> {
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))test-\(NSUUID().uuidString)"
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType)
public class func recordNameForTestFile(recipientId: String) -> String {
return "\(recordNamePrefix(forRecipientId: recipientId))test-\(NSUUID().uuidString)"
}
// "Ephemeral" files are specific to this backup export and will always need to
@ -62,21 +48,9 @@ import PromiseKit
// We wouldn't want to overwrite previous images until the entire backup export is
// complete.
@objc
public class func saveEphemeralFileToCloudObjc(recipientId: String,
label: String,
fileUrl: URL) -> AnyPromise {
return AnyPromise(saveEphemeralFileToCloud(recipientId: recipientId,
label: label,
fileUrl: fileUrl))
}
public class func saveEphemeralFileToCloud(recipientId: String,
label: String,
fileUrl: URL) -> Promise<String> {
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeral-\(label)-\(NSUUID().uuidString)"
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType)
public class func recordNameForEphemeralFile(recipientId: String,
label: String) -> String {
return "\(recordNamePrefix(forRecipientId: recipientId))ephemeral-\(label)-\(NSUUID().uuidString)"
}
// "Persistent" files may be shared between backup export; they should only be saved
@ -142,110 +116,84 @@ import PromiseKit
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 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?) -> Promise<String> {
let recordName = recordNameForPersistentFile(recipientId: recipientId, fileId: fileId)
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) -> Promise<String> {
// We want to use a well-known record id and type for manifest files.
let recordName = recordNameForManifest(recipientId: recipientId)
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) -> Promise<String> {
public class func record(forFileUrl fileUrl: URL,
recordName: String) -> CKRecord {
let recordType = signalBackupRecordType
let recordID = CKRecordID(recordName: recordName)
let record = CKRecord(recordType: recordType, recordID: recordID)
let asset = CKAsset(fileURL: fileUrl)
record[payloadKey] = asset
return saveRecordToCloud(record: record)
return record
}
@objc
public class func saveRecordToCloudObjc(record: CKRecord) -> AnyPromise {
return AnyPromise(saveRecordToCloud(record: record))
public class func saveRecordsToCloudObjc(records: [CKRecord]) -> AnyPromise {
return AnyPromise(saveRecordsToCloud(records: records))
}
public class func saveRecordToCloud(record: CKRecord) -> Promise<String> {
return saveRecordToCloud(record: record,
remainingRetries: maxRetries)
public class func saveRecordsToCloud(records: [CKRecord]) -> Promise<Void> {
// CloudKit's internal limit is 400, but I haven't found a constant for this.
let kMaxBatchSize = 100
return records.chunked(by: kMaxBatchSize).reduce(Promise.value(())) { (promise, batch) -> Promise<Void> in
return promise.then(on: .global()) {
saveRecordsToCloud(records: batch, remainingRetries: maxRetries)
}.done {
Logger.verbose("Saved batch: \(batch.count)")
}
}
}
private class func saveRecordToCloud(record: CKRecord,
remainingRetries: Int) -> Promise<String> {
private class func saveRecordsToCloud(records: [CKRecord],
remainingRetries: Int) -> Promise<Void> {
Logger.verbose("saveRecordToCloud \(record.recordID.recordName)")
let recordNames = records.map { (record) in
return record.recordID.recordName
}
Logger.verbose("recordNames[\(recordNames.count)] \(recordNames[0..<10])...")
return Promise { resolver in
let saveOperation = CKModifyRecordsOperation(recordsToSave: [record ], recordIDsToDelete: nil)
saveOperation.modifyRecordsCompletionBlock = { (records, recordIds, error) in
let saveOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
saveOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, _, error) in
let retry = {
// Only retry records which didn't already succeed.
var savedRecordNames = [String]()
if let savedRecords = savedRecords {
savedRecordNames = savedRecords.map { (record) in
return record.recordID.recordName
}
}
let retryRecords = records.filter({ (record) in
return !savedRecordNames.contains(record.recordID.recordName)
})
saveRecordsToCloud(records: retryRecords,
remainingRetries: remainingRetries - 1)
.done { _ in
resolver.fulfill(())
}.catch { (error) in
resolver.reject(error)
}.retainUntilComplete()
}
let outcome = outcomeForCloudKitError(error: error,
remainingRetries: remainingRetries,
label: "Save Record")
label: "Save Records[\(recordNames.count)]")
switch outcome {
case .success:
let recordName = record.recordID.recordName
resolver.fulfill(recordName)
resolver.fulfill(())
case .failureDoNotRetry(let outcomeError):
resolver.reject(outcomeError)
case .failureRetryAfterDelay(let retryDelay):
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
saveRecordToCloud(record: record,
remainingRetries: remainingRetries - 1)
.done { (recordName) in
resolver.fulfill(recordName)
}.catch { (error) in
resolver.reject(error)
}.retainUntilComplete()
retry()
})
case .failureRetryWithoutDelay:
DispatchQueue.global().async {
saveRecordToCloud(record: record,
remainingRetries: remainingRetries - 1)
.done { (recordName) in
resolver.fulfill(recordName)
}.catch { (error) in
resolver.reject(error)
}.retainUntilComplete()
retry()
}
case .unknownItem:
owsFailDebug("unexpected CloudKit response.")
@ -253,6 +201,11 @@ import PromiseKit
}
}
saveOperation.isAtomic = false
saveOperation.savePolicy = .allKeys
// TODO: use perRecordProgressBlock and perRecordCompletionBlock.
// open var perRecordProgressBlock: ((CKRecord, Double) -> Void)?
// open var perRecordCompletionBlock: ((CKRecord, Error?) -> Void)?
// These APIs are only available in iOS 9.3 and later.
if #available(iOS 9.3, *) {
@ -264,80 +217,6 @@ import PromiseKit
}
}
// Compare:
// * An "upsert" creates a new record if none exists and
// or updates if there is an existing record.
// * 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) -> Promise<String> {
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:
// * An "upsert" creates a new record if none exists and
// or updates if there is an existing record.
// * 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?) -> Promise<String> {
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.")))
}
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: recordType)
}
}
// MARK: - Delete
@objc

@ -18,6 +18,8 @@
#import <SignalServiceKit/TSMessage.h>
#import <SignalServiceKit/TSThread.h>
@import CloudKit;
NS_ASSUME_NONNULL_BEGIN
@class OWSAttachmentExport;
@ -738,155 +740,193 @@ NS_ASSUME_NONNULL_BEGIN
// This method returns YES IFF "work was done and there might be more work to do".
- (AnyPromise *)saveDatabaseFilesToCloud
{
AnyPromise *promise = [AnyPromise promiseWithValue:@(1)];
if (self.isComplete) {
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")];
}
// We need to preserve ordering of database shards.
for (OWSBackupExportItem *item in self.unsavedDatabaseItems) {
NSArray<OWSBackupExportItem *> *items = [self.unsavedDatabaseItems copy];
NSMutableArray<CKRecord *> *records = [NSMutableArray new];
for (OWSBackupExportItem *item in items) {
OWSAssertDebug(item.encryptedItem.filePath.length > 0);
promise
= promise
.thenInBackground(^{
if (self.isComplete) {
return [AnyPromise
promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")];
}
return [OWSBackupAPI
saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId
label:@"database"
fileUrl:[NSURL
fileURLWithPath:item.encryptedItem.filePath]];
})
.thenInBackground(^(NSString *recordName) {
item.recordName = recordName;
[self.savedDatabaseItems addObject:item];
});
NSString *recordName =
[OWSBackupAPI recordNameForEphemeralFileWithRecipientId:self.recipientId label:@"database"];
CKRecord *record =
[OWSBackupAPI recordForFileUrl:[NSURL fileURLWithPath:item.encryptedItem.filePath] recordName:recordName];
[records addObject:record];
}
[self.unsavedDatabaseItems removeAllObjects];
return promise;
// TODO: Expose progress.
return [OWSBackupAPI saveRecordsToCloudObjcWithRecords:records].thenInBackground(^{
OWSAssertDebug(items.count == records.count);
NSUInteger count = MIN(items.count, records.count);
for (NSUInteger i = 0; i < count; i++) {
OWSBackupExportItem *item = items[i];
CKRecord *record = records[i];
OWSAssertDebug(record.recordID.recordName.length > 0);
item.recordName = record.recordID.recordName;
}
[self.savedDatabaseItems addObjectsFromArray:items];
[self.unsavedDatabaseItems removeObjectsInArray:items];
});
}
// This method returns YES IFF "work was done and there might be more work to do".
- (AnyPromise *)saveAttachmentFilesToCloud
{
if (self.isComplete) {
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")];
}
AnyPromise *promise = [AnyPromise promiseWithValue:@(1)];
NSMutableArray<OWSAttachmentExport *> *items = [NSMutableArray new];
NSMutableArray<CKRecord *> *records = [NSMutableArray new];
for (OWSAttachmentExport *attachmentExport in self.unsavedAttachmentExports) {
if ([self tryToSkipAttachmentUpload:attachmentExport]) {
continue;
}
promise = promise.thenInBackground(^{
if (self.isComplete) {
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup export no longer active.")];
@autoreleasepool {
// OWSAttachmentExport is used to lazily write an encrypted copy of the
// attachment to disk.
if (![attachmentExport prepareForUpload]) {
// Attachment files are non-critical so any error preparing them is recoverable.
return @(1);
}
OWSAssertDebug(attachmentExport.relativeFilePath.length > 0);
OWSAssertDebug(attachmentExport.encryptedItem);
}
return [self saveAttachmentFileToCloud:attachmentExport];
});
}
[self.unsavedAttachmentExports removeAllObjects];
return promise;
}
- (AnyPromise *)saveAttachmentFileToCloud:(OWSAttachmentExport *)attachmentExport
{
if (self.lastValidRecordNames) {
// Wherever possible, we do incremental backups and re-use fragments of the last
// backup and/or restore.
// Recycling fragments doesn't just reduce redundant network activity,
// it allows us to skip the local export work, i.e. encryption.
// To do so, we must preserve the metadata for these fragments.
//
// We check two things:
//
// * 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 recordNameForPersistentFileWithRecipientId:self.recipientId
fileId:attachmentExport.attachmentId];
OWSBackupFragment *_Nullable lastBackupFragment = [OWSBackupFragment fetchObjectWithUniqueID:lastRecordName];
if (lastBackupFragment && [self.lastValidRecordNames containsObject:lastRecordName]) {
OWSAssertDebug(lastBackupFragment.encryptionKey.length > 0);
OWSAssertDebug(lastBackupFragment.relativeFilePath.length > 0);
// Recycle the metadata from the last backup's manifest.
OWSBackupEncryptedItem *encryptedItem = [OWSBackupEncryptedItem new];
encryptedItem.encryptionKey = lastBackupFragment.encryptionKey;
attachmentExport.encryptedItem = encryptedItem;
attachmentExport.relativeFilePath = lastBackupFragment.relativeFilePath;
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = attachmentExport.encryptedItem;
exportItem.recordName = lastRecordName;
exportItem.attachmentExport = attachmentExport;
[self.savedAttachmentItems addObject:exportItem];
OWSLogVerbose(@"recycled attachment: %@ as %@",
attachmentExport.attachmentFilePath,
attachmentExport.relativeFilePath);
return [AnyPromise promiseWithValue:@(1)];
}
}
NSURL *_Nullable fileUrl = ^{
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];
}();
@autoreleasepool {
// OWSAttachmentExport is used to lazily write an encrypted copy of the
// attachment to disk.
if (![attachmentExport prepareForUpload]) {
// Attachment files are non-critical so any error uploading them is recoverable.
return [AnyPromise promiseWithValue:@(1)];
}
OWSAssertDebug(attachmentExport.relativeFilePath.length > 0);
OWSAssertDebug(attachmentExport.encryptedItem);
if (!fileUrl) {
// Attachment files are non-critical so any error preparing them is recoverable.
return @(1);
}
NSString *recordName =
[OWSBackupAPI recordNameForPersistentFileWithRecipientId:self.recipientId
fileId:attachmentExport.attachmentId];
CKRecord *record = [OWSBackupAPI recordForFileUrl:fileUrl recordName:recordName];
[records addObject:record];
[items addObject:attachmentExport];
return @(1);
});
}
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) {
void (^cleanup)(void) = ^{
for (OWSAttachmentExport *attachmentExport in items) {
if (![attachmentExport cleanUp]) {
OWSLogError(@"couldn't clean up attachment export.");
// Attachment files are non-critical so any error uploading them is recoverable.
}
}
};
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = attachmentExport.encryptedItem;
exportItem.recordName = recordName;
exportItem.attachmentExport = attachmentExport;
[self.savedAttachmentItems addObject:exportItem];
// Immediately save the record metadata to facilitate export resume.
OWSBackupFragment *backupFragment = [[OWSBackupFragment alloc] initWithUniqueId:recordName];
backupFragment.recordName = recordName;
backupFragment.encryptionKey = exportItem.encryptedItem.encryptionKey;
backupFragment.relativeFilePath = attachmentExport.relativeFilePath;
backupFragment.attachmentId = attachmentExport.attachmentId;
backupFragment.uncompressedDataLength = exportItem.uncompressedDataLength;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[backupFragment saveWithTransaction:transaction];
}];
OWSLogVerbose(
@"saved attachment: %@ as %@", attachmentExport.attachmentFilePath, attachmentExport.relativeFilePath);
// TODO: Expose progress.
return promise
.thenInBackground(^{
return [OWSBackupAPI saveRecordsToCloudObjcWithRecords:records];
})
.catchInBackground(^{
if (![attachmentExport cleanUp]) {
OWSLogError(@"couldn't clean up attachment export.");
// Attachment files are non-critical so any error uploading them is recoverable.
.thenInBackground(^{
OWSAssertDebug(items.count == records.count);
NSUInteger count = MIN(items.count, records.count);
for (NSUInteger i = 0; i < count; i++) {
OWSAttachmentExport *attachmentExport = items[i];
CKRecord *record = records[i];
NSString *recordName = record.recordID.recordName;
OWSAssertDebug(recordName.length > 0);
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = attachmentExport.encryptedItem;
exportItem.recordName = recordName;
exportItem.attachmentExport = attachmentExport;
[self.savedAttachmentItems addObject:exportItem];
// Immediately save the record metadata to facilitate export resume.
OWSBackupFragment *backupFragment = [[OWSBackupFragment alloc] initWithUniqueId:recordName];
backupFragment.recordName = recordName;
backupFragment.encryptionKey = exportItem.encryptedItem.encryptionKey;
backupFragment.relativeFilePath = attachmentExport.relativeFilePath;
backupFragment.attachmentId = attachmentExport.attachmentId;
backupFragment.uncompressedDataLength = exportItem.uncompressedDataLength;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[backupFragment saveWithTransaction:transaction];
}];
OWSLogVerbose(@"saved attachment: %@ as %@",
attachmentExport.attachmentFilePath,
attachmentExport.relativeFilePath);
}
})
.thenInBackground(^{
cleanup();
})
.catchInBackground(^(NSError *error) {
cleanup();
// Attachment files are non-critical so any error uploading them is recoverable.
return [AnyPromise promiseWithValue:@(1)];
return error;
});
}
- (BOOL)tryToSkipAttachmentUpload:(OWSAttachmentExport *)attachmentExport
{
if (!self.lastValidRecordNames) {
return NO;
}
// Wherever possible, we do incremental backups and re-use fragments of the last
// backup and/or restore.
// Recycling fragments doesn't just reduce redundant network activity,
// it allows us to skip the local export work, i.e. encryption.
// To do so, we must preserve the metadata for these fragments.
//
// We check two things:
//
// * 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 *recordName =
[OWSBackupAPI recordNameForPersistentFileWithRecipientId:self.recipientId fileId:attachmentExport.attachmentId];
OWSBackupFragment *_Nullable lastBackupFragment = [OWSBackupFragment fetchObjectWithUniqueID:recordName];
if (!lastBackupFragment || ![self.lastValidRecordNames containsObject:recordName]) {
return NO;
}
OWSAssertDebug(lastBackupFragment.encryptionKey.length > 0);
OWSAssertDebug(lastBackupFragment.relativeFilePath.length > 0);
// Recycle the metadata from the last backup's manifest.
OWSBackupEncryptedItem *encryptedItem = [OWSBackupEncryptedItem new];
encryptedItem.encryptionKey = lastBackupFragment.encryptionKey;
attachmentExport.encryptedItem = encryptedItem;
attachmentExport.relativeFilePath = lastBackupFragment.relativeFilePath;
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = attachmentExport.encryptedItem;
exportItem.recordName = recordName;
exportItem.attachmentExport = attachmentExport;
[self.savedAttachmentItems addObject:exportItem];
OWSLogVerbose(
@"recycled attachment: %@ as %@", attachmentExport.attachmentFilePath, attachmentExport.relativeFilePath);
return YES;
}
- (AnyPromise *)saveLocalProfileAvatarToCloud
{
if (self.isComplete) {
@ -907,15 +947,14 @@ NS_ASSUME_NONNULL_BEGIN
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = encryptedItem;
return [OWSBackupAPI saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId
label:@"local-profile-avatar"
fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath]]
.thenInBackground(^(NSString *recordName) {
exportItem.recordName = recordName;
self.localProfileAvatarItem = exportItem;
return [AnyPromise promiseWithValue:@(1)];
});
NSString *recordName =
[OWSBackupAPI recordNameForEphemeralFileWithRecipientId:self.recipientId label:@"local-profile-avatar"];
CKRecord *record =
[OWSBackupAPI recordForFileUrl:[NSURL fileURLWithPath:encryptedItem.filePath] recordName:recordName];
return [OWSBackupAPI saveRecordsToCloudObjcWithRecords:@[ record ]].thenInBackground(^{
exportItem.recordName = recordName;
self.localProfileAvatarItem = exportItem;
});
}
- (AnyPromise *)saveManifestFileToCloud
@ -932,14 +971,14 @@ NS_ASSUME_NONNULL_BEGIN
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
exportItem.encryptedItem = encryptedItem;
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.
});
NSString *recordName = [OWSBackupAPI recordNameForManifestWithRecipientId:self.recipientId];
CKRecord *record =
[OWSBackupAPI recordForFileUrl:[NSURL fileURLWithPath:encryptedItem.filePath] recordName:recordName];
return [OWSBackupAPI saveRecordsToCloudObjcWithRecords:@[ record ]].thenInBackground(^{
exportItem.recordName = recordName;
self.manifestItem = exportItem;
});
}
- (nullable OWSBackupEncryptedItem *)writeManifestFile

Loading…
Cancel
Save