Implement backup import logic.

This commit is contained in:
Matthew Chen 2018-03-08 15:38:42 -03:00 committed by Matthew Chen
parent 5035cb040e
commit 30065493a3
12 changed files with 1345 additions and 1122 deletions

View File

@ -34,11 +34,12 @@
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8A1204DAC8D007AEB0F /* OWSAddToContactViewController.m */; };
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8A2204DAC8D007AEB0F /* FingerprintViewController.m */; };
340FC8BD204DAC8D007AEB0F /* ShowGroupMembersViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8A6204DAC8D007AEB0F /* ShowGroupMembersViewController.m */; };
340FC8C0204DB7D2007AEB0F /* OWSBackupExport.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8BF204DB7D2007AEB0F /* OWSBackupExport.m */; };
340FC8C0204DB7D2007AEB0F /* OWSBackupExportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8BF204DB7D2007AEB0F /* OWSBackupExportJob.m */; };
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 */; };
340FC8CA20517B84007AEB0F /* OWSBackupImportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8C820517B84007AEB0F /* OWSBackupImportJob.m */; };
340FC8CD20518C77007AEB0F /* OWSBackupJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8CC20518C76007AEB0F /* OWSBackupJob.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 */; };
@ -572,14 +573,16 @@
340FC8A4204DAC8D007AEB0F /* AddToGroupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddToGroupViewController.h; sourceTree = "<group>"; };
340FC8A5204DAC8D007AEB0F /* FingerprintViewScanController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FingerprintViewScanController.h; sourceTree = "<group>"; };
340FC8A6204DAC8D007AEB0F /* ShowGroupMembersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShowGroupMembersViewController.m; sourceTree = "<group>"; };
340FC8BE204DB7D1007AEB0F /* OWSBackupExport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupExport.h; sourceTree = "<group>"; };
340FC8BF204DB7D2007AEB0F /* OWSBackupExport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupExport.m; sourceTree = "<group>"; };
340FC8BE204DB7D1007AEB0F /* OWSBackupExportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupExportJob.h; sourceTree = "<group>"; };
340FC8BF204DB7D2007AEB0F /* OWSBackupExportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupExportJob.m; sourceTree = "<group>"; };
340FC8C1204DDF66007AEB0F /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
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>"; };
340FC8C820517B84007AEB0F /* OWSBackupImportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupImportJob.m; sourceTree = "<group>"; };
340FC8C920517B84007AEB0F /* OWSBackupImportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImportJob.h; sourceTree = "<group>"; };
340FC8CB20518C76007AEB0F /* OWSBackupJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupJob.h; sourceTree = "<group>"; };
340FC8CC20518C76007AEB0F /* OWSBackupJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupJob.m; 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>"; };
@ -1937,10 +1940,12 @@
34A9105E1FFEB113000C4745 /* OWSBackup.h */,
34A9105F1FFEB114000C4745 /* OWSBackup.m */,
340FC8C6204DE64D007AEB0F /* OWSBackupAPI.swift */,
340FC8BE204DB7D1007AEB0F /* OWSBackupExport.h */,
340FC8BF204DB7D2007AEB0F /* OWSBackupExport.m */,
340FC8C920517B84007AEB0F /* OWSBackupImport.h */,
340FC8C820517B84007AEB0F /* OWSBackupImport.m */,
340FC8BE204DB7D1007AEB0F /* OWSBackupExportJob.h */,
340FC8BF204DB7D2007AEB0F /* OWSBackupExportJob.m */,
340FC8C920517B84007AEB0F /* OWSBackupImportJob.h */,
340FC8C820517B84007AEB0F /* OWSBackupImportJob.m */,
340FC8CB20518C76007AEB0F /* OWSBackupJob.h */,
340FC8CC20518C76007AEB0F /* OWSBackupJob.m */,
4579431C1E7C8CE9008ED0C0 /* Pastelog.h */,
4579431D1E7C8CE9008ED0C0 /* Pastelog.m */,
450DF2041E0D74AC003D14BE /* Platform.swift */,
@ -3117,6 +3122,7 @@
34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */,
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */,
34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */,
340FC8CD20518C77007AEB0F /* OWSBackupJob.m in Sources */,
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */,
451686AB1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift in Sources */,
45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
@ -3170,12 +3176,12 @@
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */,
340FC8C0204DB7D2007AEB0F /* OWSBackupExport.m in Sources */,
340FC8C0204DB7D2007AEB0F /* OWSBackupExportJob.m in Sources */,
340FC8A7204DAC8D007AEB0F /* RegistrationViewController.m in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */,
345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */,
340FC8CA20517B84007AEB0F /* OWSBackupImport.m in Sources */,
340FC8CA20517B84007AEB0F /* OWSBackupImportJob.m in Sources */,
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,

View File

