Implement backup import logic.
This commit is contained in:
parent
5035cb040e
commit
30065493a3
|
@ -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 */,
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue