Fix bugs in new db representation, add batch record deletion, improve memory management.

pull/1/head
Matthew Chen 7 years ago
parent fed524ba16
commit 2ebd8668b4

@ -90,11 +90,6 @@
self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity"); self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity");
[self updateTableContents]; [self updateTableContents];
dispatch_async(dispatch_get_main_queue(), ^{
[self showBackup];
// [self showDebugUI];
});
} }
- (void)viewWillAppear:(BOOL)animated - (void)viewWillAppear:(BOOL)animated

@ -426,10 +426,6 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
} }
[self checkIfEmptyView]; [self checkIfEmptyView];
dispatch_async(dispatch_get_main_queue(), ^{
[self settingsButtonPressed:nil];
});
} }
- (void)viewWillDisappear:(BOOL)animated - (void)viewWillDisappear:(BOOL)animated

@ -225,28 +225,30 @@ import CloudKit
// MARK: - Delete // MARK: - Delete
@objc @objc
public class func deleteRecordFromCloud(recordName: String, public class func deleteRecordsFromCloud(recordNames: [String],
success: @escaping (()) -> Void, success: @escaping (()) -> Void,
failure: @escaping (Error) -> Void) { failure: @escaping (Error) -> Void) {
deleteRecordFromCloud(recordName: recordName, deleteRecordsFromCloud(recordNames: recordNames,
remainingRetries: maxRetries, remainingRetries: maxRetries,
success: success, success: success,
failure: failure) failure: failure)
} }
private class func deleteRecordFromCloud(recordName: String, private class func deleteRecordsFromCloud(recordNames: [String],
remainingRetries: Int, remainingRetries: Int,
success: @escaping (()) -> Void, success: @escaping (()) -> Void,
failure: @escaping (Error) -> Void) { failure: @escaping (Error) -> Void) {
let recordID = CKRecordID(recordName: recordName) var recordIDs = [CKRecordID]()
for recordName in recordNames {
database().delete(withRecordID: recordID) { recordIDs.append(CKRecordID(recordName: recordName))
(_, error) in }
let deleteOperation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDs)
deleteOperation.modifyRecordsCompletionBlock = { (records, recordIds, error) in
let outcome = outcomeForCloudKitError(error: error, let outcome = outcomeForCloudKitError(error: error,
remainingRetries: remainingRetries, remainingRetries: remainingRetries,
label: "Delete Record") label: "Delete Records")
switch outcome { switch outcome {
case .success: case .success:
success() success()
@ -254,14 +256,14 @@ import CloudKit
failure(outcomeError) failure(outcomeError)
case .failureRetryAfterDelay(let retryDelay): case .failureRetryAfterDelay(let retryDelay):
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: { DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
deleteRecordFromCloud(recordName: recordName, deleteRecordsFromCloud(recordNames: recordNames,
remainingRetries: remainingRetries - 1, remainingRetries: remainingRetries - 1,
success: success, success: success,
failure: failure) failure: failure)
}) })
case .failureRetryWithoutDelay: case .failureRetryWithoutDelay:
DispatchQueue.global().async { DispatchQueue.global().async {
deleteRecordFromCloud(recordName: recordName, deleteRecordsFromCloud(recordNames: recordNames,
remainingRetries: remainingRetries - 1, remainingRetries: remainingRetries - 1,
success: success, success: success,
failure: failure) failure: failure)
@ -271,6 +273,7 @@ import CloudKit
failure(invalidServiceResponseError()) failure(invalidServiceResponseError())
} }
} }
database().add(deleteOperation)
} }
// MARK: - Exists? // MARK: - Exists?

