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