Implement backup import logic.

This commit is contained in:
Matthew Chen 2018-03-08 11:52:57 -03:00 committed by Matthew Chen
parent f53f1fb46a
commit 04c527a0f4
8 changed files with 1023 additions and 14 deletions

View File

@ -38,6 +38,7 @@
340FC8C2204DDF67007AEB0F /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 340FC8C1204DDF66007AEB0F /* CloudKit.framework */; };
340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8C4204DE223007AEB0F /* DebugUIBackup.m */; };
340FC8C7204DE64D007AEB0F /* OWSBackupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8C6204DE64D007AEB0F /* OWSBackupAPI.swift */; };
340FC8CA20517B84007AEB0F /* OWSBackupImport.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8C820517B84007AEB0F /* OWSBackupImport.m */; };
341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */ = {isa = PBXBuildFile; fileRef = 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */; };
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; };
@ -577,6 +578,8 @@
340FC8C3204DE223007AEB0F /* DebugUIBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIBackup.h; sourceTree = "<group>"; };
340FC8C4204DE223007AEB0F /* DebugUIBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIBackup.m; sourceTree = "<group>"; };
340FC8C6204DE64D007AEB0F /* OWSBackupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupAPI.swift; sourceTree = "<group>"; };
340FC8C820517B84007AEB0F /* OWSBackupImport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupImport.m; sourceTree = "<group>"; };
340FC8C920517B84007AEB0F /* OWSBackupImport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImport.h; sourceTree = "<group>"; };
341458471FBE11C4005ABCF9 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = "<group>"; };
341F2C0D1F2B8AE700D07D6B /* DebugUIMisc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMisc.h; sourceTree = "<group>"; };
341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMisc.m; sourceTree = "<group>"; };
@ -1936,6 +1939,8 @@
340FC8C6204DE64D007AEB0F /* OWSBackupAPI.swift */,
340FC8BE204DB7D1007AEB0F /* OWSBackupExport.h */,
340FC8BF204DB7D2007AEB0F /* OWSBackupExport.m */,
340FC8C920517B84007AEB0F /* OWSBackupImport.h */,
340FC8C820517B84007AEB0F /* OWSBackupImport.m */,
4579431C1E7C8CE9008ED0C0 /* Pastelog.h */,
4579431D1E7C8CE9008ED0C0 /* Pastelog.m */,
450DF2041E0D74AC003D14BE /* Platform.swift */,
@ -3170,6 +3175,7 @@
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */,
345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */,
340FC8CA20517B84007AEB0F /* OWSBackupImport.m in Sources */,
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,

View File