@ -4,8 +4,8 @@
#import "OWSBackup.h"
#import "NSNotificationCenter+OWS.h"
#import "OWSBackupExport.h"
#import "OWSBackupImport.h"
#import "OWSBackupExportJob.h"
#import "OWSBackupImportJob.h"
#import "Signal-Swift.h"
#import <Curve25519Kit/Randomness.h>
#import <SignalServiceKit/AppContext.h>
@ -26,15 +26,15 @@ NSString *const OWSBackup_LastExportFailureDateKey = @"OWSBackup_LastExportFailu
NS_ASSUME_NONNULL_BEGIN
// TODO: Observe Reachability.
@interface OWSBackup () <OWSBackupExportDelegate, OWSBackupImportDelegate>
@interface OWSBackup () <OWSBackupJobDelegate>
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
// This property should only be accessed on the main thread.
@property (nonatomic, nullable) OWSBackupExport *backupExport;
@property (nonatomic, nullable) OWSBackupExportJob *backupExport;
// This property should only be accessed on the main thread.
@property (nonatomic, nullable) OWSBackupImport *backupImport;
@property (nonatomic, nullable) OWSBackupImportJob *backupImport;
@property (nonatomic, nullable) NSString *backupExportDescription;
@property (nonatomic, nullable) NSNumber *backupExportProgress;
@ -252,7 +252,7 @@ NS_ASSUME_NONNULL_BEGIN
self.backupExport = nil;
} else if (self.shouldHaveBackupExport && !self.backupExport) {
self.backupExport =
[[OWSBackupExport alloc] initWithDelegate:self primaryStorage:[OWSPrimaryStorage sharedManager]];
[[OWSBackupExportJob alloc] initWithDelegate:self primaryStorage:[OWSPrimaryStorage sharedManager]];
[self.backupExport startAsync];
}
@ -318,7 +318,7 @@ NS_ASSUME_NONNULL_BEGIN
_backupImportState = OWSBackupState_InProgress;
self.backupImport =
[[OWSBackupImport alloc] initWithDelegate:self primaryStorage:[OWSPrimaryStorage sharedManager]];
[[OWSBackupImportJob alloc] initWithDelegate:self primaryStorage:[OWSPrimaryStorage sharedManager]];
[self.backupImport startAsync];
[self postDidChangeNotification];
@ -350,7 +350,7 @@ NS_ASSUME_NONNULL_BEGIN
[self ensureBackupExportState];
}
#pragma mark - OWSBackupExportDelegate
#pragma mark - OWSBackupJobDelegate
// We use a delegate method to avoid storing this key in memory.
- (nullable NSData *)backupKey
@ -358,117 +358,78 @@ NS_ASSUME_NONNULL_BEGIN
return self.backupPrivateKey;
}
- (void)backupExportDidSucceed:(OWSBackupExport *)backupExport
- (void)backupJobDidSucceed:(OWSBackupJob *)backupJob
{
OWSAssertIsOnMainThread();
if (self.backupExport != backupExport) {
return;
}
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
self.backupExport = nil;
if (self.backupImport == backupJob) {
self.backupImport = nil;
[self setLastExportSuccessDate:[NSDate new]];
_backupImportState = OWSBackupState_Succeeded;
} else if (self.backupExport == backupJob) {
self.backupExport = nil;
[self ensureBackupExportState];
}
[self setLastExportSuccessDate:[NSDate new]];
- (void)backupExportDidFail:(OWSBackupExport *)backupExport error:(NSError *)error
{
OWSAssertIsOnMainThread();
if (self.backupExport != backupExport) {
[self ensureBackupExportState];
} else {
return;
}
DDLogInfo(@"%@ %s: %@", self.logTag, __PRETTY_FUNCTION__, error);
self.backupExport = nil;
[self setLastExportFailureDate:[NSDate new]];
[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);
BOOL didChange = !([NSObject isNullableObject:self.backupExportDescription equalTo:description] &&
[NSObject isNullableObject:self.backupExportProgress equalTo:progress]);
self.backupExportDescription = description;
self.backupExportProgress = progress;
if (didChange) {
[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
- (void)backupJobDidFail:(OWSBackupJob *)backupJob error:(NSError *)error
{
OWSAssertIsOnMainThread();
if (self.backupImport != backupImport) {
return;
}
DDLogInfo(@"%@ %s: %@", self.logTag, __PRETTY_FUNCTION__, error);
self.backupImport = nil;
if (self.backupImport == backupJob) {
self.backupImport = nil;
_backupImportState = OWSBackupState_Failed;
_backupImportState = OWSBackupState_Failed;
} else if (self.backupExport == backupJob) {
self.backupExport = nil;
[self setLastExportFailureDate:[NSDate new]];
[self ensureBackupExportState];
} else {
return;
}
[self postDidChangeNotification];
}
- (void)backupImportDidUpdate:(OWSBackupImport *)backupImport
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress
- (void)backupJobDidUpdate:(OWSBackupJob *)backupJob
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress
{
OWSAssertIsOnMainThread();
if (self.backupImport != backupImport) {
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
// TODO: Should we consolidate this state?
BOOL didChange;
if (self.backupImport == backupJob) {
didChange = !([NSObject isNullableObject:self.backupImportDescription equalTo:description] &&
[NSObject isNullableObject:self.backupImportProgress equalTo:progress]);
self.backupImportDescription = description;
self.backupImportProgress = progress;
} else if (self.backupExport == backupJob) {
didChange = !([NSObject isNullableObject:self.backupExportDescription equalTo:description] &&
[NSObject isNullableObject:self.backupExportProgress equalTo:progress]);
self.backupExportDescription = description;
self.backupExportProgress = progress;
} else {
return;
}
DDLogInfo(@"%@ %s: %@, %@", self.logTag, __PRETTY_FUNCTION__, description, progress);
BOOL didChange = !([NSObject isNullableObject:self.backupImportDescription equalTo:description] &&
[NSObject isNullableObject:self.backupImportProgress equalTo:progress]);
self.backupImportDescription = description;
self.backupImportProgress = progress;
if (didChange) {
[self postDidChangeNotification];
}

View File

@ -324,6 +324,94 @@ import CloudKit
privateDatabase.add(queryOperation)
}
@objc
public class func downloadManifestFromCloud(
success: @escaping (Data) -> Swift.Void,
failure: @escaping (Error) -> Swift.Void) {
downloadDataFromCloud(recordName: manifestRecordName,
success: success,
failure: failure)
}
@objc
public class func downloadDataFromCloud(recordName: String,
success: @escaping (Data) -> Swift.Void,
failure: @escaping (Error) -> Swift.Void) {
downloadFromCloud(recordName: recordName,
success: { (asset) in
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: asset.fileURL)
success(data)
} catch {
Logger.error("\(self.logTag) couldn't copy asset file.")
failure(OWSErrorWithCodeDescription(.exportBackupError,
NSLocalizedString("BACKUP_IMPORT_ERROR_DOWNLOAD_FILE_FROM_CLOUD_FAILED",
comment: "Error indicating the a backup import failed to download a file from the cloud.")))
}
}
},
failure: failure)
}
@objc
public class func downloadFileFromCloud(recordName: String,
toFileUrl: URL,
success: @escaping (Swift.Void) -> Swift.Void,
failure: @escaping (Error) -> Swift.Void) {
downloadFromCloud(recordName: recordName,
success: { (asset) in
DispatchQueue.global().async {
do {
let fileManager = FileManager.default
try fileManager.copyItem(at: asset.fileURL, to: toFileUrl)
success()
} catch {
Logger.error("\(self.logTag) couldn't copy asset file.")
failure(OWSErrorWithCodeDescription(.exportBackupError,
NSLocalizedString("BACKUP_IMPORT_ERROR_DOWNLOAD_FILE_FROM_CLOUD_FAILED",
comment: "Error indicating the a backup import failed to download a file from the cloud.")))
}
}
},
failure: failure)
}
private class func downloadFromCloud(recordName: String,
success: @escaping (CKAsset) -> Swift.Void,
failure: @escaping (Error) -> Swift.Void) {
let recordId = CKRecordID(recordName: recordName)
let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ])
// Download all keys for this record.
fetchOperation.perRecordCompletionBlock = { (record, recordId, error) in
if let error = error {
failure(error)
return
}
guard let record = record else {
Logger.error("\(self.logTag) missing fetching record.")
failure(OWSErrorWithCodeDescription(.exportBackupError,
NSLocalizedString("BACKUP_IMPORT_ERROR_DOWNLOAD_FILE_FROM_CLOUD_FAILED",
comment: "Error indicating the a backup import failed to download a file from the cloud.")))
return
}
guard let asset = record[payloadKey] as? CKAsset else {
Logger.error("\(self.logTag) record missing payload.")
failure(OWSErrorWithCodeDescription(.exportBackupError,
NSLocalizedString("BACKUP_IMPORT_ERROR_DOWNLOAD_FILE_FROM_CLOUD_FAILED",
comment: "Error indicating the a backup import failed to download a file from the cloud.")))
return
}
success(asset)
}
let myContainer = CKContainer.default()
let privateDatabase = myContainer.privateCloudDatabase
privateDatabase.add(fetchOperation)
}
@objc
public class func checkCloudKitAccess(completion: @escaping (Bool) -> Swift.Void) {
CKContainer.default().accountStatus(completionHandler: { (accountStatus, error) in

View File

@ -1,46 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class OWSBackupExport;
@protocol OWSBackupExportDelegate <NSObject>
// TODO: This should eventually be the backup key stored in the Signal Service
// and retrieved with the backup PIN.
- (nullable NSData *)backupKey;
// Either backupExportDidSucceed:... or backupExportDidFail:... will
// be called exactly once on the main thread UNLESS:
//
// * The export was never started.
// * The export was cancelled.
- (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 -
@class OWSPrimaryStorage;
@interface OWSBackupExport : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithDelegate:(id<OWSBackupExportDelegate>)delegate
primaryStorage:(OWSPrimaryStorage *)primaryStorage;
- (void)startAsync;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,17 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBackupJob.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSBackupExportJob : OWSBackupJob
- (void)startAsync;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@ -2,10 +2,9 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBackupExport.h"
#import "OWSBackupExportJob.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>
@ -19,27 +18,15 @@
#import <SignalServiceKit/Threading.h>
#import <SignalServiceKit/YapDatabaseConnection+OWS.h>
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseCryptoUtils.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^OWSBackupExportBoolCompletion)(BOOL success);
typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
@interface OWSBackupExport (Private)
+ (nullable NSString *)encryptAsTempFile:(NSString *)srcFilePath
exportDirPath:(NSString *)exportDirPath
delegate:(id<OWSBackupExportDelegate>)delegate;
@end
#pragma mark -
NSString *const kOWSBackup_ExportDatabaseKeySpec = @"kOWSBackup_ExportDatabaseKeySpec";
@interface OWSAttachmentExport : NSObject
@property (nonatomic, weak) id<OWSBackupExportDelegate> delegate;
@property (nonatomic) NSString *exportDirPath;
@property (nonatomic, weak) id<OWSBackupJobDelegate> delegate;
@property (nonatomic) NSString *jobTempDirPath;
@property (nonatomic) NSString *attachmentId;
@property (nonatomic) NSString *attachmentFilePath;
@property (nonatomic, nullable) NSString *tempFilePath;
@ -65,7 +52,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
// On success, tempFilePath will be non-nil.
- (void)prepareForUpload
{
OWSAssert(self.exportDirPath.length > 0);
OWSAssert(self.jobTempDirPath.length > 0);
OWSAssert(self.attachmentId.length > 0);
OWSAssert(self.attachmentFilePath.length > 0);
@ -82,9 +69,9 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
}
self.relativeFilePath = relativeFilePath;
NSString *_Nullable tempFilePath = [OWSBackupExport encryptAsTempFile:self.attachmentFilePath
exportDirPath:self.exportDirPath
delegate:self.delegate];
NSString *_Nullable tempFilePath = [OWSBackupExportJob encryptFileAsTempFile:self.attachmentFilePath
jobTempDirPath:self.jobTempDirPath
delegate:self.delegate];
if (!tempFilePath) {
DDLogError(@"%@ attachment could not be encrypted.", self.logTag);
OWSFail(@"%@ attachment could not be encrypted: %@", self.logTag, self.attachmentFilePath);
@ -97,21 +84,10 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
#pragma mark -
@interface OWSBackupExport () <SSZipArchiveDelegate>
@property (nonatomic, weak) id<OWSBackupExportDelegate> delegate;
@property (nonatomic, nullable) YapDatabaseConnection *srcDBConnection;
@property (nonatomic, nullable) YapDatabaseConnection *dstDBConnection;
// Indicates that the backup succeeded, failed or was cancelled.
@property (atomic) BOOL isComplete;
@interface OWSBackupExportJob () <SSZipArchiveDelegate>
@property (nonatomic, nullable) OWSBackupStorage *backupStorage;
@property (nonatomic, nullable) NSData *databaseKeySpec;
@property (nonatomic, nullable) OWSBackgroundTask *backgroundTask;
@property (nonatomic) NSMutableArray<NSString *> *databaseFilePaths;
@ -126,43 +102,11 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
@property (nonatomic, nullable) NSString *manifestFilePath;
@property (nonatomic, nullable) NSString *manifestRecordName;
@property (nonatomic) NSString *exportDirPath;
@end
#pragma mark -
@implementation OWSBackupExport
- (instancetype)initWithDelegate:(id<OWSBackupExportDelegate>)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.exportDirPath) {
[OWSFileSystem deleteFileIfExists:self.exportDirPath];
}
}
@implementation OWSBackupExportJob
- (void)startAsync
{
@ -174,7 +118,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
[self updateProgressWithDescription:nil progress:nil];
__weak OWSBackupExport *weakSelf = self;
__weak OWSBackupExportJob *weakSelf = self;
[OWSBackupAPI checkCloudKitAccessWithCompletion:^(BOOL hasAccess) {
if (hasAccess) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@ -190,7 +134,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
@"Indicates that the backup export is being configured.")
progress:nil];
__weak OWSBackupExport *weakSelf = self;
__weak OWSBackupExportJob *weakSelf = self;
[self configureExport:^(BOOL success) {
if (!success) {
[self failWithErrorDescription:
@ -230,32 +174,35 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
}];
}
- (void)configureExport:(OWSBackupExportBoolCompletion)completion
- (void)configureExport:(OWSBackupJobBoolCompletion)completion
{
OWSAssert(completion);
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
NSString *temporaryDirectory = NSTemporaryDirectory();
self.exportDirPath = [temporaryDirectory stringByAppendingString:[NSUUID UUID].UUIDString];
NSString *exportDatabaseDirPath = [self.exportDirPath stringByAppendingPathComponent:@"Database"];
self.databaseKeySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
if (![OWSFileSystem ensureDirectoryExists:self.exportDirPath]) {
OWSProdLogAndFail(@"%@ Could not create exportDirPath.", self.logTag);
if (![self ensureJobTempDir]) {
OWSProdLogAndFail(@"%@ Could not create jobTempDirPath.", self.logTag);
return completion(NO);
}
if (![OWSBackupJob generateRandomDatabaseKeySpecWithKeychainKey:kOWSBackup_ExportDatabaseKeySpec]) {
OWSProdLogAndFail(@"%@ Could not generate database key spec for export.", self.logTag);
return completion(NO);
}
NSString *exportDatabaseDirPath = [self.jobTempDirPath stringByAppendingPathComponent:@"Database"];
if (![OWSFileSystem ensureDirectoryExists:exportDatabaseDirPath]) {
OWSProdLogAndFail(@"%@ Could not create exportDatabaseDirPath.", self.logTag);
return completion(NO);
}
if (!self.databaseKeySpec) {
OWSProdLogAndFail(@"%@ Could not create databaseKeySpec.", self.logTag);
return completion(NO);
}
__weak OWSBackupExport *weakSelf = self;
BackupStorageKeySpecBlock keySpecBlock = ^{
return weakSelf.databaseKeySpec;
NSData *_Nullable databaseKeySpec =
[OWSBackupJob loadDatabaseKeySpecWithKeychainKey:kOWSBackup_ExportDatabaseKeySpec];
if (!databaseKeySpec) {
OWSProdLogAndFail(@"%@ Could not load database keyspec for export.", self.logTag);
}
return databaseKeySpec;
};
self.backupStorage =
[[OWSBackupStorage alloc] initBackupStorageWithDatabaseDirPath:exportDatabaseDirPath keySpecBlock:keySpecBlock];
@ -263,11 +210,6 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
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(), ^{
@ -284,6 +226,17 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
{
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
YapDatabaseConnection *_Nullable tempDBConnection = self.backupStorage.newDatabaseConnection;
if (!tempDBConnection) {
OWSProdLogAndFail(@"%@ Could not create tempDBConnection.", self.logTag);
return NO;
}
YapDatabaseConnection *_Nullable primaryDBConnection = self.primaryStorage.newDatabaseConnection;
if (!primaryDBConnection) {
OWSProdLogAndFail(@"%@ Could not create primaryDBConnection.", self.logTag);
return NO;
}
__block unsigned long long copiedThreads = 0;
__block unsigned long long copiedInteractions = 0;
__block unsigned long long copiedEntities = 0;
@ -291,9 +244,8 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
self.attachmentFilePathMap = [NSMutableDictionary new];
[self.srcDBConnection readWithBlock:^(YapDatabaseReadTransaction *srcTransaction) {
[self.dstDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *dstTransaction) {
[primaryDBConnection readWithBlock:^(YapDatabaseReadTransaction *srcTransaction) {
[tempDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *dstTransaction) {
// Copy threads.
[srcTransaction
enumerateKeysAndObjectsInCollection:[TSThread collection]
@ -389,13 +341,13 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
] mutableCopy];
// Close the database.
self.dstDBConnection = nil;
tempDBConnection = nil;
self.backupStorage = nil;
return YES;
}
- (void)saveToCloud:(OWSBackupExportCompletion)completion
- (void)saveToCloud:(OWSBackupJobCompletion)completion
{
OWSAssert(completion);
@ -407,7 +359,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
[self saveNextFileToCloud:completion];
}
- (void)saveNextFileToCloud:(OWSBackupExportCompletion)completion
- (void)saveNextFileToCloud:(OWSBackupJobCompletion)completion
{
OWSAssert(completion);
@ -421,7 +373,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
@"Indicates that the backup export data is being uploaded.")
progress:@(progress)];
__weak OWSBackupExport *weakSelf = self;
__weak OWSBackupExportJob *weakSelf = self;
if (self.databaseFilePaths.count > 0) {
NSString *filePath = self.databaseFilePaths.lastObject;
@ -432,7 +384,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
success:^(NSString *recordName) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSBackupExport *strongSelf = weakSelf;
OWSBackupExportJob *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
@ -456,7 +408,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
// attachment to disk.
OWSAttachmentExport *attachmentExport = [OWSAttachmentExport new];
attachmentExport.delegate = self.delegate;
attachmentExport.exportDirPath = self.exportDirPath;
attachmentExport.jobTempDirPath = self.jobTempDirPath;
attachmentExport.attachmentId = attachmentId;
attachmentExport.attachmentFilePath = attachmentFilePath;
@ -476,7 +428,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
success:^(NSString *recordName) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSBackupExport *strongSelf = weakSelf;
OWSBackupExportJob *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
@ -507,7 +459,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
success:^(NSString *recordName) {
// Ensure that we continue to work off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSBackupExport *strongSelf = weakSelf;
OWSBackupExportJob *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
@ -530,14 +482,20 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
{
OWSAssert(self.databaseRecordMap.count > 0);
OWSAssert(self.attachmentRecordMap);
OWSAssert(self.exportDirPath.length > 0);
OWSAssert(self.databaseKeySpec.length > 0);
OWSAssert(self.jobTempDirPath.length > 0);
NSData *_Nullable databaseKeySpec =
[OWSBackupJob loadDatabaseKeySpecWithKeychainKey:kOWSBackup_ExportDatabaseKeySpec];
if (databaseKeySpec.length < 1) {
OWSProdLogAndFail(@"%@ Could not load database keyspec for export.", self.logTag);
return nil;
}
NSDictionary *json = @{
@"database_files" : self.databaseRecordMap,
@"attachment_files" : self.attachmentRecordMap,
kOWSBackup_ManifestKey_DatabaseFiles : self.databaseRecordMap,
kOWSBackup_ManifestKey_AttachmentFiles : self.attachmentRecordMap,
// JSON doesn't support byte arrays.
@"database_key_spec" : self.databaseKeySpec.base64EncodedString,
kOWSBackup_ManifestKey_DatabaseKeySpec : databaseKeySpec.base64EncodedString,
};
NSError *error;
NSData *_Nullable jsonData =
@ -546,16 +504,12 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
OWSProdLogAndFail(@"%@ error encoding manifest file: %@", self.logTag, error);
return NO;
}
// TODO: Encrypt the manifest.
self.manifestFilePath = [self.exportDirPath stringByAppendingPathComponent:@"manifest.json"];
if (![jsonData writeToFile:self.manifestFilePath atomically:YES]) {
OWSProdLogAndFail(@"%@ error writing manifest file: %@", self.logTag, error);
return NO;
}
return YES;
self.manifestFilePath =
[OWSBackupJob encryptDataAsTempFile:jsonData jobTempDirPath:self.jobTempDirPath delegate:self.delegate];
return self.manifestFilePath != nil;
}
- (void)cleanUpCloud:(OWSBackupExportCompletion)completion
- (void)cleanUpCloud:(OWSBackupJobCompletion)completion
{
OWSAssert(completion);
@ -579,7 +533,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
OWSAssert(self.manifestRecordName.length > 0);
[activeRecordNames addObject:self.manifestRecordName];
__weak OWSBackupExport *weakSelf = self;
__weak OWSBackupExportJob *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), ^{
@ -609,7 +563,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
- (void)deleteRecordsFromCloud:(NSMutableArray<NSString *> *)obsoleteRecordNames
deletedCount:(NSUInteger)deletedCount
completion:(OWSBackupExportCompletion)completion
completion:(OWSBackupJobCompletion)completion
{
OWSAssert(obsoleteRecordNames);
OWSAssert(completion);
@ -630,7 +584,7 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
NSString *recordName = obsoleteRecordNames.lastObject;
[obsoleteRecordNames removeLastObject];
__weak OWSBackupExport *weakSelf = self;
__weak OWSBackupExportJob *weakSelf = self;
[OWSBackupAPI deleteRecordFromCloudWithRecordName:recordName
success:^{
// Ensure that we continue to work off the main thread.
@ -651,86 +605,6 @@ typedef void (^OWSBackupExportCompletion)(NSError *_Nullable error);
}];
}
#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 backupExportDidSucceed:self];
});
// TODO:
}
- (void)failWithErrorDescription:(NSString *)description
{
[self failWithError:OWSErrorWithCodeDescription(OWSErrorCodeExportBackupFailed, 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 backupExportDidFail: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 backupExportDidUpdate:self description:description progress:progress];
});
}
#pragma mark - Encryption
+ (nullable NSString *)encryptAsTempFile:(NSString *)srcFilePath
exportDirPath:(NSString *)exportDirPath
delegate:(id<OWSBackupExportDelegate>)delegate
{
OWSAssert(srcFilePath.length > 0);
OWSAssert(exportDirPath.length > 0);
OWSAssert(delegate);
// TODO: Encrypt the file using self.delegate.backupKey;
NSData *_Nullable backupKey = [delegate backupKey];
OWSAssert(backupKey);
NSString *dstFilePath = [exportDirPath 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

View File

@ -1,46 +0,0 @@
//
// 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

@ -1,735 +0,0 @@
//
// 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

View File

@ -0,0 +1,17 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBackupJob.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSBackupImportJob : OWSBackupJob
- (void)startAsync;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,690 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBackupImportJob.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
NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKeySpec";
//@interface OWSAttachmentImport : NSObject
//
//@property (nonatomic, weak) id<OWSBackupJobDelegate> delegate;
//@property (nonatomic) NSString *jobTempDirPath;
//@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.jobTempDirPath.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 = [OWSBackupImportJob encryptFileAsTempFile:self.attachmentFilePath
// jobTempDirPath:self.jobTempDirPath
// 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 OWSBackupImportJob () <SSZipArchiveDelegate>
//@property (nonatomic, nullable) OWSBackupStorage *backupStorage;
@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;
@end
#pragma mark -
@implementation OWSBackupImportJob
- (void)startAsync
{
OWSAssertIsOnMainThread();
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
self.backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
[self updateProgressWithDescription:nil progress:nil];
__weak OWSBackupImportJob *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_IMPORT_PHASE_CONFIGURATION",
@"Indicates that the backup import is being configured.")
progress:nil];
__weak OWSBackupImportJob *weakSelf = self;
[self configureImport:^(BOOL success) {
if (!success) {
[self failWithErrorDescription:
NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
@"Error indicating the a backup import could not import the user's data.")];
return;
}
if (self.isComplete) {
return;
}
[self downloadAndProcessManifest:^(NSError *_Nullable error) {
if (error) {
[self failWithError:error];
return;
}
if (self.isComplete) {
return;
}
// TODO:
}];
// [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_IMPORT",
// @"Indicates that the backup import data is being imported.")
// progress:nil];
// if (![self importDatabase]) {
// [self failWithErrorDescription:
// NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
// @"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];
// }];
// }];
}];
}
// TODO: Convert these methods to sync.
- (void)configureImport:(OWSBackupJobBoolCompletion)completion
{
OWSAssert(completion);
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
if (![self ensureJobTempDir]) {
OWSProdLogAndFail(@"%@ Could not create jobTempDirPath.", self.logTag);
return completion(NO);
}
completion(YES);
// NSString *importDatabaseDirPath = [self.jobTempDirPath stringByAppendingPathComponent:@"Database"];
// self.tempDatabaseKeySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
//
// if (![OWSFileSystem ensureDirectoryExists:importDatabaseDirPath]) {
// OWSProdLogAndFail(@"%@ Could not create importDatabaseDirPath.", self.logTag);
// return completion(NO);
// }
// if (!self.tempDatabaseKeySpec) {
// OWSProdLogAndFail(@"%@ Could not create tempDatabaseKeySpec.", self.logTag);
// return completion(NO);
// }
// __weak OWSBackupImportJob *weakSelf = self;
// BackupStorageKeySpecBlock keySpecBlock = ^{
// return weakSelf.tempDatabaseKeySpec;
// };
// self.backupStorage =
// [[OWSBackupStorage alloc] initBackupStorageWithDatabaseDirPath:importDatabaseDirPath
// keySpecBlock:keySpecBlock];
// if (!self.backupStorage) {
// OWSProdLogAndFail(@"%@ Could not create backupStorage.", 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);
// });
// }];
// });
}
- (void)downloadAndProcessManifest:(OWSBackupJobCompletion)completion
{
OWSAssert(completion);
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
__weak OWSBackupImportJob *weakSelf = self;
[OWSBackupAPI downloadManifestFromCloudWithSuccess:^(NSData *data) {
[weakSelf processManifest:data
completion:^(BOOL success) {
if (success) {
completion(nil);
} else {
completion(OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed,
NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
@"Error indicating the a backup import could not import the user's data.")));
}
}];
}
failure:^(NSError *error) {
// The manifest file is critical so any error downloading it is unrecoverable.
OWSProdLogAndFail(@"%@ Could not download manifest.", self.logTag);
completion(error);
}];
}
- (void)processManifest:(NSData *)manifestData completion:(OWSBackupJobBoolCompletion)completion
{
OWSAssert(completion);
if (self.isComplete) {
return;
}
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
NSError *error;
NSDictionary<NSString *, id> *_Nullable json =
[NSJSONSerialization JSONObjectWithData:manifestData options:0 error:&error];
if (![json isKindOfClass:[NSDictionary class]]) {
OWSProdLogAndFail(@"%@ Could not download manifest.", self.logTag);
return completion(NO);
}
NSDictionary<NSString *, NSString *> *_Nullable databaseRecordMap = json[kOWSBackup_ManifestKey_DatabaseFiles];
NSDictionary<NSString *, NSString *> *_Nullable attachmentRecordMap = json[kOWSBackup_ManifestKey_AttachmentFiles];
NSString *_Nullable databaseKeySpecBase64 = json[kOWSBackup_ManifestKey_DatabaseKeySpec];
if (!([databaseRecordMap isKindOfClass:[NSDictionary class]] &&
[attachmentRecordMap isKindOfClass:[NSDictionary class]] &&
[databaseKeySpecBase64 isKindOfClass:[NSString class]])) {
OWSProdLogAndFail(@"%@ Invalid manifest.", self.logTag);
return completion(NO);
}
NSData *_Nullable databaseKeySpec = [NSData dataFromBase64String:databaseKeySpecBase64];
if (!databaseKeySpec) {
OWSProdLogAndFail(@"%@ Invalid manifest databaseKeySpec.", self.logTag);
return completion(NO);
}
if (![OWSBackupJob storeDatabaseKeySpec:databaseKeySpec keychainKey:kOWSBackup_ImportDatabaseKeySpec]) {
OWSProdLogAndFail(@"%@ Couldn't store databaseKeySpec from manifest.", self.logTag);
return completion(NO);
}
self.databaseRecordMap = [databaseRecordMap mutableCopy];
self.attachmentRecordMap = [attachmentRecordMap mutableCopy];
return completion(YES);
}
//- (BOOL)importDatabase
//{
// DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
//
// YapDatabaseConnection *_Nullable tempDBConnection = self.backupStorage.newDatabaseConnection;
// if (!tempDBConnection) {
// OWSProdLogAndFail(@"%@ Could not create tempDBConnection.", self.logTag);
// return NO;
// }
// YapDatabaseConnection *_Nullable primaryDBConnection = self.primaryStorage.newDatabaseConnection;
// if (!primaryDBConnection) {
// OWSProdLogAndFail(@"%@ Could not create primaryDBConnection.", self.logTag);
// return NO;
// }
//
// __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];
//
// [primaryDBConnection readWithBlock:^(YapDatabaseReadTransaction *srcTransaction) {
// [tempDBConnection 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.
// tempDBConnection = nil;
// self.backupStorage = nil;
//
// return YES;
//}
//
//- (void)saveToCloud:(OWSBackupJobCompletion)completion
//{
// OWSAssert(completion);
//
// DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
//
// self.databaseRecordMap = [NSMutableDictionary new];
// self.attachmentRecordMap = [NSMutableDictionary new];
//
// [self saveNextFileToCloud:completion];
//}
//
//- (void)saveNextFileToCloud:(OWSBackupJobCompletion)completion
//{
// OWSAssert(completion);
//
// if (self.isComplete) {
// return;
// }
//
// CGFloat progress
// = (self.databaseRecordMap.count / (CGFloat)(self.databaseRecordMap.count + self.databaseFilePaths.count));
// [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_UPLOAD",
// @"Indicates that the backup import data is being uploaded.")
// progress:@(progress)];
//
// __weak OWSBackupImportJob *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), ^{
// OWSBackupImportJob *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.jobTempDirPath = self.jobTempDirPath;
// 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), ^{
// OWSBackupImportJob *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_IMPORT_ERROR_COULD_NOT_IMPORT",
// @"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), ^{
// OWSBackupImportJob *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.jobTempDirPath.length > 0);
// OWSAssert(self.tempDatabaseKeySpec.length > 0);
//
// NSDictionary *json = @{
// @"database_files" : self.databaseRecordMap,
// @"attachment_files" : self.attachmentRecordMap,
// // JSON doesn't support byte arrays.
// @"database_key_spec" : self.tempDatabaseKeySpec.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.jobTempDirPath stringByAppendingPathComponent:@"manifest.json"];
// if (![jsonData writeToFile:self.manifestFilePath atomically:YES]) {
// OWSProdLogAndFail(@"%@ error writing manifest file: %@", self.logTag, error);
// return NO;
// }
// return YES;
//}
//
//- (void)cleanUpCloud:(OWSBackupJobCompletion)completion
//{
// OWSAssert(completion);
//
// DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
//
// [self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_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 OWSBackupImportJob *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:(OWSBackupJobCompletion)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_IMPORT_PHASE_CLEAN_UP",
// @"Indicates that the cloud is being cleaned up.")
// progress:@(progress)];
//
// NSString *recordName = obsoleteRecordNames.lastObject;
// [obsoleteRecordNames removeLastObject];
//
// __weak OWSBackupImportJob *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];
// });
// }];
//}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,84 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
extern NSString *const kOWSBackup_ManifestKey_DatabaseFiles;
extern NSString *const kOWSBackup_ManifestKey_AttachmentFiles;
extern NSString *const kOWSBackup_ManifestKey_DatabaseKeySpec;
typedef void (^OWSBackupJobBoolCompletion)(BOOL success);
typedef void (^OWSBackupJobCompletion)(NSError *_Nullable error);
@class OWSBackupJob;
@protocol OWSBackupJobDelegate <NSObject>
// TODO: This should eventually be the backup key stored in the Signal Service
// and retrieved with the backup PIN.
- (nullable NSData *)backupKey;
// Either backupJobDidSucceed:... or backupJobDidFail:... will
// be called exactly once on the main thread UNLESS:
//
// * The job was never started.
// * The job was cancelled.
- (void)backupJobDidSucceed:(OWSBackupJob *)backupJob;
- (void)backupJobDidFail:(OWSBackupJob *)backupJob error:(NSError *)error;
- (void)backupJobDidUpdate:(OWSBackupJob *)backupJob
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress;
@end
#pragma mark -
@class OWSPrimaryStorage;
@interface OWSBackupJob : NSObject
@property (nonatomic, weak, readonly) id<OWSBackupJobDelegate> delegate;
// Indicates that the backup succeeded, failed or was cancelled.
@property (atomic, readonly) BOOL isComplete;
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
@property (nonatomic, readonly) NSString *jobTempDirPath;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)delegate primaryStorage:(OWSPrimaryStorage *)primaryStorage;
#pragma mark - Private
- (BOOL)ensureJobTempDir;
- (void)cancel;
- (void)succeed;
- (void)failWithErrorDescription:(NSString *)description;
- (void)failWithError:(NSError *)error;
- (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress;
#pragma mark - Database KeySpec
+ (nullable NSData *)loadDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey;
+ (BOOL)storeDatabaseKeySpec:(NSData *)data keychainKey:(NSString *)keychainKey;
+ (BOOL)generateRandomDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey;
#pragma mark - Encryption
+ (nullable NSString *)encryptFileAsTempFile:(NSString *)srcFilePath
jobTempDirPath:(NSString *)jobTempDirPath
delegate:(id<OWSBackupJobDelegate>)delegate;
+ (nullable NSString *)encryptDataAsTempFile:(NSData *)data
jobTempDirPath:(NSString *)jobTempDirPath
delegate:(id<OWSBackupJobDelegate>)delegate;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,313 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBackupJob.h"
#import "Signal-Swift.h"
#import <Curve25519Kit/Randomness.h>
#import <SAMKeychain/SAMKeychain.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
NSString *const kOWSBackup_ManifestKey_DatabaseFiles = @"database_files";
NSString *const kOWSBackup_ManifestKey_AttachmentFiles = @"attachment_files";
NSString *const kOWSBackup_ManifestKey_DatabaseKeySpec = @"database_key_spec";
NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
@interface OWSBackupJob ()
//<SSZipArchiveDelegate>
@property (nonatomic, weak) id<OWSBackupJobDelegate> delegate;
@property (atomic) BOOL isComplete;
@property (nonatomic) OWSPrimaryStorage *primaryStorage;
//@property (nonatomic, nullable) OWSBackupStorage *backupStorage;
//
//// TODO: Should we store this in the keychain instead?
//@property (nonatomic, nullable) NSData *tempDatabaseKeySpec;
//
//@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 *jobTempDirPath;
@end
#pragma mark -
@implementation OWSBackupJob
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)delegate primaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
OWSAssert(primaryStorage);
OWSAssert([OWSStorage isStorageReady]);
self.delegate = delegate;
self.primaryStorage = primaryStorage;
return self;
}
- (void)dealloc
{
// Surface memory leaks by logging the deallocation.
DDLogVerbose(@"Dealloc: %@", self.class);
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (self.jobTempDirPath) {
[OWSFileSystem deleteFileIfExists:self.jobTempDirPath];
}
}
- (BOOL)ensureJobTempDir
{
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
NSString *temporaryDirectory = NSTemporaryDirectory();
self.jobTempDirPath = [temporaryDirectory stringByAppendingString:[NSUUID UUID].UUIDString];
if (![OWSFileSystem ensureDirectoryExists:self.jobTempDirPath]) {
OWSProdLogAndFail(@"%@ Could not create jobTempDirPath.", self.logTag);
return NO;
}
return YES;
}
//- (void)configureImport:(OWSBackupJobBoolCompletion)completion
//{
// OWSAssert(completion);
//
// DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
//
// NSString *temporaryDirectory = NSTemporaryDirectory();
// self.jobTempDirPath = [temporaryDirectory stringByAppendingString:[NSUUID UUID].UUIDString];
// NSString *tempDatabaseDirPath = [self.jobTempDirPath stringByAppendingPathComponent:@"Database"];
// self.tempDatabaseKeySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
//
// if (![OWSFileSystem ensureDirectoryExists:self.jobTempDirPath]) {
// OWSProdLogAndFail(@"%@ Could not create jobTempDirPath.", self.logTag);
// return completion(NO);
// }
// if (![OWSFileSystem ensureDirectoryExists:tempDatabaseDirPath]) {
// OWSProdLogAndFail(@"%@ Could not create tempDatabaseDirPath.", self.logTag);
// return completion(NO);
// }
// if (!self.tempDatabaseKeySpec) {
// OWSProdLogAndFail(@"%@ Could not create tempDatabaseKeySpec.", self.logTag);
// return completion(NO);
// }
// __weak OWSBackupImportJob *weakSelf = self;
// BackupStorageKeySpecBlock keySpecBlock = ^{
// return weakSelf.tempDatabaseKeySpec;
// };
// self.backupStorage =
// [[OWSBackupStorage alloc] initBackupStorageWithDatabaseDirPath:tempDatabaseDirPath keySpecBlock:keySpecBlock];
// if (!self.backupStorage) {
// OWSProdLogAndFail(@"%@ Could not create backupStorage.", self.logTag);
// return completion(NO);
// }
// _tempDBConnection = self.backupStorage.newDatabaseConnection;
// if (!self.tempDBConnection) {
// OWSProdLogAndFail(@"%@ Could not create tempDBConnection.", 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);
// });
// }];
// });
//}
#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 backupJobDidSucceed: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 backupJobDidFail: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 backupJobDidUpdate:self description:description progress:progress];
});
}
#pragma mark - Database KeySpec
+ (nullable NSData *)loadDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey
{
OWSAssert(keychainKey.length > 0);
NSError *error;
NSData *_Nullable value =
[SAMKeychain passwordDataForService:kOWSBackup_KeychainService account:keychainKey error:&error];
if (!value || error) {
DDLogError(@"%@ could not load database keyspec: %@", self.logTag, error);
}
return value;
}
+ (BOOL)storeDatabaseKeySpec:(NSData *)data keychainKey:(NSString *)keychainKey
{
OWSAssert(keychainKey.length > 0);
OWSAssert(data.length > 0);
NSError *error;
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
BOOL success =
[SAMKeychain setPasswordData:data forService:kOWSBackup_KeychainService account:keychainKey error:&error];
if (!success || error) {
OWSFail(@"%@ Could not store database keyspec: %@.", self.logTag, error);
return NO;
} else {
return YES;
}
}
+ (BOOL)generateRandomDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey
{
OWSAssert(keychainKey.length > 0);
NSData *_Nullable databaseKeySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
if (!databaseKeySpec) {
OWSFail(@"%@ Could not generate database keyspec.", self.logTag);
return NO;
}
return [self storeDatabaseKeySpec:databaseKeySpec keychainKey:keychainKey];
}
#pragma mark - Encryption
+ (nullable NSString *)encryptFileAsTempFile:(NSString *)srcFilePath
jobTempDirPath:(NSString *)jobTempDirPath
delegate:(id<OWSBackupJobDelegate>)delegate
{
OWSAssert(srcFilePath.length > 0);
OWSAssert(jobTempDirPath.length > 0);
OWSAssert(delegate);
// TODO: Encrypt the file using self.delegate.backupKey;
NSData *_Nullable backupKey = [delegate backupKey];
OWSAssert(backupKey);
NSString *dstFilePath = [jobTempDirPath 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;
}
+ (nullable NSString *)encryptDataAsTempFile:(NSData *)data
jobTempDirPath:(NSString *)jobTempDirPath
delegate:(id<OWSBackupJobDelegate>)delegate
{
OWSAssert(data);
OWSAssert(jobTempDirPath.length > 0);
OWSAssert(delegate);
// TODO: Encrypt the file using self.delegate.backupKey;
NSData *_Nullable backupKey = [delegate backupKey];
OWSAssert(backupKey);
NSString *dstFilePath = [jobTempDirPath stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
NSError *error;
BOOL success = [data writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
if (!success || error) {
OWSProdLogAndFail(@"%@ error writing encrypted file: %@", self.logTag, error);
return nil;
}
return dstFilePath;
}
@end
NS_ASSUME_NONNULL_END