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

This commit is contained in:
Matthew Chen 2018-03-16 14:58:18 -03:00
parent fed524ba16
commit 2ebd8668b4
6 changed files with 254 additions and 215 deletions

View File

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

View File

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

View File

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

View File

@ -124,7 +124,9 @@ NS_ASSUME_NONNULL_BEGIN
static const int kMaxDBSnapshotSize = 1000;
if (self.cachedItemCount > kMaxDBSnapshotSize) {
return [self flush];
@autoreleasepool {
return [self flush];
}
}
return YES;
@ -137,27 +139,31 @@ NS_ASSUME_NONNULL_BEGIN
return YES;
}
NSData *_Nullable uncompressedData = [self.backupSnapshotBuilder build].data;
NSUInteger uncompressedDataLength = uncompressedData.length;
self.backupSnapshotBuilder = nil;
self.cachedItemCount = 0;
if (!uncompressedData) {
OWSProdLogAndFail(@"%@ couldn't convert database snapshot to data.", self.logTag);
return NO;
// Try to release allocated buffers ASAP.
@autoreleasepool {
NSData *_Nullable uncompressedData = [self.backupSnapshotBuilder build].data;
NSUInteger uncompressedDataLength = uncompressedData.length;
self.backupSnapshotBuilder = nil;
self.cachedItemCount = 0;
if (!uncompressedData) {
OWSProdLogAndFail(@"%@ couldn't convert database snapshot to data.", self.logTag);
return NO;
}
NSData *compressedData = [self.backupIO compressData:uncompressedData];
OWSBackupEncryptedItem *_Nullable encryptedItem = [self.backupIO encryptDataAsTempFile:compressedData];
if (!encryptedItem) {
OWSProdLogAndFail(@"%@ couldn't encrypt database snapshot.", self.logTag);
return NO;
}
OWSBackupExportItem *exportItem = [[OWSBackupExportItem alloc] initWithEncryptedItem:encryptedItem];
exportItem.uncompressedDataLength = @(uncompressedDataLength);
[self.exportItems addObject:exportItem];
}
NSData *compressedData = [self.backupIO compressData:uncompressedData];
OWSBackupEncryptedItem *_Nullable encryptedItem = [self.backupIO encryptDataAsTempFile:compressedData];
if (!encryptedItem) {
OWSProdLogAndFail(@"%@ couldn't encrypt database snapshot.", self.logTag);
return NO;
}
OWSBackupExportItem *exportItem = [[OWSBackupExportItem alloc] initWithEncryptedItem:encryptedItem];
exportItem.uncompressedDataLength = @(uncompressedDataLength);
[self.exportItems addObject:exportItem];
return YES;
}
@ -500,9 +506,11 @@ NS_ASSUME_NONNULL_BEGIN
return NO;
}
if (![exportStream flush]) {
OWSProdLogAndFail(@"%@ Could not flush database snapshots.", self.logTag);
return NO;
@autoreleasepool {
if (![exportStream flush]) {
OWSProdLogAndFail(@"%@ Could not flush database snapshots.", self.logTag);
return NO;
}
}
self.unsavedDatabaseItems = [exportStream.exportItems mutableCopy];
@ -629,15 +637,17 @@ NS_ASSUME_NONNULL_BEGIN
OWSAttachmentExport *attachmentExport = self.unsavedAttachmentExports.lastObject;
[self.unsavedAttachmentExports removeLastObject];
// 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.
[weakSelf saveNextFileToCloud:completion];
return YES;
@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.
[weakSelf saveNextFileToCloud:completion];
return YES;
}
OWSAssert(attachmentExport.relativeFilePath.length > 0);
OWSAssert(attachmentExport.encryptedItem);
}
OWSAssert(attachmentExport.relativeFilePath.length > 0);
OWSAssert(attachmentExport.encryptedItem);
[OWSBackupAPI savePersistentFileOnceToCloudWithFileId:attachmentExport.attachmentId
fileUrlBlock:^{
@ -856,16 +866,21 @@ NS_ASSUME_NONNULL_BEGIN
@"Indicates that the cloud is being cleaned up.")
progress:@(progress)];
NSString *recordName = obsoleteRecordNames.lastObject;
[obsoleteRecordNames removeLastObject];
static const NSUInteger kMaxBatchSize = 100;
NSMutableArray<NSString *> *batchRecordNames = [NSMutableArray new];
while (obsoleteRecordNames.count > 0 && batchRecordNames.count < kMaxBatchSize) {
NSString *recordName = obsoleteRecordNames.lastObject;
[obsoleteRecordNames removeLastObject];
[batchRecordNames addObject:recordName];
}
__weak OWSBackupExportJob *weakSelf = self;
[OWSBackupAPI deleteRecordFromCloudWithRecordName:recordName
[OWSBackupAPI deleteRecordsFromCloudWithRecordNames:batchRecordNames
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
deletedCount:deletedCount + 1
deletedCount:deletedCount + batchRecordNames.count
completion:completion];
});
}
@ -874,7 +889,7 @@ NS_ASSUME_NONNULL_BEGIN
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Cloud cleanup is non-critical so any error is recoverable.
[weakSelf deleteRecordsFromCloud:obsoleteRecordNames
deletedCount:deletedCount + 1
deletedCount:deletedCount + batchRecordNames.count
completion:completion];
});
}];