@ -28,6 +28,14 @@ NS_ASSUME_NONNULL_BEGIN
actionBlock:^{
[DebugUIBackup backupTestFile];
}]];
[items addObject:[OWSTableItem itemWithTitle:@"Check for CloudKit backup"
actionBlock:^{
[DebugUIBackup checkForBackup];
}]];
[items addObject:[OWSTableItem itemWithTitle:@"Try to restore CloudKit backup"
actionBlock:^{
[DebugUIBackup tryToImportBackup];
}]];
return [OWSTableSection sectionWithTitle:self.name items:items];
}
@ -55,6 +63,25 @@ NS_ASSUME_NONNULL_BEGIN
}];
}
+ (void)checkForBackup
{
DDLogInfo(@"%@ checkForBackup.", self.logTag);
[OWSBackup.sharedManager checkCanImportBackup:^(BOOL value) {
DDLogInfo(@"%@ has backup available for import? %d", self.logTag, value);
}
failure:^(NSError *error){
// Do nothing.
}];
}
+ (void)tryToImportBackup
{
DDLogInfo(@"%@ tryToImportBackup.", self.logTag);
[OWSBackup.sharedManager tryToImportBackup];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -6,7 +6,7 @@ NS_ASSUME_NONNULL_BEGIN
extern NSString *const NSNotificationNameBackupStateDidChange;
typedef void (^OWSBackupBoolBlock)(BOOL success);
typedef void (^OWSBackupBoolBlock)(BOOL value);
typedef void (^OWSBackupErrorBlock)(NSError *error);
typedef NS_ENUM(NSUInteger, OWSBackupState) {
@ -24,16 +24,43 @@ typedef NS_ENUM(NSUInteger, OWSBackupState) {
@interface OWSBackup : NSObject
@property (nonatomic, readonly) OWSBackupState backupExportState;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedManager;
- (void)setup;
#pragma mark - Backup Export
@property (nonatomic, readonly) OWSBackupState backupExportState;
// If a "backup export" is in progress (see backupExportState),
// backupExportDescription _might_ contain a string that describes
// the current phase and backupExportProgress _might_ contain a
// 0.0<=x<=1.0 progress value that indicates progress within the
// current phase.
@property (nonatomic, readonly, nullable) NSString *backupExportDescription;
@property (nonatomic, readonly, nullable) NSNumber *backupExportProgress;
- (BOOL)isBackupEnabled;
- (void)setIsBackupEnabled:(BOOL)value;
- (void)setup;
#pragma mark - Backup Import
@property (nonatomic, readonly) OWSBackupState backupImportState;
// If a "backup import" is in progress (see backupImportState),
// backupImportDescription _might_ contain a string that describes
// the current phase and backupImportProgress _might_ contain a
// 0.0<=x<=1.0 progress value that indicates progress within the
// current phase.
@property (nonatomic, readonly, nullable) NSString *backupImportDescription;
@property (nonatomic, readonly, nullable) NSNumber *backupImportProgress;
- (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure;
// TODO: After a successful import, we should enable backup and
// preserve our PIN and/or private key so that restored users
// continues to backup.
- (void)tryToImportBackup;
@end

View File

@ -5,6 +5,7 @@
#import "OWSBackup.h"
#import "NSNotificationCenter+OWS.h"
#import "OWSBackupExport.h"
#import "OWSBackupImport.h"
#import "Signal-Swift.h"
#import <Curve25519Kit/Randomness.h>
#import <SignalServiceKit/AppContext.h>
@ -25,13 +26,22 @@ NSString *const OWSBackup_LastExportFailureDateKey = @"OWSBackup_LastExportFailu
NS_ASSUME_NONNULL_BEGIN
// TODO: Observe Reachability.
@interface OWSBackup () <OWSBackupExportDelegate>
@interface OWSBackup () <OWSBackupExportDelegate, OWSBackupImportDelegate>
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
// This property should only be accessed on the main thread.
@property (nonatomic, nullable) OWSBackupExport *backupExport;
// This property should only be accessed on the main thread.
@property (nonatomic, nullable) OWSBackupImport *backupImport;
@property (nonatomic, nullable) NSString *backupExportDescription;
@property (nonatomic, nullable) NSNumber *backupExportProgress;
@property (nonatomic, nullable) NSString *backupImportDescription;
@property (nonatomic, nullable) NSNumber *backupImportProgress;
@end
#pragma mark -
@ -70,6 +80,7 @@ NS_ASSUME_NONNULL_BEGIN
_dbConnection = primaryStorage.newDatabaseConnection;
_backupExportState = OWSBackupState_Idle;
_backupImportState = OWSBackupState_Idle;
OWSSingletonAssert();
@ -183,9 +194,7 @@ NS_ASSUME_NONNULL_BEGIN
inCollection:OWSPrimaryStorage_OWSBackupCollection];
}
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationNameBackupStateDidChange
object:nil
userInfo:nil];
[self postDidChangeNotification];
[self ensureBackupExportState];
}
@ -220,6 +229,13 @@ NS_ASSUME_NONNULL_BEGIN
if (lastExportFailureDate && fabs(lastExportFailureDate.timeIntervalSinceNow) < kRetryAfterFailure) {
return NO;
}
// Don't export backup if there's an import in progress.
//
// This conflict shouldn't occur in production since we won't enable backup
// export until an import is complete, but this could happen in development.
if (self.backupImport) {
return NO;
}
// TODO: There's other conditions that affect this decision,
// e.g. Reachability, wifi v. cellular, etc.
@ -264,9 +280,7 @@ NS_ASSUME_NONNULL_BEGIN
BOOL stateDidChange = _backupExportState != backupExportState;
_backupExportState = backupExportState;
if (stateDidChange) {
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationNameBackupStateDidChange
object:nil
userInfo:nil];
[self postDidChangeNotification];
}
}
@ -290,6 +304,24 @@ NS_ASSUME_NONNULL_BEGIN
}];
}
- (void)tryToImportBackup
{
OWSAssertIsOnMainThread();
OWSAssert(!self.backupImport);
// In development, make sure there's no export or import in progress.
[self.backupExport cancel];
self.backupExport = nil;
[self.backupImport cancel];
self.backupImport = nil;
_backupImportState = OWSBackupState_InProgress;
self.backupImport =
[[OWSBackupImport alloc] initWithDelegate:self primaryStorage:[OWSPrimaryStorage sharedManager]];
[self.backupImport startAsync];
}
#pragma mark -
- (void)applicationDidBecomeActive:(NSNotification *)notification
@ -316,6 +348,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)backupExportDidSucceed:(OWSBackupExport *)backupExport
{
OWSAssertIsOnMainThread();
if (self.backupExport != backupExport) {
return;
}
@ -331,6 +365,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)backupExportDidFail:(OWSBackupExport *)backupExport error:(NSError *)error
{
OWSAssertIsOnMainThread();
if (self.backupExport != backupExport) {
return;
}
@ -344,6 +380,88 @@ NS_ASSUME_NONNULL_BEGIN
[self ensureBackupExportState];
}
- (void)backupExportDidUpdate:(OWSBackupExport *)backupExport
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress
{
OWSAssertIsOnMainThread();
if (self.backupExport != backupExport) {
return;
}
DDLogInfo(@"%@ %s: %@, %@", self.logTag, __PRETTY_FUNCTION__, description, progress);
self.backupExportDescription = description;
self.backupExportProgress = progress;
[self postDidChangeNotification];
}
#pragma mark - OWSBackupImportDelegate
- (void)backupImportDidSucceed:(OWSBackupImport *)backupImport
{
OWSAssertIsOnMainThread();
if (self.backupImport != backupImport) {
return;
}
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
self.backupImport = nil;
_backupImportState = OWSBackupState_Succeeded;
[self postDidChangeNotification];
}
- (void)backupImportDidFail:(OWSBackupImport *)backupImport error:(NSError *)error
{
OWSAssertIsOnMainThread();
if (self.backupImport != backupImport) {
return;
}
DDLogInfo(@"%@ %s: %@", self.logTag, __PRETTY_FUNCTION__, error);
self.backupImport = nil;
_backupImportState = OWSBackupState_Failed;
[self postDidChangeNotification];
}
- (void)backupImportDidUpdate:(OWSBackupImport *)backupImport
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress
{
OWSAssertIsOnMainThread();
if (self.backupImport != backupImport) {
return;
}
DDLogInfo(@"%@ %s: %@, %@", self.logTag, __PRETTY_FUNCTION__, description, progress);
self.backupImportDescription = description;
self.backupImportProgress = progress;
[self postDidChangeNotification];
}
#pragma mark - Notifications
- (void)postDidChangeNotification
{
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationNameBackupStateDidChange
object:nil
userInfo:nil];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -20,6 +20,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)backupExportDidSucceed:(OWSBackupExport *)backupExport;
- (void)backupExportDidFail:(OWSBackupExport *)backupExport error:(NSError *)error;
- (void)backupExportDidUpdate:(OWSBackupExport *)backupExport
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress;
@end
//#pragma mark -