@ -124,8 +124,10 @@ NS_ASSUME_NONNULL_BEGIN
static const int kMaxDBSnapshotSize = 1000; static const int kMaxDBSnapshotSize = 1000;
if (self.cachedItemCount > kMaxDBSnapshotSize) { if (self.cachedItemCount > kMaxDBSnapshotSize) {
@autoreleasepool {
return [self flush]; return [self flush];
} }
}
return YES; return YES;
} }
@ -137,6 +139,9 @@ NS_ASSUME_NONNULL_BEGIN
return YES; return YES;
} }
// Try to release allocated buffers ASAP.
@autoreleasepool {
NSData *_Nullable uncompressedData = [self.backupSnapshotBuilder build].data; NSData *_Nullable uncompressedData = [self.backupSnapshotBuilder build].data;
NSUInteger uncompressedDataLength = uncompressedData.length; NSUInteger uncompressedDataLength = uncompressedData.length;
self.backupSnapshotBuilder = nil; self.backupSnapshotBuilder = nil;
@ -157,6 +162,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSBackupExportItem *exportItem = [[OWSBackupExportItem alloc] initWithEncryptedItem:encryptedItem]; OWSBackupExportItem *exportItem = [[OWSBackupExportItem alloc] initWithEncryptedItem:encryptedItem];
exportItem.uncompressedDataLength = @(uncompressedDataLength); exportItem.uncompressedDataLength = @(uncompressedDataLength);
[self.exportItems addObject:exportItem]; [self.exportItems addObject:exportItem];
}
return YES; return YES;
} }
@ -500,10 +506,12 @@ NS_ASSUME_NONNULL_BEGIN
return NO; return NO;
} }
@autoreleasepool {
if (![exportStream flush]) { if (![exportStream flush]) {
OWSProdLogAndFail(@"%@ Could not flush database snapshots.", self.logTag); OWSProdLogAndFail(@"%@ Could not flush database snapshots.", self.logTag);
return NO; return NO;
} }
}
self.unsavedDatabaseItems = [exportStream.exportItems mutableCopy]; self.unsavedDatabaseItems = [exportStream.exportItems mutableCopy];
@ -629,6 +637,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAttachmentExport *attachmentExport = self.unsavedAttachmentExports.lastObject; OWSAttachmentExport *attachmentExport = self.unsavedAttachmentExports.lastObject;
[self.unsavedAttachmentExports removeLastObject]; [self.unsavedAttachmentExports removeLastObject];
@autoreleasepool {
// OWSAttachmentExport is used to lazily write an encrypted copy of the // OWSAttachmentExport is used to lazily write an encrypted copy of the
// attachment to disk. // attachment to disk.
if (![attachmentExport prepareForUpload]) { if (![attachmentExport prepareForUpload]) {
@ -638,6 +647,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
OWSAssert(attachmentExport.relativeFilePath.length > 0); OWSAssert(attachmentExport.relativeFilePath.length > 0);
OWSAssert(attachmentExport.encryptedItem); OWSAssert(attachmentExport.encryptedItem);
}
[OWSBackupAPI savePersistentFileOnceToCloudWithFileId:attachmentExport.attachmentId [OWSBackupAPI savePersistentFileOnceToCloudWithFileId:attachmentExport.attachmentId
fileUrlBlock:^{ fileUrlBlock:^{
@ -856,16 +866,21 @@ NS_ASSUME_NONNULL_BEGIN
@"Indicates that the cloud is being cleaned up.") @"Indicates that the cloud is being cleaned up.")
progress:@(progress)]; progress:@(progress)];
static const NSUInteger kMaxBatchSize = 100;
NSMutableArray<NSString *> *batchRecordNames = [NSMutableArray new];
while (obsoleteRecordNames.count > 0 && batchRecordNames.count < kMaxBatchSize) {
NSString *recordName = obsoleteRecordNames.lastObject; NSString *recordName = obsoleteRecordNames.lastObject;
[obsoleteRecordNames removeLastObject]; [obsoleteRecordNames removeLastObject];
[batchRecordNames addObject:recordName];
}
__weak OWSBackupExportJob *weakSelf = self; __weak OWSBackupExportJob *weakSelf = self;
[OWSBackupAPI deleteRecordFromCloudWithRecordName:recordName [OWSBackupAPI deleteRecordsFromCloudWithRecordNames:batchRecordNames
success:^{ success:^{
// Ensure that we continue to work 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), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf deleteRecordsFromCloud:obsoleteRecordNames [weakSelf deleteRecordsFromCloud:obsoleteRecordNames
deletedCount:deletedCount + 1 deletedCount:deletedCount + batchRecordNames.count
completion:completion]; completion:completion];
}); });
} }
@ -874,7 +889,7 @@ NS_ASSUME_NONNULL_BEGIN
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Cloud cleanup is non-critical so any error is recoverable. // Cloud cleanup is non-critical so any error is recoverable.
[weakSelf deleteRecordsFromCloud:obsoleteRecordNames [weakSelf deleteRecordsFromCloud:obsoleteRecordNames
deletedCount:deletedCount + 1 deletedCount:deletedCount + batchRecordNames.count
completion:completion]; completion:completion];
}); });
}]; }];