View File

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

View File

@ -256,7 +256,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSAssert(json);
OWSAssert(key.length);
if (![json isKindOfClass:[NSArray class]]) {
if (![json isKindOfClass:[NSDictionary class]]) {
OWSProdLogAndFail(@"%@ manifest has invalid data: %@.", self.logTag, key);
return nil;
}
@ -408,12 +408,14 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
DDLogError(@"%@ skipping redundant file restore: %@.", self.logTag, dstFilePath);
continue;
}
if (![self.backupIO decryptFileAsFile:item.downloadFilePath
dstFilePath:dstFilePath
encryptionKey:item.encryptionKey]) {
DDLogError(@"%@ attachment could not be restored.", self.logTag);
// Attachment-related errors are recoverable and can be ignored.
continue;
@autoreleasepool {
if (![self.backupIO decryptFileAsFile:item.downloadFilePath
dstFilePath:dstFilePath
encryptionKey:item.encryptionKey]) {
DDLogError(@"%@ attachment could not be restored.", self.logTag);
// Attachment-related errors are recoverable and can be ignored.
continue;
}
}
DDLogError(@"%@ restored file: %@.", self.logTag, item.relativeFilePath);
@ -497,60 +499,62 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
@"Indicates that the backup database is being restored.")
progress:@(count / (CGFloat)self.databaseItems.count)];
NSData *_Nullable compressedData =
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
if (!compressedData) {
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
NSData *_Nullable uncompressedData =
[self.backupIO decompressData:compressedData
uncompressedDataLength:item.uncompressedDataLength.unsignedIntValue];
if (!uncompressedData) {
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
OWSSignalServiceProtosBackupSnapshot *_Nullable entities =
[OWSSignalServiceProtosBackupSnapshot parseFromData:uncompressedData];
if (!entities || entities.entity.count < 1) {
DDLogError(@"%@ missing entities.", self.logTag);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
for (OWSSignalServiceProtosBackupSnapshotBackupEntity *entity in entities.entity) {
NSData *_Nullable entityData = entity.entityData;
if (entityData.length < 1) {
DDLogError(@"%@ missing entity data.", self.logTag);
@autoreleasepool {
NSData *_Nullable compressedData =
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
if (!compressedData) {
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
__block TSYapDatabaseObject *object = nil;
@try {
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:entityData];
object = [unarchiver decodeObjectForKey:@"root"];
if (![object isKindOfClass:[object class]]) {
DDLogError(@"%@ invalid decoded entity: %@.", self.logTag, [object class]);
NSData *_Nullable uncompressedData =
[self.backupIO decompressData:compressedData
uncompressedDataLength:item.uncompressedDataLength.unsignedIntValue];
if (!uncompressedData) {
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
OWSSignalServiceProtosBackupSnapshot *_Nullable entities =
[OWSSignalServiceProtosBackupSnapshot parseFromData:uncompressedData];
if (!entities || entities.entity.count < 1) {
DDLogError(@"%@ missing entities.", self.logTag);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
for (OWSSignalServiceProtosBackupSnapshotBackupEntity *entity in entities.entity) {
NSData *_Nullable entityData = entity.entityData;
if (entityData.length < 1) {
DDLogError(@"%@ missing entity data.", self.logTag);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
} @catch (NSException *exception) {
DDLogError(@"%@ could not decode entity.", self.logTag);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
[object saveWithTransaction:transaction];
copiedEntities++;
NSString *collection = [object.class collection];
NSUInteger restoredEntityCount = restoredEntityCounts[collection].unsignedIntValue;
restoredEntityCounts[collection] = @(restoredEntityCount + 1);
__block TSYapDatabaseObject *object = nil;
@try {
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:entityData];
object = [unarchiver decodeObjectForKey:@"root"];
if (![object isKindOfClass:[object class]]) {
DDLogError(@"%@ invalid decoded entity: %@.", self.logTag, [object class]);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
} @catch (NSException *exception) {
DDLogError(@"%@ could not decode entity.", self.logTag);
// Database-related errors are unrecoverable.
aborted = YES;
return completion(NO);
}
[object saveWithTransaction:transaction];
copiedEntities++;
NSString *collection = [object.class collection];
NSUInteger restoredEntityCount = restoredEntityCounts[collection].unsignedIntValue;
restoredEntityCounts[collection] = @(restoredEntityCount + 1);
}
}
}
}];