View File

@ -172,6 +172,8 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
self.backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
[self updateProgressWithDescription:nil progress:nil];
__weak OWSBackupExport *weakSelf = self;
[OWSBackupAPI checkCloudKitAccessWithCompletion:^(BOOL hasAccess) {
if (hasAccess) {
@ -184,6 +186,10 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
- (void)start
{
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_CONFIGURATION",
@"Indicates that the backup export is being configured.")
progress:nil];
__weak OWSBackupExport *weakSelf = self;
[self configureExport:^(BOOL success) {
if (!success) {
@ -196,6 +202,9 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
if (self.isComplete) {
return;
}
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_EXPORT",
@"Indicates that the backup export data is being exported.")
progress:nil];
if (![self exportDatabase]) {
[self failWithErrorDescription:
NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT",
@ -283,6 +292,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
self.attachmentFilePathMap = [NSMutableDictionary new];
[self.srcDBConnection readWithBlock:^(YapDatabaseReadTransaction *srcTransaction) {
[self.dstDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *dstTransaction) {
// Copy threads.
[srcTransaction
@ -405,6 +415,12 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
return;
}
CGFloat progress
= (self.databaseRecordMap.count / (CGFloat)(self.databaseRecordMap.count + self.databaseFilePaths.count));
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_UPLOAD",
@"Indicates that the backup export data is being uploaded.")
progress:@(progress)];
__weak OWSBackupExport *weakSelf = self;
if (self.databaseFilePaths.count > 0) {
@ -545,6 +561,10 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_CLEAN_UP",
@"Indicates that the cloud is being cleaned up.")
progress:nil];
// 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.
@ -573,7 +593,9 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
activeRecordNames.count,
obsoleteRecordNames.count);
[weakSelf deleteRecordsFromCloud:[obsoleteRecordNames.allObjects mutableCopy] completion:completion];
[weakSelf deleteRecordsFromCloud:[obsoleteRecordNames.allObjects mutableCopy]
deletedCount:0
completion:completion];
});
}
failure:^(NSError *error) {
@ -586,6 +608,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
}
- (void)deleteRecordsFromCloud:(NSMutableArray<NSString *> *)obsoleteRecordNames
deletedCount:(NSUInteger)deletedCount
completion:(OWSBackupExportCompletion)completion
{
OWSAssert(obsoleteRecordNames);
@ -599,6 +622,11 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
return;
}
CGFloat progress = (obsoleteRecordNames.count / (CGFloat)(obsoleteRecordNames.count + deletedCount));
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_CLEAN_UP",
@"Indicates that the cloud is being cleaned up.")
progress:@(progress)];
NSString *recordName = obsoleteRecordNames.lastObject;
[obsoleteRecordNames removeLastObject];
@ -607,14 +635,18 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
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];
[weakSelf deleteRecordsFromCloud:obsoleteRecordNames
deletedCount:deletedCount + 1
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];
[weakSelf deleteRecordsFromCloud:obsoleteRecordNames
deletedCount:deletedCount + 1
completion:completion];
});
}];
}
@ -662,6 +694,20 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
});
}
- (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress
{
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
dispatch_async(dispatch_get_main_queue(), ^{
if (self.isComplete) {
return;
}
[self.delegate backupExportDidUpdate:self description:description progress:progress];
});
}
#pragma mark - Encryption
+ (nullable NSString *)encryptAsTempFile:(NSString *)srcFilePath
exportDirPath:(NSString *)exportDirPath
delegate:(id<OWSBackupExportDelegate>)delegate

View File

@ -0,0 +1,46 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class OWSBackupImport;
@protocol OWSBackupImportDelegate <NSObject>
// TODO: This should eventually be the backup key stored in the Signal Service
// and retrieved with the backup PIN.
- (nullable NSData *)backupKey;
// Either backupImportDidSucceed:... or backupImportDidFail:... will
// be called exactly once on the main thread UNLESS:
//
// * The import was never started.
// * The import was cancelled.
- (void)backupImportDidSucceed:(OWSBackupImport *)backupImport;
- (void)backupImportDidFail:(OWSBackupImport *)backupImport error:(NSError *)error;
- (void)backupImportDidUpdate:(OWSBackupImport *)backupImport
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress;
@end
//#pragma mark -
@class OWSPrimaryStorage;
@interface OWSBackupImport : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithDelegate:(id<OWSBackupImportDelegate>)delegate
primaryStorage:(OWSPrimaryStorage *)primaryStorage;
- (void)startAsync;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,735 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBackupImport.h"
#import "Signal-Swift.h"
#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>
#import <SignalServiceKit/OWSError.h>
#import <SignalServiceKit/OWSFileSystem.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSMessage.h>
#import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/Threading.h>
#import <SignalServiceKit/YapDatabaseConnection+OWS.h>
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseCryptoUtils.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^OWSBackupImportBoolCompletion)(BOOL success);
typedef void (^OWSBackupImportCompletion)(NSError *_Nullable error);
@interface OWSBackupImport (Private)
+ (nullable NSString *)encryptAsTempFile:(NSString *)srcFilePath
importDirPath:(NSString *)importDirPath
delegate:(id<OWSBackupImportDelegate>)delegate;
@end
#pragma mark -
@interface OWSAttachmentImport : NSObject
@property (nonatomic, weak) id<OWSBackupImportDelegate> delegate;
@property (nonatomic) NSString *importDirPath;
@property (nonatomic) NSString *attachmentId;
@property (nonatomic) NSString *attachmentFilePath;
@property (nonatomic, nullable) NSString *tempFilePath;
@property (nonatomic, nullable) NSString *relativeFilePath;
@end
#pragma mark -
@implementation OWSAttachmentImport
- (void)dealloc
{
// Surface memory leaks by logging the deallocation.
DDLogVerbose(@"Dealloc: %@", self.class);
// Delete temporary file ASAP.
if (self.tempFilePath) {
[OWSFileSystem deleteFileIfExists:self.tempFilePath];
}
}
// On success, tempFilePath will be non-nil.
- (void)prepareForUpload
{
OWSAssert(self.importDirPath.length > 0);
OWSAssert(self.attachmentId.length > 0);
OWSAssert(self.attachmentFilePath.length > 0);
NSString *attachmentsDirPath = [TSAttachmentStream attachmentsFolder];
if (![self.attachmentFilePath hasPrefix:attachmentsDirPath]) {
DDLogError(@"%@ attachment has unexpected path.", self.logTag);
OWSFail(@"%@ attachment has unexpected path: %@", self.logTag, self.attachmentFilePath);
return;
}
NSString *relativeFilePath = [self.attachmentFilePath substringFromIndex:attachmentsDirPath.length];
NSString *pathSeparator = @"/";
if ([relativeFilePath hasPrefix:pathSeparator]) {
relativeFilePath = [relativeFilePath substringFromIndex:pathSeparator.length];
}
self.relativeFilePath = relativeFilePath;
NSString *_Nullable tempFilePath = [OWSBackupImport encryptAsTempFile:self.attachmentFilePath
importDirPath:self.importDirPath
delegate:self.delegate];
if (!tempFilePath) {
DDLogError(@"%@ attachment could not be encrypted.", self.logTag);
OWSFail(@"%@ attachment could not be encrypted: %@", self.logTag, self.attachmentFilePath);
return;
}
self.tempFilePath = tempFilePath;
}
@end
#pragma mark -
@interface OWSBackupImport () <SSZipArchiveDelegate>
@property (nonatomic, weak) id<OWSBackupImportDelegate> delegate;
@property (nonatomic, nullable) YapDatabaseConnection *srcDBConnection;
@property (nonatomic, nullable) YapDatabaseConnection *dstDBConnection;
// Indicates that the backup succeeded, failed or was cancelled.
@property (atomic) BOOL isComplete;
@property (nonatomic, nullable) OWSBackupStorage *backupStorage;
@property (nonatomic, nullable) NSData *databaseKeySpec;
@property (nonatomic, nullable) OWSBackgroundTask *backgroundTask;
@property (nonatomic) NSMutableArray<NSString *> *databaseFilePaths;
// A map of "record name"-to-"file name".
@property (nonatomic) NSMutableDictionary<NSString *, NSString *> *databaseRecordMap;
// A map of "attachment id"-to-"local file path".
@property (nonatomic) NSMutableDictionary<NSString *, NSString *> *attachmentFilePathMap;
// A map of "record name"-to-"file relative path".
@property (nonatomic) NSMutableDictionary<NSString *, NSString *> *attachmentRecordMap;
@property (nonatomic, nullable) NSString *manifestFilePath;
@property (nonatomic, nullable) NSString *manifestRecordName;
@property (nonatomic) NSString *importDirPath;
@end
#pragma mark -
@implementation OWSBackupImport
- (instancetype)initWithDelegate:(id<OWSBackupImportDelegate>)delegate
primaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
OWSAssert(primaryStorage);
OWSAssert([OWSStorage isStorageReady]);
self.delegate = delegate;
_srcDBConnection = primaryStorage.newDatabaseConnection;
return self;
}
- (void)dealloc
{
// Surface memory leaks by logging the deallocation.
DDLogVerbose(@"Dealloc: %@", self.class);
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (self.importDirPath) {
[OWSFileSystem deleteFileIfExists:self.importDirPath];
}
}
- (void)startAsync
{
OWSAssertIsOnMainThread();
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
self.backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
[self updateProgressWithDescription:nil progress:nil];
__weak OWSBackupImport *weakSelf = self;
[OWSBackupAPI checkCloudKitAccessWithCompletion:^(BOOL hasAccess) {
if (hasAccess) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf start];
});
}
}];
}
- (void)start
{
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_CONFIGURATION",
@"Indicates that the backup import is being configured.")
progress:nil];
__weak OWSBackupImport *weakSelf = self;
[self configureImport:^(BOOL success) {
if (!success) {
[self failWithErrorDescription:
NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT",
@"Error indicating the a backup import could not import the user's data.")];
return;
}
if (self.isComplete) {
return;
}
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_EXPORT",
@"Indicates that the backup import data is being imported.")
progress:nil];
if (![self importDatabase]) {
[self failWithErrorDescription:
NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT",
@"Error indicating the a backup import could not import the user's data.")];
return;
}
if (self.isComplete) {
return;
}
[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)configureImport:(OWSBackupImportBoolCompletion)completion
{
OWSAssert(completion);
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
NSString *temporaryDirectory = NSTemporaryDirectory();
self.importDirPath = [temporaryDirectory stringByAppendingString:[NSUUID UUID].UUIDString];
NSString *importDatabaseDirPath = [self.importDirPath stringByAppendingPathComponent:@"Database"];
self.databaseKeySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
if (![OWSFileSystem ensureDirectoryExists:self.importDirPath]) {
OWSProdLogAndFail(@"%@ Could not create importDirPath.", self.logTag);
return completion(NO);
}
if (![OWSFileSystem ensureDirectoryExists:importDatabaseDirPath]) {
OWSProdLogAndFail(@"%@ Could not create importDatabaseDirPath.", self.logTag);
return completion(NO);
}
if (!self.databaseKeySpec) {
OWSProdLogAndFail(@"%@ Could not create databaseKeySpec.", self.logTag);
return completion(NO);
}
__weak OWSBackupImport *weakSelf = self;
BackupStorageKeySpecBlock keySpecBlock = ^{
return weakSelf.databaseKeySpec;
};
self.backupStorage =
[[OWSBackupStorage alloc] initBackupStorageWithDatabaseDirPath:importDatabaseDirPath keySpecBlock:keySpecBlock];
if (!self.backupStorage) {
OWSProdLogAndFail(@"%@ Could not create backupStorage.", self.logTag);
return completion(NO);
}
_dstDBConnection = self.backupStorage.newDatabaseConnection;
if (!self.dstDBConnection) {
OWSProdLogAndFail(@"%@ Could not create dstDBConnection.", self.logTag);
return completion(NO);
}
// TODO: Do we really need to run these registrations on the main thread?
dispatch_async(dispatch_get_main_queue(), ^{
[self.backupStorage runSyncRegistrations];
[self.backupStorage runAsyncRegistrationsWithCompletion:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(YES);
});
}];
});
}
- (BOOL)importDatabase
{
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
__block unsigned long long copiedThreads = 0;
__block unsigned long long copiedInteractions = 0;
__block unsigned long long copiedEntities = 0;
__block unsigned long long copiedAttachments = 0;
self.attachmentFilePathMap = [NSMutableDictionary new];
[self.srcDBConnection readWithBlock:^(YapDatabaseReadTransaction *srcTransaction) {
[self.dstDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *dstTransaction) {
// Copy threads.
[srcTransaction
enumerateKeysAndObjectsInCollection:[TSThread collection]
usingBlock:^(NSString *key, id object, BOOL *stop) {
if (self.isComplete) {
*stop = YES;
return;
}
if (![object isKindOfClass:[TSThread class]]) {
OWSProdLogAndFail(
@"%@ unexpected class: %@", self.logTag, [object class]);
return;
}
TSThread *thread = object;
[thread saveWithTransaction:dstTransaction];
copiedThreads++;
copiedEntities++;
}];
// Copy interactions.
[srcTransaction
enumerateKeysAndObjectsInCollection:[TSInteraction collection]
usingBlock:^(NSString *key, id object, BOOL *stop) {
if (self.isComplete) {
*stop = YES;
return;
}
if (![object isKindOfClass:[TSInteraction class]]) {
OWSProdLogAndFail(
@"%@ unexpected class: %@", self.logTag, [object class]);
return;
}
// Ignore disappearing messages.
if ([object isKindOfClass:[TSMessage class]]) {
TSMessage *message = object;
if (message.isExpiringMessage) {
return;
}
}
TSInteraction *interaction = object;
// Ignore dynamic interactions.
if (interaction.isDynamicInteraction) {
return;
}
[interaction saveWithTransaction:dstTransaction];
copiedInteractions++;
copiedEntities++;
}];
// Copy attachments.
[srcTransaction
enumerateKeysAndObjectsInCollection:[TSAttachmentStream collection]
usingBlock:^(NSString *key, id object, BOOL *stop) {
if (self.isComplete) {
*stop = YES;
return;
}
if (![object isKindOfClass:[TSAttachment class]]) {
OWSProdLogAndFail(
@"%@ unexpected class: %@", self.logTag, [object class]);
return;
}
if ([object isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *attachmentStream = object;
NSString *_Nullable filePath = attachmentStream.filePath;
if (filePath) {
OWSAssert(attachmentStream.uniqueId.length > 0);
self.attachmentFilePathMap[attachmentStream.uniqueId] = filePath;
}
}
TSAttachment *attachment = object;
[attachment saveWithTransaction:dstTransaction];
copiedAttachments++;
copiedEntities++;
}];
}];
}];
// TODO: Should we do a database checkpoint?
DDLogInfo(@"%@ copiedThreads: %llu", self.logTag, copiedThreads);
DDLogInfo(@"%@ copiedMessages: %llu", self.logTag, copiedInteractions);
DDLogInfo(@"%@ copiedEntities: %llu", self.logTag, copiedEntities);
DDLogInfo(@"%@ copiedAttachments: %llu", self.logTag, copiedAttachments);
[self.backupStorage logFileSizes];
// Capture the list of files to save.
self.databaseFilePaths = [@[
self.backupStorage.databaseFilePath,
self.backupStorage.databaseFilePath_WAL,
self.backupStorage.databaseFilePath_SHM,
] mutableCopy];
// Close the database.
self.dstDBConnection = nil;
self.backupStorage = nil;
return YES;
}
- (void)saveToCloud:(OWSBackupImportCompletion)completion
{
OWSAssert(completion);
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
self.databaseRecordMap = [NSMutableDictionary new];
self.attachmentRecordMap = [NSMutableDictionary new];
[self saveNextFileToCloud:completion];
}
- (void)saveNextFileToCloud:(OWSBackupImportCompletion)completion
{
OWSAssert(completion);
if (self.isComplete) {
return;
}
CGFloat progress
= (self.databaseRecordMap.count / (CGFloat)(self.databaseRecordMap.count + self.databaseFilePaths.count));
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_UPLOAD",
@"Indicates that the backup import data is being uploaded.")
progress:@(progress)];
__weak OWSBackupImport *weakSelf = self;
if (self.databaseFilePaths.count > 0) {
NSString *filePath = self.databaseFilePaths.lastObject;
[self.databaseFilePaths removeLastObject];
// Database files are encrypted and can be safely stored unencrypted in the cloud.
// TODO: Security review.
[OWSBackupAPI saveEphemeralDatabaseFileToCloudWithFileUrl:[NSURL fileURLWithPath:filePath]
success:^(NSString *recordName) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSBackupImport *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
strongSelf.databaseRecordMap[recordName] = [filePath lastPathComponent];
[strongSelf saveNextFileToCloud:completion];
});
}
failure:^(NSError *error) {
// Database files are critical so any error uploading them is unrecoverable.
completion(error);
}];
return;
}
if (self.attachmentFilePathMap.count > 0) {
NSString *attachmentId = self.attachmentFilePathMap.allKeys.lastObject;
NSString *attachmentFilePath = self.attachmentFilePathMap[attachmentId];
[self.attachmentFilePathMap removeObjectForKey:attachmentId];
// OWSAttachmentImport is used to lazily write an encrypted copy of the
// attachment to disk.
OWSAttachmentImport *attachmentImport = [OWSAttachmentImport new];
attachmentImport.delegate = self.delegate;
attachmentImport.importDirPath = self.importDirPath;
attachmentImport.attachmentId = attachmentId;
attachmentImport.attachmentFilePath = attachmentFilePath;
[OWSBackupAPI savePersistentFileOnceToCloudWithFileId:attachmentId
fileUrlBlock:^{
[attachmentImport prepareForUpload];
if (attachmentImport.tempFilePath.length < 1) {
DDLogError(@"%@ attachment import missing temp file path", self.logTag);
return (NSURL *)nil;
}
if (attachmentImport.relativeFilePath.length < 1) {
DDLogError(@"%@ attachment import missing relative file path", self.logTag);
return (NSURL *)nil;
}
return [NSURL fileURLWithPath:attachmentImport.tempFilePath];
}
success:^(NSString *recordName) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSBackupImport *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
strongSelf.attachmentRecordMap[recordName] = attachmentImport.relativeFilePath;
[strongSelf saveNextFileToCloud: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), ^{
// Attachment files are non-critical so any error uploading them is recoverable.
[weakSelf saveNextFileToCloud:completion];
});
}];
return;
}
if (!self.manifestFilePath) {
if (![self writeManifestFile]) {
completion(OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed,
NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT",
@"Error indicating the a backup import could not import the user's data.")));
return;
}
OWSAssert(self.manifestFilePath);
[OWSBackupAPI upsertManifestFileToCloudWithFileUrl:[NSURL fileURLWithPath:self.manifestFilePath]
success:^(NSString *recordName) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSBackupImport *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
strongSelf.manifestRecordName = recordName;
[strongSelf saveNextFileToCloud:completion];
});
}
failure:^(NSError *error) {
// The manifest file is critical so any error uploading them is unrecoverable.
completion(error);
}];
return;
}
// All files have been saved to the cloud.
completion(nil);
}
- (BOOL)writeManifestFile
{
OWSAssert(self.databaseRecordMap.count > 0);
OWSAssert(self.attachmentRecordMap);
OWSAssert(self.importDirPath.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 =
[NSJSONSerialization dataWithJSONObject:json options:NSJSONWritingPrettyPrinted error:&error];
if (!jsonData || error) {
OWSProdLogAndFail(@"%@ error encoding manifest file: %@", self.logTag, error);
return NO;
}
// TODO: Encrypt the manifest.
self.manifestFilePath = [self.importDirPath stringByAppendingPathComponent:@"manifest.json"];
if (![jsonData writeToFile:self.manifestFilePath atomically:YES]) {
OWSProdLogAndFail(@"%@ error writing manifest file: %@", self.logTag, error);
return NO;
}
return YES;
}
- (void)cleanUpCloud:(OWSBackupImportCompletion)completion
{
OWSAssert(completion);
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_CLEAN_UP",
@"Indicates that the cloud is being cleaned up.")
progress:nil];
// Now that our backup import has successfully completed,
// we try to clean up the cloud. We can safely delete any
// records not involved in this backup import.
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 OWSBackupImport *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]
deletedCount:0
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
deletedCount:(NSUInteger)deletedCount
completion:(OWSBackupImportCompletion)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;
}
CGFloat progress = (obsoleteRecordNames.count / (CGFloat)(obsoleteRecordNames.count + deletedCount));
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_CLEAN_UP",
@"Indicates that the cloud is being cleaned up.")
progress:@(progress)];
NSString *recordName = obsoleteRecordNames.lastObject;
[obsoleteRecordNames removeLastObject];
__weak OWSBackupImport *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
deletedCount:deletedCount + 1
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
deletedCount:deletedCount + 1
completion:completion];
});
}];
}
#pragma mark -
- (void)cancel
{
OWSAssertIsOnMainThread();
self.isComplete = YES;
}
- (void)succeed
{
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
dispatch_async(dispatch_get_main_queue(), ^{
if (self.isComplete) {
return;
}
self.isComplete = YES;
[self.delegate backupImportDidSucceed:self];
});
// TODO:
}
- (void)failWithErrorDescription:(NSString *)description
{
[self failWithError:OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed, description)];
}
- (void)failWithError:(NSError *)error
{
OWSProdLogAndFail(@"%@ %s %@", self.logTag, __PRETTY_FUNCTION__, error);
// TODO:
dispatch_async(dispatch_get_main_queue(), ^{
if (self.isComplete) {
return;
}
self.isComplete = YES;
[self.delegate backupImportDidFail:self error:error];
});
}
- (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress
{
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
dispatch_async(dispatch_get_main_queue(), ^{
if (self.isComplete) {
return;
}
[self.delegate backupImportDidUpdate:self description:description progress:progress];
});
}
#pragma mark - Encryption
+ (nullable NSString *)encryptAsTempFile:(NSString *)srcFilePath
importDirPath:(NSString *)importDirPath
delegate:(id<OWSBackupImportDelegate>)delegate
{
OWSAssert(srcFilePath.length > 0);
OWSAssert(importDirPath.length > 0);
OWSAssert(delegate);
// TODO: Encrypt the file using self.delegate.backupKey;
NSData *_Nullable backupKey = [delegate backupKey];
OWSAssert(backupKey);
NSString *dstFilePath = [importDirPath stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;
BOOL success = [fileManager copyItemAtPath:srcFilePath toPath:dstFilePath error:&error];
if (!success || error) {
OWSProdLogAndFail(@"%@ error writing encrypted file: %@", self.logTag, error);
return nil;
}
return dstFilePath;
}
@end
NS_ASSUME_NONNULL_END