@ -56,13 +56,16 @@ static const NSUInteger kOWSBackupKeyLength = 32;
OWSAssert(srcFilePath.length > 0); OWSAssert(srcFilePath.length > 0);
OWSAssert(encryptionKey.length > 0); OWSAssert(encryptionKey.length > 0);
@autoreleasepool {
// TODO: Encrypt the file without loading it into memory. // TODO: Encrypt the file without loading it into memory.
NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath]; NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath];
if (!srcData) { if (srcData.length < 1) {
OWSProdLogAndFail(@"%@ could not load file into memory", self.logTag); OWSProdLogAndFail(@"%@ could not load file into memory for encryption.", self.logTag);
return nil; return nil;
} }
return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey]; return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey];
}
} }
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData - (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData
@ -74,16 +77,20 @@ static const NSUInteger kOWSBackupKeyLength = 32;
return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey]; return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey];
} }
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData encryptionKey:(NSData *)encryptionKey - (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)unencryptedData
encryptionKey:(NSData *)encryptionKey
{ {
OWSAssert(srcData); OWSAssert(unencryptedData);
OWSAssert(encryptionKey.length > 0); OWSAssert(encryptionKey.length > 0);
@autoreleasepool {
// TODO: Encrypt the data using key; // TODO: Encrypt the data using key;
NSData *encryptedData = unencryptedData;
NSString *dstFilePath = [self.jobTempDirPath stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; NSString *dstFilePath = [self.jobTempDirPath stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
NSError *error; NSError *error;
BOOL success = [srcData writeToFile:dstFilePath options:NSDataWritingAtomic error:&error]; BOOL success = [encryptedData writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
if (!success || error) { if (!success || error) {
OWSProdLogAndFail(@"%@ error writing encrypted data: %@", self.logTag, error); OWSProdLogAndFail(@"%@ error writing encrypted data: %@", self.logTag, error);
return nil; return nil;
@ -93,6 +100,7 @@ static const NSUInteger kOWSBackupKeyLength = 32;
item.filePath = dstFilePath; item.filePath = dstFilePath;
item.encryptionKey = encryptionKey; item.encryptionKey = encryptionKey;
return item; return item;
}
} }
#pragma mark - Decrypt #pragma mark - Decrypt
@ -104,10 +112,12 @@ static const NSUInteger kOWSBackupKeyLength = 32;
OWSAssert(srcFilePath.length > 0); OWSAssert(srcFilePath.length > 0);
OWSAssert(encryptionKey.length > 0); OWSAssert(encryptionKey.length > 0);
@autoreleasepool {
// TODO: Decrypt the file without loading it into memory. // TODO: Decrypt the file without loading it into memory.
NSData *data = [self decryptFileAsData:srcFilePath encryptionKey:encryptionKey]; NSData *data = [self decryptFileAsData:srcFilePath encryptionKey:encryptionKey];
if (!data) { if (data.length < 1) {
return NO; return NO;
} }
@ -120,6 +130,7 @@ static const NSUInteger kOWSBackupKeyLength = 32;
[OWSFileSystem protectFileOrFolderAtPath:dstFilePath]; [OWSFileSystem protectFileOrFolderAtPath:dstFilePath];
return YES; return YES;
}
} }
- (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey - (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey
@ -127,30 +138,36 @@ static const NSUInteger kOWSBackupKeyLength = 32;
OWSAssert(srcFilePath.length > 0); OWSAssert(srcFilePath.length > 0);
OWSAssert(encryptionKey.length > 0); OWSAssert(encryptionKey.length > 0);
@autoreleasepool {
if (![NSFileManager.defaultManager fileExistsAtPath:srcFilePath]) { if (![NSFileManager.defaultManager fileExistsAtPath:srcFilePath]) {
DDLogError(@"%@ missing downloaded file.", self.logTag); DDLogError(@"%@ missing downloaded file.", self.logTag);
return nil; return nil;
} }
// TODO: Decrypt the file without loading it into memory.
NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath]; NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath];
if (!srcData) { if (srcData.length < 1) {
OWSProdLogAndFail(@"%@ could not load file into memory", self.logTag); OWSProdLogAndFail(@"%@ could not load file into memory for decryption.", self.logTag);
return nil; return nil;
} }
NSData *_Nullable dstData = [self decryptDataAsData:srcData encryptionKey:encryptionKey]; NSData *_Nullable dstData = [self decryptDataAsData:srcData encryptionKey:encryptionKey];
return dstData; return dstData;
}
} }
- (nullable NSData *)decryptDataAsData:(NSData *)srcData encryptionKey:(NSData *)encryptionKey - (nullable NSData *)decryptDataAsData:(NSData *)encryptedData encryptionKey:(NSData *)encryptionKey
{ {
OWSAssert(srcData); OWSAssert(encryptedData);
OWSAssert(encryptionKey.length > 0); OWSAssert(encryptionKey.length > 0);
@autoreleasepool {
// TODO: Decrypt the data using key; // TODO: Decrypt the data using key;
NSData *unencryptedData = encryptedData;
return srcData; return unencryptedData;
}
} }
#pragma mark - Compression #pragma mark - Compression
@ -159,6 +176,8 @@ static const NSUInteger kOWSBackupKeyLength = 32;
{ {
OWSAssert(srcData); OWSAssert(srcData);
@autoreleasepool {
if (!srcData) { if (!srcData) {
OWSProdLogAndFail(@"%@ missing unencrypted data.", self.logTag); OWSProdLogAndFail(@"%@ missing unencrypted data.", self.logTag);
return nil; return nil;
@ -172,27 +191,32 @@ static const NSUInteger kOWSBackupKeyLength = 32;
// This assumes that dst will always be smaller than src. // This assumes that dst will always be smaller than src.
// //
// We slightly pad the buffer size to account for the worst case. // We slightly pad the buffer size to account for the worst case.
uint8_t *dstBuffer = malloc(sizeof(uint8_t) * srcLength) + 64 * 1024; size_t dstBufferLength = srcLength + 64 * 1024;
uint8_t *dstBuffer = malloc(sizeof(uint8_t) * dstBufferLength);
if (!dstBuffer) { if (!dstBuffer) {
return nil; return nil;
} }
// TODO: Should we use COMPRESSION_LZFSE? // TODO: Should we use COMPRESSION_LZFSE?
size_t dstLength = compression_encode_buffer(dstBuffer, srcLength, srcBuffer, srcLength, NULL, COMPRESSION_LZFSE); size_t dstLength
NSData *compressedData = [NSData dataWithBytes:dstBuffer length:dstLength]; = compression_encode_buffer(dstBuffer, dstBufferLength, srcBuffer, srcLength, NULL, COMPRESSION_LZFSE);
NSData *compressedData = [NSData dataWithBytesNoCopy:dstBuffer length:dstLength freeWhenDone:YES];
DDLogVerbose(@"%@ compressed %zd -> %zd = %0.2f", DDLogVerbose(@"%@ compressed %zd -> %zd = %0.2f",
self.logTag, self.logTag,
srcLength, srcLength,
dstLength, dstLength,
(srcLength > 0 ? (dstLength / (CGFloat)srcLength) : 0)); (srcLength > 0 ? (dstLength / (CGFloat)srcLength) : 0));
free(dstBuffer);
return compressedData; return compressedData;
}
} }
- (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength - (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength
{ {
OWSAssert(srcData); OWSAssert(srcData);
@autoreleasepool {
if (!srcData) { if (!srcData) {
OWSProdLogAndFail(@"%@ missing unencrypted data.", self.logTag); OWSProdLogAndFail(@"%@ missing unencrypted data.", self.logTag);
return nil; return nil;
@ -204,22 +228,24 @@ static const NSUInteger kOWSBackupKeyLength = 32;
return nil; return nil;
} }
// We pad the buffer to be defensive. // We pad the buffer to be defensive.
uint8_t *dstBuffer = malloc(sizeof(uint8_t) * (uncompressedDataLength + 1024)); size_t dstBufferLength = uncompressedDataLength + 1024;
uint8_t *dstBuffer = malloc(sizeof(uint8_t) * dstBufferLength);
if (!dstBuffer) { if (!dstBuffer) {
return nil; return nil;
} }
// TODO: Should we use COMPRESSION_LZFSE? // TODO: Should we use COMPRESSION_LZFSE?
size_t dstLength = compression_decode_buffer(dstBuffer, srcLength, srcBuffer, srcLength, NULL, COMPRESSION_LZFSE); size_t dstLength
NSData *decompressedData = [NSData dataWithBytes:dstBuffer length:dstLength]; = compression_decode_buffer(dstBuffer, dstBufferLength, srcBuffer, srcLength, NULL, COMPRESSION_LZFSE);
NSData *decompressedData = [NSData dataWithBytesNoCopy:dstBuffer length:dstLength freeWhenDone:YES];
OWSAssert(decompressedData.length == uncompressedDataLength); OWSAssert(decompressedData.length == uncompressedDataLength);
DDLogVerbose(@"%@ decompressed %zd -> %zd = %0.2f", DDLogVerbose(@"%@ decompressed %zd -> %zd = %0.2f",
self.logTag, self.logTag,
srcLength, srcLength,
dstLength, dstLength,
(dstLength > 0 ? (srcLength / (CGFloat)dstLength) : 0)); (dstLength > 0 ? (srcLength / (CGFloat)dstLength) : 0));
free(dstBuffer);
return decompressedData; return decompressedData;
}
} }
@end @end

@ -256,7 +256,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSAssert(json); OWSAssert(json);
OWSAssert(key.length); OWSAssert(key.length);
if (![json isKindOfClass:[NSArray class]]) { if (![json isKindOfClass:[NSDictionary class]]) {
OWSProdLogAndFail(@"%@ manifest has invalid data: %@.", self.logTag, key); OWSProdLogAndFail(@"%@ manifest has invalid data: %@.", self.logTag, key);
return nil; return nil;
} }
@ -408,6 +408,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
DDLogError(@"%@ skipping redundant file restore: %@.", self.logTag, dstFilePath); DDLogError(@"%@ skipping redundant file restore: %@.", self.logTag, dstFilePath);
continue; continue;
} }
@autoreleasepool {
if (![self.backupIO decryptFileAsFile:item.downloadFilePath if (![self.backupIO decryptFileAsFile:item.downloadFilePath
dstFilePath:dstFilePath dstFilePath:dstFilePath
encryptionKey:item.encryptionKey]) { encryptionKey:item.encryptionKey]) {
@ -415,6 +416,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
// Attachment-related errors are recoverable and can be ignored. // Attachment-related errors are recoverable and can be ignored.
continue; continue;
} }
}
DDLogError(@"%@ restored file: %@.", self.logTag, item.relativeFilePath); DDLogError(@"%@ restored file: %@.", self.logTag, item.relativeFilePath);
} }
@ -497,6 +499,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
@"Indicates that the backup database is being restored.") @"Indicates that the backup database is being restored.")
progress:@(count / (CGFloat)self.databaseItems.count)]; progress:@(count / (CGFloat)self.databaseItems.count)];
@autoreleasepool {
NSData *_Nullable compressedData = NSData *_Nullable compressedData =
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey]; [self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
if (!compressedData) { if (!compressedData) {
@ -553,6 +556,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
restoredEntityCounts[collection] = @(restoredEntityCount + 1); restoredEntityCounts[collection] = @(restoredEntityCount + 1);
} }
} }
}
}]; }];
if (self.isComplete || aborted) { if (self.isComplete || aborted) {

Loading…
Cancel
Save