session-ios/Session/Signal/OWSBackup.m

913 lines
31 KiB
Mathematica
Raw Normal View History

2018-01-04 21:16:24 +01:00
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
2018-01-04 21:16:24 +01:00
//
#import "OWSBackup.h"
2018-03-08 19:38:42 +01:00
#import "OWSBackupExportJob.h"
2018-03-20 22:22:19 +01:00
#import "OWSBackupIO.h"
2018-03-08 19:38:42 +01:00
#import "OWSBackupImportJob.h"
2019-05-02 23:58:48 +02:00
#import "Session-Swift.h"
2018-11-27 17:15:09 +01:00
#import <PromiseKit/AnyPromise.h>
2020-11-12 00:41:45 +01:00
#import <SignalCoreKit/Randomness.h>
2020-11-25 06:15:16 +01:00
#import <SessionMessagingKit/OWSIdentityManager.h>
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
2018-01-04 21:16:24 +01:00
2018-11-27 17:15:09 +01:00
@import CloudKit;
2018-11-19 23:35:35 +01:00
NS_ASSUME_NONNULL_BEGIN
NSString *const NSNotificationNameBackupStateDidChange = @"NSNotificationNameBackupStateDidChange";
2018-01-09 16:12:14 +01:00
NSString *const OWSPrimaryStorage_OWSBackupCollection = @"OWSPrimaryStorage_OWSBackupCollection";
NSString *const OWSBackup_IsBackupEnabledKey = @"OWSBackup_IsBackupEnabledKey";
2018-03-06 21:00:28 +01:00
NSString *const OWSBackup_LastExportSuccessDateKey = @"OWSBackup_LastExportSuccessDateKey";
NSString *const OWSBackup_LastExportFailureDateKey = @"OWSBackup_LastExportFailureDateKey";
NSString *const OWSBackupErrorDomain = @"OWSBackupErrorDomain";
2018-11-19 23:35:35 +01:00
NSString *NSStringForBackupExportState(OWSBackupState state)
{
switch (state) {
case OWSBackupState_Idle:
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_IDLE", @"Indicates that app is not backing up.");
case OWSBackupState_InProgress:
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_IN_PROGRESS", @"Indicates that app is backing up.");
case OWSBackupState_Failed:
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_FAILED", @"Indicates that the last backup failed.");
case OWSBackupState_Succeeded:
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_SUCCEEDED", @"Indicates that the last backup succeeded.");
}
}
NSString *NSStringForBackupImportState(OWSBackupState state)
{
switch (state) {
case OWSBackupState_Idle:
return NSLocalizedString(@"SETTINGS_BACKUP_IMPORT_STATUS_IDLE", @"Indicates that app is not restoring up.");
case OWSBackupState_InProgress:
return NSLocalizedString(
@"SETTINGS_BACKUP_IMPORT_STATUS_IN_PROGRESS", @"Indicates that app is restoring up.");
case OWSBackupState_Failed:
return NSLocalizedString(
@"SETTINGS_BACKUP_IMPORT_STATUS_FAILED", @"Indicates that the last backup restore failed.");
case OWSBackupState_Succeeded:
return NSLocalizedString(
@"SETTINGS_BACKUP_IMPORT_STATUS_SUCCEEDED", @"Indicates that the last backup restore succeeded.");
}
}
2018-01-09 16:12:14 +01:00
2018-11-28 15:32:12 +01:00
NSArray<NSString *> *MiscCollectionsToBackup(void)
{
return @[
2018-11-28 15:48:41 +01:00
kOWSBlockingManager_BlockListCollection,
OWSUserProfile.collection,
SSKIncrementingIdFinder.collectionName,
OWSPreferencesSignalDatabaseCollection,
2018-11-28 15:32:12 +01:00
];
}
2018-11-27 20:01:25 +01:00
typedef NS_ENUM(NSInteger, OWSBackupErrorCode) {
OWSBackupErrorCodeAssertionFailure = 0,
};
NSError *OWSBackupErrorWithDescription(NSString *description)
{
return [NSError errorWithDomain:@"OWSBackupErrorDomain"
code:OWSBackupErrorCodeAssertionFailure
userInfo:@{ NSLocalizedDescriptionKey : description }];
}
2018-03-06 21:00:28 +01:00
// TODO: Observe Reachability.
2018-03-08 19:38:42 +01:00
@interface OWSBackup () <OWSBackupJobDelegate>
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
2018-01-04 21:16:24 +01:00
2018-03-06 14:29:25 +01:00
// This property should only be accessed on the main thread.
2018-03-12 21:56:12 +01:00
@property (nonatomic, nullable) OWSBackupExportJob *backupExportJob;
2018-01-04 21:16:24 +01:00
2018-03-08 15:52:57 +01:00
// This property should only be accessed on the main thread.
2018-03-12 21:56:12 +01:00
@property (nonatomic, nullable) OWSBackupImportJob *backupImportJob;
2018-03-08 15:52:57 +01:00
@property (nonatomic, nullable) NSString *backupExportDescription;
@property (nonatomic, nullable) NSNumber *backupExportProgress;
@property (nonatomic, nullable) NSString *backupImportDescription;
@property (nonatomic, nullable) NSNumber *backupImportProgress;
@property (atomic) OWSBackupState backupExportState;
@property (atomic) OWSBackupState backupImportState;
2018-01-04 21:16:24 +01:00
@end
#pragma mark -
@implementation OWSBackup
@synthesize dbConnection = _dbConnection;
+ (instancetype)sharedManager
2018-01-04 23:47:33 +01:00
{
2018-11-19 15:36:18 +01:00
OWSAssertDebug(AppEnvironment.shared.backup);
2018-11-19 15:36:18 +01:00
return AppEnvironment.shared.backup;
}
2018-11-21 18:25:24 +01:00
- (instancetype)init
{
self = [super init];
2018-01-09 16:12:14 +01:00
if (!self) {
return self;
2018-01-09 16:12:14 +01:00
}
self.backupExportState = OWSBackupState_Idle;
self.backupImportState = OWSBackupState_Idle;
2018-01-09 16:12:14 +01:00
OWSSingletonAssert();
2018-01-09 16:12:14 +01:00
2018-11-19 15:36:18 +01:00
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
[self setup];
}];
2018-03-06 16:10:22 +01:00
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setup
{
2019-04-03 19:11:08 +02:00
if (!OWSBackup.isFeatureEnabled) {
return;
}
2019-04-03 19:11:08 +02:00
[OWSBackupAPI setup];
2018-03-06 14:29:25 +01:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:OWSApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(registrationStateDidChange)
name:RegistrationStateDidChangeNotification
object:nil];
2018-11-27 17:15:09 +01:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(ckAccountChanged)
name:CKAccountChangedNotification
object:nil];
2018-03-06 14:29:25 +01:00
2018-03-06 16:10:22 +01:00
// We want to start a backup if necessary on app launch, but app launch is a
// busy time and it's important to remain responsive, so wait a few seconds before
// starting the backup.
//
// TODO: Make this period longer.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self ensureBackupExportState];
});
2018-01-09 16:12:14 +01:00
}
2018-11-21 18:25:24 +01:00
- (YapDatabaseConnection *)dbConnection
{
@synchronized(self) {
if (!_dbConnection) {
_dbConnection = self.primaryStorage.newDatabaseConnection;
}
return _dbConnection;
}
}
#pragma mark - Dependencies
- (OWSPrimaryStorage *)primaryStorage
{
OWSAssertDebug(SSKEnvironment.shared.primaryStorage);
return SSKEnvironment.shared.primaryStorage;
}
2018-11-26 16:24:36 +01:00
- (TSAccountManager *)tsAccountManager
{
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
return SSKEnvironment.shared.tsAccountManager;
}
2018-12-03 14:46:58 +01:00
+ (BOOL)isFeatureEnabled
{
return NO;
}
2018-03-08 14:59:34 +01:00
#pragma mark - Backup Export
2018-03-13 16:18:41 +01:00
- (void)tryToExportBackup
{
OWSAssertIsOnMainThread();
OWSAssertDebug(!self.backupExportJob);
2018-03-13 16:18:41 +01:00
if (!self.canBackupExport) {
// TODO: Offer a reason in the UI.
return;
}
if (!self.tsAccountManager.isRegisteredAndReady) {
2018-11-27 15:43:32 +01:00
OWSFailDebug(@"Can't backup; not registered and ready.");
return;
}
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
if (recipientId.length < 1) {
OWSFailDebug(@"Can't backup; missing recipientId.");
return;
}
2018-03-13 16:18:41 +01:00
// In development, make sure there's no export or import in progress.
[self.backupExportJob cancel];
self.backupExportJob = nil;
[self.backupImportJob cancel];
self.backupImportJob = nil;
self.backupExportState = OWSBackupState_InProgress;
2018-03-13 16:18:41 +01:00
self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self recipientId:recipientId];
2018-11-28 23:01:26 +01:00
[self.backupExportJob start];
2018-03-13 16:18:41 +01:00
[self postDidChangeNotification];
}
- (void)cancelExportBackup
{
[self.backupExportJob cancel];
self.backupExportJob = nil;
2018-03-13 16:20:26 +01:00
[self ensureBackupExportState];
2018-03-13 16:18:41 +01:00
}
2018-03-06 21:00:28 +01:00
- (void)setLastExportSuccessDate:(NSDate *)value
{
OWSAssertDebug(value);
2018-03-08 14:49:44 +01:00
2018-03-06 21:00:28 +01:00
[self.dbConnection setDate:value
forKey:OWSBackup_LastExportSuccessDateKey
inCollection:OWSPrimaryStorage_OWSBackupCollection];
}
2018-03-06 21:00:28 +01:00
- (nullable NSDate *)lastExportSuccessDate
2018-01-09 16:12:14 +01:00
{
2018-03-06 21:00:28 +01:00
return [self.dbConnection dateForKey:OWSBackup_LastExportSuccessDateKey
inCollection:OWSPrimaryStorage_OWSBackupCollection];
}
2018-01-09 16:12:14 +01:00
2018-03-06 21:00:28 +01:00
- (void)setLastExportFailureDate:(NSDate *)value
{
OWSAssertDebug(value);
2018-03-08 14:49:44 +01:00
2018-03-06 21:00:28 +01:00
[self.dbConnection setDate:value
forKey:OWSBackup_LastExportFailureDateKey
inCollection:OWSPrimaryStorage_OWSBackupCollection];
}
- (nullable NSDate *)lastExportFailureDate
{
return [self.dbConnection dateForKey:OWSBackup_LastExportFailureDateKey
inCollection:OWSPrimaryStorage_OWSBackupCollection];
2018-01-09 16:12:14 +01:00
}
- (BOOL)isBackupEnabled
{
return [self.dbConnection boolForKey:OWSBackup_IsBackupEnabledKey
inCollection:OWSPrimaryStorage_OWSBackupCollection
defaultValue:NO];
}
- (void)setIsBackupEnabled:(BOOL)value
{
[self.dbConnection setBool:value
forKey:OWSBackup_IsBackupEnabledKey
inCollection:OWSPrimaryStorage_OWSBackupCollection];
if (!value) {
[self.dbConnection removeObjectForKey:OWSBackup_LastExportSuccessDateKey
inCollection:OWSPrimaryStorage_OWSBackupCollection];
[self.dbConnection removeObjectForKey:OWSBackup_LastExportFailureDateKey
inCollection:OWSPrimaryStorage_OWSBackupCollection];
}
2018-03-08 15:52:57 +01:00
[self postDidChangeNotification];
2018-03-06 16:10:22 +01:00
[self ensureBackupExportState];
}
2018-11-21 20:05:15 +01:00
- (BOOL)hasPendingRestoreDecision
{
return [self.tsAccountManager hasPendingBackupRestoreDecision];
2018-11-21 20:05:15 +01:00
}
- (void)setHasPendingRestoreDecision:(BOOL)value
{
[self.tsAccountManager setHasPendingBackupRestoreDecision:value];
2018-11-21 20:05:15 +01:00
}
2018-03-13 16:18:41 +01:00
- (BOOL)canBackupExport
2018-03-06 14:29:25 +01:00
{
if (!self.isBackupEnabled) {
return NO;
}
if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
2018-03-12 21:49:57 +01:00
// Don't start backups when app is in the background.
2018-03-06 14:29:25 +01:00
return NO;
}
if (![self.tsAccountManager isRegisteredAndReady]) {
2018-03-06 14:29:25 +01:00
return NO;
}
2018-03-13 16:18:41 +01:00
return YES;
}
- (BOOL)shouldHaveBackupExport
{
if (!self.canBackupExport) {
return NO;
}
if (self.backupExportJob) {
// If there's already a job in progress, let it complete.
return YES;
}
2018-03-06 21:00:28 +01:00
NSDate *_Nullable lastExportSuccessDate = self.lastExportSuccessDate;
NSDate *_Nullable lastExportFailureDate = self.lastExportFailureDate;
// Wait N hours before retrying after a success.
2018-03-12 20:32:13 +01:00
const NSTimeInterval kRetryAfterSuccess = 24 * kHourInterval;
2018-03-06 21:00:28 +01:00
if (lastExportSuccessDate && fabs(lastExportSuccessDate.timeIntervalSinceNow) < kRetryAfterSuccess) {
return NO;
}
// Wait N hours before retrying after a failure.
2018-03-12 20:32:13 +01:00
const NSTimeInterval kRetryAfterFailure = 6 * kHourInterval;
2018-03-06 21:00:28 +01:00
if (lastExportFailureDate && fabs(lastExportFailureDate.timeIntervalSinceNow) < kRetryAfterFailure) {
return NO;
}
2018-03-08 15:52:57 +01:00
// Don't export backup if there's an import in progress.
//
// This conflict shouldn't occur in production since we won't enable backup
// export until an import is complete, but this could happen in development.
2018-03-12 21:56:12 +01:00
if (self.backupImportJob) {
2018-03-08 15:52:57 +01:00
return NO;
}
2018-03-06 14:29:25 +01:00
2018-03-06 16:10:22 +01:00
// TODO: There's other conditions that affect this decision,
2018-03-06 21:00:28 +01:00
// e.g. Reachability, wifi v. cellular, etc.
2018-03-06 14:29:25 +01:00
return YES;
}
- (void)ensureBackupExportState
{
OWSAssertIsOnMainThread();
2018-12-03 14:46:58 +01:00
if (!OWSBackup.isFeatureEnabled) {
return;
}
2018-11-19 15:36:18 +01:00
if (!CurrentAppContext().isMainApp) {
return;
}
if (!self.tsAccountManager.isRegisteredAndReady) {
OWSLogError(@"Can't backup; not registered and ready.");
return;
}
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
if (recipientId.length < 1) {
OWSFailDebug(@"Can't backup; missing recipientId.");
return;
}
2018-03-06 21:00:28 +01:00
// Start or abort a backup export if neccessary.
2018-03-12 21:56:12 +01:00
if (!self.shouldHaveBackupExport && self.backupExportJob) {
[self.backupExportJob cancel];
self.backupExportJob = nil;
} else if (self.shouldHaveBackupExport && !self.backupExportJob) {
self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self recipientId:recipientId];
2018-11-28 23:01:26 +01:00
[self.backupExportJob start];
2018-03-06 14:29:25 +01:00
}
2018-03-06 21:00:28 +01:00
// Update the state flag.
OWSBackupState backupExportState = OWSBackupState_Idle;
2018-03-12 21:56:12 +01:00
if (self.backupExportJob) {
2018-03-06 21:00:28 +01:00
backupExportState = OWSBackupState_InProgress;
} else {
NSDate *_Nullable lastExportSuccessDate = self.lastExportSuccessDate;
NSDate *_Nullable lastExportFailureDate = self.lastExportFailureDate;
if (!lastExportSuccessDate && !lastExportFailureDate) {
backupExportState = OWSBackupState_Idle;
} else if (lastExportSuccessDate && lastExportFailureDate) {
2018-03-12 22:03:43 +01:00
backupExportState = ([lastExportSuccessDate isAfterDate:lastExportFailureDate] ? OWSBackupState_Succeeded
: OWSBackupState_Failed);
2018-03-06 21:00:28 +01:00
} else if (lastExportSuccessDate) {
backupExportState = OWSBackupState_Succeeded;
} else if (lastExportFailureDate) {
backupExportState = OWSBackupState_Failed;
} else {
OWSFailDebug(@"unexpected condition.");
2018-03-06 21:00:28 +01:00
}
}
BOOL stateDidChange = self.backupExportState != backupExportState;
self.backupExportState = backupExportState;
2018-03-06 21:00:28 +01:00
if (stateDidChange) {
2018-03-08 15:52:57 +01:00
[self postDidChangeNotification];
2018-03-06 21:00:28 +01:00
}
2018-03-06 14:29:25 +01:00
}
2018-03-08 14:59:34 +01:00
#pragma mark - Backup Import
- (void)allRecipientIdsWithManifestsInCloud:(OWSBackupStringListBlock)success failure:(OWSBackupErrorBlock)failure
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
[OWSBackupAPI
allRecipientIdsWithManifestsInCloudWithSuccess:^(NSArray<NSString *> *recipientIds) {
dispatch_async(dispatch_get_main_queue(), ^{
success(recipientIds);
});
}
failure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
failure(error);
});
}];
}
2018-11-28 23:01:26 +01:00
- (AnyPromise *)ensureCloudKitAccess
2018-11-27 17:15:09 +01:00
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
2018-11-27 17:33:31 +01:00
AnyPromise * (^failWithUnexpectedError)(void) = ^{
NSError *error = [NSError errorWithDomain:OWSBackupErrorDomain
code:1
userInfo:@{
NSLocalizedDescriptionKey : NSLocalizedString(@"BACKUP_UNEXPECTED_ERROR",
@"Error shown when backup fails due to an unexpected error.")
}];
return [AnyPromise promiseWithValue:error];
2018-11-27 17:15:09 +01:00
};
if (!self.tsAccountManager.isRegisteredAndReady) {
OWSLogError(@"Can't backup; not registered and ready.");
return failWithUnexpectedError();
}
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
if (recipientId.length < 1) {
OWSFailDebug(@"Can't backup; missing recipientId.");
return failWithUnexpectedError();
}
2018-11-28 23:01:26 +01:00
return [OWSBackupAPI ensureCloudKitAccessObjc];
2018-11-27 17:15:09 +01:00
}
2018-03-08 14:59:34 +01:00
- (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
2018-03-08 14:59:34 +01:00
2018-12-11 15:58:37 +01:00
if (!OWSBackup.isFeatureEnabled) {
dispatch_async(dispatch_get_main_queue(), ^{
success(NO);
});
return;
}
void (^failWithUnexpectedError)(void) = ^{
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error =
[NSError errorWithDomain:OWSBackupErrorDomain
code:1
userInfo:@{
NSLocalizedDescriptionKey : NSLocalizedString(@"BACKUP_UNEXPECTED_ERROR",
@"Error shown when backup fails due to an unexpected error.")
}];
failure(error);
});
};
if (!self.tsAccountManager.isRegisteredAndReady) {
OWSLogError(@"Can't backup; not registered and ready.");
return failWithUnexpectedError();
}
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
if (recipientId.length < 1) {
OWSFailDebug(@"Can't backup; missing recipientId.");
return failWithUnexpectedError();
}
2018-11-28 23:01:26 +01:00
[[OWSBackupAPI ensureCloudKitAccessObjc]
2018-11-27 17:49:30 +01:00
.thenInBackground(^{
2018-11-27 20:01:25 +01:00
return [OWSBackupAPI checkForManifestInCloudObjcWithRecipientId:recipientId];
})
.then(^(NSNumber *value) {
success(value.boolValue);
2018-11-27 17:33:31 +01:00
})
.catch(^(NSError *error) {
2018-11-27 17:49:30 +01:00
failure(error);
2018-11-27 17:33:31 +01:00
}) retainUntilComplete];
2018-03-08 14:59:34 +01:00
}
2018-03-08 15:52:57 +01:00
- (void)tryToImportBackup
{
OWSAssertIsOnMainThread();
OWSAssertDebug(!self.backupImportJob);
2018-03-08 15:52:57 +01:00
if (!self.tsAccountManager.isRegisteredAndReady) {
OWSLogError(@"Can't restore backup; not registered and ready.");
return;
}
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
if (recipientId.length < 1) {
OWSLogError(@"Can't restore backup; missing recipientId.");
return;
}
2018-03-08 15:52:57 +01:00
// In development, make sure there's no export or import in progress.
2018-03-12 21:56:12 +01:00
[self.backupExportJob cancel];
self.backupExportJob = nil;
[self.backupImportJob cancel];
self.backupImportJob = nil;
2018-03-08 15:52:57 +01:00
self.backupImportState = OWSBackupState_InProgress;
2018-03-08 15:52:57 +01:00
self.backupImportJob = [[OWSBackupImportJob alloc] initWithDelegate:self recipientId:recipientId];
2018-11-29 16:32:27 +01:00
[self.backupImportJob start];
2018-03-08 16:10:43 +01:00
[self postDidChangeNotification];
}
- (void)cancelImportBackup
{
2018-03-12 21:56:12 +01:00
[self.backupImportJob cancel];
self.backupImportJob = nil;
2018-03-08 16:10:43 +01:00
self.backupImportState = OWSBackupState_Idle;
2018-03-08 16:10:43 +01:00
[self postDidChangeNotification];
2018-03-08 15:52:57 +01:00
}
2018-03-06 14:29:25 +01:00
#pragma mark -
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
[self ensureBackupExportState];
}
- (void)registrationStateDidChange
{
OWSAssertIsOnMainThread();
[self ensureBackupExportState];
[self postDidChangeNotification];
2018-03-06 14:29:25 +01:00
}
2018-11-27 17:15:09 +01:00
- (void)ckAccountChanged
{
dispatch_async(dispatch_get_main_queue(), ^{
[self ensureBackupExportState];
[self postDidChangeNotification];
});
2018-11-27 17:15:09 +01:00
}
2018-03-08 19:38:42 +01:00
#pragma mark - OWSBackupJobDelegate
2018-03-06 16:10:22 +01:00
2018-03-08 14:49:44 +01:00
// We use a delegate method to avoid storing this key in memory.
- (nullable NSData *)backupEncryptionKey
2018-03-06 16:10:22 +01:00
{
// TODO: Use actual encryption key.
return [@"temp" dataUsingEncoding:NSUTF8StringEncoding];
2018-03-06 16:10:22 +01:00
}
2018-03-08 19:38:42 +01:00
- (void)backupJobDidSucceed:(OWSBackupJob *)backupJob
2018-03-06 16:10:22 +01:00
{
2018-03-08 15:52:57 +01:00
OWSAssertIsOnMainThread();
OWSLogInfo(@".");
2018-03-06 16:10:22 +01:00
2018-03-12 21:56:12 +01:00
if (self.backupImportJob == backupJob) {
self.backupImportJob = nil;
2018-03-06 21:00:28 +01:00
self.backupImportState = OWSBackupState_Succeeded;
2018-03-12 21:56:12 +01:00
} else if (self.backupExportJob == backupJob) {
self.backupExportJob = nil;
2018-03-06 16:10:22 +01:00
2018-03-08 19:38:42 +01:00
[self setLastExportSuccessDate:[NSDate new]];
2018-03-08 15:52:57 +01:00
2018-03-08 19:38:42 +01:00
[self ensureBackupExportState];
} else {
OWSLogWarn(@"obsolete job succeeded: %@", [backupJob class]);
2018-03-06 16:10:22 +01:00
return;
}
2018-03-08 19:38:42 +01:00
[self postDidChangeNotification];
2018-03-06 21:00:28 +01:00
}
2018-03-08 19:38:42 +01:00
- (void)backupJobDidFail:(OWSBackupJob *)backupJob error:(NSError *)error
2018-03-08 15:52:57 +01:00
{
OWSAssertIsOnMainThread();
OWSLogInfo(@": %@", error);
2018-03-08 16:10:43 +01:00
2018-03-12 21:56:12 +01:00
if (self.backupImportJob == backupJob) {
self.backupImportJob = nil;
2018-03-08 15:52:57 +01:00
self.backupImportState = OWSBackupState_Failed;
2018-03-12 21:56:12 +01:00
} else if (self.backupExportJob == backupJob) {
self.backupExportJob = nil;
2018-03-08 15:52:57 +01:00
2018-03-08 19:38:42 +01:00
[self setLastExportFailureDate:[NSDate new]];
2018-03-08 15:52:57 +01:00
2018-03-08 19:38:42 +01:00
[self ensureBackupExportState];
} else {
OWSLogInfo(@"obsolete backup job failed.");
2018-03-08 15:52:57 +01:00
return;
}
[self postDidChangeNotification];
}
2018-03-08 19:38:42 +01:00
- (void)backupJobDidUpdate:(OWSBackupJob *)backupJob
description:(nullable NSString *)description
progress:(nullable NSNumber *)progress
2018-03-08 15:52:57 +01:00
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
2018-03-08 15:52:57 +01:00
2018-03-08 19:38:42 +01:00
// TODO: Should we consolidate this state?
BOOL didChange;
2018-03-12 21:56:12 +01:00
if (self.backupImportJob == backupJob) {
2018-03-08 19:38:42 +01:00
didChange = !([NSObject isNullableObject:self.backupImportDescription equalTo:description] &&
[NSObject isNullableObject:self.backupImportProgress equalTo:progress]);
2018-03-08 15:52:57 +01:00
2018-03-08 19:38:42 +01:00
self.backupImportDescription = description;
self.backupImportProgress = progress;
2018-03-12 21:56:12 +01:00
} else if (self.backupExportJob == backupJob) {
2018-03-08 19:38:42 +01:00
didChange = !([NSObject isNullableObject:self.backupExportDescription equalTo:description] &&
[NSObject isNullableObject:self.backupExportProgress equalTo:progress]);
2018-03-08 15:52:57 +01:00
2018-03-08 19:38:42 +01:00
self.backupExportDescription = description;
self.backupExportProgress = progress;
} else {
2018-03-08 15:52:57 +01:00
return;
}
2018-03-08 16:10:43 +01:00
if (didChange) {
[self postDidChangeNotification];
}
2018-03-08 15:52:57 +01:00
}
2018-03-12 20:10:37 +01:00
- (void)logBackupRecords
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
2018-03-12 20:10:37 +01:00
if (!self.tsAccountManager.isRegisteredAndReady) {
OWSLogError(@"Can't interact with backup; not registered and ready.");
return;
}
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
if (recipientId.length < 1) {
OWSLogError(@"Can't interact with backup; missing recipientId.");
return;
}
[OWSBackupAPI fetchAllRecordNamesWithRecipientId:recipientId
success:^(NSArray<NSString *> *recordNames) {
for (NSString *recordName in [recordNames sortedArrayUsingSelector:@selector(compare:)]) {
OWSLogInfo(@"\t %@", recordName);
}
OWSLogInfo(@"record count: %zd", recordNames.count);
2018-03-12 20:10:37 +01:00
}
failure:^(NSError *error) {
OWSLogError(@"Failed to retrieve backup records: %@", error);
2018-03-12 20:10:37 +01:00
}];
}
2018-03-17 21:29:57 +01:00
- (void)clearAllCloudKitRecords
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
2018-03-17 21:29:57 +01:00
if (!self.tsAccountManager.isRegisteredAndReady) {
OWSLogError(@"Can't interact with backup; not registered and ready.");
return;
}
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
if (recipientId.length < 1) {
OWSLogError(@"Can't interact with backup; missing recipientId.");
return;
}
[OWSBackupAPI fetchAllRecordNamesWithRecipientId:recipientId
success:^(NSArray<NSString *> *recordNames) {
if (recordNames.count < 1) {
OWSLogInfo(@"No CloudKit records found to clear.");
return;
2018-03-17 21:29:57 +01:00
}
[OWSBackupAPI deleteRecordsFromCloudWithRecordNames:recordNames
success:^{
OWSLogInfo(@"Clear all CloudKit records succeeded.");
}
failure:^(NSError *error) {
OWSLogError(@"Clear all CloudKit records failed: %@.", error);
}];
}
2018-03-17 21:29:57 +01:00
failure:^(NSError *error) {
OWSLogError(@"Failed to retrieve CloudKit records: %@", error);
2018-03-17 21:29:57 +01:00
}];
}
2018-03-20 22:22:19 +01:00
#pragma mark - Lazy Restore
- (NSArray<NSString *> *)attachmentRecordNamesForLazyRestore
2018-03-20 22:22:19 +01:00
{
NSMutableArray<NSString *> *recordNames = [NSMutableArray new];
2018-03-20 22:22:19 +01:00
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
2018-03-22 19:05:12 +01:00
id ext = [transaction ext:TSLazyRestoreAttachmentsDatabaseViewExtensionName];
2018-03-20 22:22:19 +01:00
if (!ext) {
OWSFailDebug(@"Could not load database view.");
2018-03-20 22:22:19 +01:00
return;
}
[ext enumerateKeysAndObjectsInGroup:TSLazyRestoreAttachmentsGroup
usingBlock:^(
NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
if (![object isKindOfClass:[TSAttachmentPointer class]]) {
OWSFailDebug(
@"Unexpected object: %@ in collection:%@", [object class], collection);
2018-03-20 22:22:19 +01:00
return;
}
TSAttachmentPointer *attachmentPointer = object;
if (!attachmentPointer.lazyRestoreFragment) {
OWSFailDebug(
@"Invalid object: %@ in collection:%@", [object class], collection);
return;
}
[recordNames addObject:attachmentPointer.lazyRestoreFragment.recordName];
2018-03-20 22:22:19 +01:00
}];
}];
return recordNames;
2018-03-20 22:22:19 +01:00
}
- (NSArray<NSString *> *)attachmentIdsForLazyRestore
{
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
2018-03-22 19:05:12 +01:00
id ext = [transaction ext:TSLazyRestoreAttachmentsDatabaseViewExtensionName];
2018-03-20 22:22:19 +01:00
if (!ext) {
OWSFailDebug(@"Could not load database view.");
2018-03-20 22:22:19 +01:00
return;
}
2018-11-28 21:55:18 +01:00
2018-03-20 22:22:19 +01:00
[ext enumerateKeysInGroup:TSLazyRestoreAttachmentsGroup
usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) {
[attachmentIds addObject:key];
}];
}];
return attachmentIds;
}
2018-11-28 21:49:45 +01:00
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO
2018-03-20 22:22:19 +01:00
{
OWSAssertDebug(attachment);
OWSAssertDebug(backupIO);
2018-03-20 22:22:19 +01:00
OWSBackupFragment *_Nullable lazyRestoreFragment = attachment.lazyRestoreFragment;
if (!lazyRestoreFragment) {
2018-11-28 21:49:45 +01:00
OWSLogError(@"Attachment missing lazy restore metadata.");
return
[AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment missing lazy restore metadata.")];
}
if (lazyRestoreFragment.recordName.length < 1 || lazyRestoreFragment.encryptionKey.length < 1) {
OWSLogError(@"Incomplete lazy restore metadata.");
2018-11-28 21:49:45 +01:00
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Incomplete lazy restore metadata.")];
2018-03-20 22:22:19 +01:00
}
// Use a predictable file path so that multiple "import backup" attempts
// will leverage successful file downloads from previous attempts.
//
// TODO: This will also require imports using a predictable jobTempDirPath.
2018-03-22 19:05:12 +01:00
NSString *tempFilePath = [backupIO generateTempFilePath];
2018-03-20 22:22:19 +01:00
2018-11-28 21:49:45 +01:00
return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:lazyRestoreFragment.recordName
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
.thenInBackground(^{
return [self lazyRestoreAttachment:attachment
backupIO:backupIO
encryptedFilePath:tempFilePath
encryptionKey:lazyRestoreFragment.encryptionKey];
});
2018-03-20 22:22:19 +01:00
}
2018-11-28 21:49:45 +01:00
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachmentPointer
backupIO:(OWSBackupIO *)backupIO
encryptedFilePath:(NSString *)encryptedFilePath
encryptionKey:(NSData *)encryptionKey
2018-03-20 22:22:19 +01:00
{
OWSAssertDebug(attachmentPointer);
OWSAssertDebug(backupIO);
OWSAssertDebug(encryptedFilePath.length > 0);
OWSAssertDebug(encryptionKey.length > 0);
2018-03-20 22:22:19 +01:00
NSData *_Nullable data = [NSData dataWithContentsOfFile:encryptedFilePath];
if (!data) {
OWSLogError(@"Could not load encrypted file.");
2018-11-28 21:49:45 +01:00
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load encrypted file.")];
2018-03-20 22:22:19 +01:00
}
2018-03-22 19:05:12 +01:00
NSString *decryptedFilePath = [backupIO generateTempFilePath];
2018-03-20 22:22:19 +01:00
2018-03-22 18:33:34 +01:00
@autoreleasepool {
if (![backupIO decryptFileAsFile:encryptedFilePath dstFilePath:decryptedFilePath encryptionKey:encryptionKey]) {
OWSLogError(@"Could not load decrypt file.");
2018-11-28 21:49:45 +01:00
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load decrypt file.")];
2018-03-22 18:33:34 +01:00
}
2018-03-20 22:22:19 +01:00
}
TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithPointer:attachmentPointer];
NSString *attachmentFilePath = stream.originalFilePath;
2018-03-20 22:22:19 +01:00
if (attachmentFilePath.length < 1) {
OWSLogError(@"Attachment has invalid file path.");
2018-11-28 21:49:45 +01:00
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment has invalid file path.")];
2018-03-20 22:22:19 +01:00
}
2018-03-22 19:05:12 +01:00
NSString *attachmentDirPath = [attachmentFilePath stringByDeletingLastPathComponent];
if (![OWSFileSystem ensureDirectoryExists:attachmentDirPath]) {
OWSLogError(@"Couldn't create directory for attachment file.");
2018-11-28 21:49:45 +01:00
return [AnyPromise
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't create directory for attachment file.")];
2018-03-22 19:05:12 +01:00
}
if (![OWSFileSystem deleteFileIfExists:attachmentFilePath]) {
2018-11-21 18:25:24 +01:00
OWSFailDebug(@"Couldn't delete existing file at attachment path.");
2018-11-28 21:49:45 +01:00
return [AnyPromise
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't delete existing file at attachment path.")];
}
2018-03-20 22:22:19 +01:00
NSError *error;
BOOL success =
[NSFileManager.defaultManager moveItemAtPath:decryptedFilePath toPath:attachmentFilePath error:&error];
if (!success || error) {
OWSLogError(@"Attachment file could not be restored: %@.", error);
2018-11-28 21:49:45 +01:00
return [AnyPromise promiseWithValue:error];
2018-03-20 22:22:19 +01:00
}
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// This should overwrite the attachment pointer with an attachment stream.
[stream saveWithTransaction:transaction];
2020-11-04 01:46:30 +01:00
}];
2018-03-20 22:22:19 +01:00
2018-11-28 21:49:45 +01:00
return [AnyPromise promiseWithValue:@(1)];
2018-03-20 22:22:19 +01:00
}
2018-11-30 18:05:18 +01:00
- (void)logBackupMetadataCache:(YapDatabaseConnection *)dbConnection
{
OWSLogInfo(@"");
[dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[transaction enumerateKeysAndObjectsInCollection:[OWSBackupFragment collection]
usingBlock:^(NSString *key, OWSBackupFragment *fragment, BOOL *stop) {
OWSLogVerbose(@"fragment: %@, %@, %lu, %@, %@, %@, %@",
key,
fragment.recordName,
(unsigned long)fragment.encryptionKey.length,
fragment.relativeFilePath,
fragment.attachmentId,
fragment.downloadFilePath,
fragment.uncompressedDataLength);
}];
OWSLogVerbose(@"Number of fragments: %lu",
(unsigned long)[transaction numberOfKeysInCollection:[OWSBackupFragment collection]]);
}];
}
2018-03-08 15:52:57 +01:00
#pragma mark - Notifications
- (void)postDidChangeNotification
{
2018-03-08 16:10:43 +01:00
OWSAssertIsOnMainThread();
2018-03-08 15:52:57 +01:00
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationNameBackupStateDidChange
object:nil
userInfo:nil];
}
2018-01-04 21:16:24 +01:00
@end
NS_ASSUME_NONNULL_END