mirror of https://github.com/oxen-io/session-ios
Merge branch 'dev' into feature/session-id-blinding-part-2
# Conflicts: # SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift # SessionMessagingKit/Storage.swiftpull/592/head
commit
93f248d149
@ -1,175 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
public class BackupRestoreViewController: OWSTableViewController {
|
||||
|
||||
private var hasBegunImport = false
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var backup: OWSBackup {
|
||||
return AppEnvironment.shared.backup
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
override public func loadView() {
|
||||
super.loadView()
|
||||
|
||||
navigationItem.title = NSLocalizedString("SETTINGS_BACKUP", comment: "Label for the backup view in app settings.")
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didPressCancelButton))
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(backupStateDidChange),
|
||||
name: NSNotification.Name(NSNotificationNameBackupStateDidChange),
|
||||
object: nil)
|
||||
|
||||
updateTableContents()
|
||||
}
|
||||
|
||||
private func updateTableContents() {
|
||||
if hasBegunImport {
|
||||
updateProgressContents()
|
||||
} else {
|
||||
updateDecisionContents()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateDecisionContents() {
|
||||
let contents = OWSTableContents()
|
||||
|
||||
let section = OWSTableSection()
|
||||
|
||||
section.headerTitle = NSLocalizedString("BACKUP_RESTORE_DECISION_TITLE", comment: "Label for the backup restore decision section.")
|
||||
|
||||
section.add(OWSTableItem.actionItem(withText: NSLocalizedString("CHECK_FOR_BACKUP_DO_NOT_RESTORE",
|
||||
comment: "The label for the 'do not restore backup' button."), actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.cancelAndDismiss()
|
||||
}))
|
||||
section.add(OWSTableItem.actionItem(withText: NSLocalizedString("CHECK_FOR_BACKUP_RESTORE",
|
||||
comment: "The label for the 'restore backup' button."), actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.startImport()
|
||||
}))
|
||||
|
||||
contents.addSection(section)
|
||||
self.contents = contents
|
||||
}
|
||||
|
||||
private var progressFormatter: NumberFormatter = {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .percent
|
||||
numberFormatter.maximumFractionDigits = 0
|
||||
numberFormatter.multiplier = 1
|
||||
return numberFormatter
|
||||
}()
|
||||
|
||||
private func updateProgressContents() {
|
||||
let contents = OWSTableContents()
|
||||
|
||||
let section = OWSTableSection()
|
||||
|
||||
section.add(OWSTableItem.label(withText: NSLocalizedString("BACKUP_RESTORE_STATUS", comment: "Label for the backup restore status."), accessoryText: NSStringForBackupImportState(backup.backupImportState)))
|
||||
|
||||
if backup.backupImportState == .inProgress {
|
||||
if let backupImportDescription = backup.backupImportDescription {
|
||||
section.add(OWSTableItem.label(withText: NSLocalizedString("BACKUP_RESTORE_DESCRIPTION", comment: "Label for the backup restore description."), accessoryText: backupImportDescription))
|
||||
}
|
||||
|
||||
if let backupImportProgress = backup.backupImportProgress {
|
||||
let progressInt = backupImportProgress.floatValue * 100
|
||||
if let progressString = progressFormatter.string(from: NSNumber(value: progressInt)) {
|
||||
section.add(OWSTableItem.label(withText: NSLocalizedString("BACKUP_RESTORE_PROGRESS", comment: "Label for the backup restore progress."), accessoryText: progressString))
|
||||
} else {
|
||||
owsFailDebug("Could not format progress: \(progressInt)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contents.addSection(section)
|
||||
self.contents = contents
|
||||
|
||||
// TODO: Add cancel button.
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
@objc
|
||||
private func didPressCancelButton(sender: UIButton) {
|
||||
Logger.info("")
|
||||
|
||||
// TODO: Cancel import.
|
||||
|
||||
cancelAndDismiss()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func cancelAndDismiss() {
|
||||
Logger.info("")
|
||||
|
||||
backup.setHasPendingRestoreDecision(false)
|
||||
|
||||
showHomeView()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func startImport() {
|
||||
Logger.info("")
|
||||
|
||||
hasBegunImport = true
|
||||
|
||||
backup.tryToImport()
|
||||
}
|
||||
|
||||
private func showHomeView() {
|
||||
// In production, this view will never be presented in a modal.
|
||||
// During testing (debug UI, etc.), it may be a modal.
|
||||
let isModal = navigationController?.presentingViewController != nil
|
||||
if isModal {
|
||||
dismiss(animated: true, completion: {
|
||||
SignalApp.shared().showHomeView()
|
||||
})
|
||||
} else {
|
||||
SignalApp.shared().showHomeView()
|
||||
}
|
||||
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc func backupStateDidChange() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.verbose("backup.backupImportState: \(NSStringForBackupImportState(backup.backupImportState))")
|
||||
Logger.flush()
|
||||
|
||||
if backup.backupImportState == .succeeded {
|
||||
backup.setHasPendingRestoreDecision(false)
|
||||
|
||||
showHomeView()
|
||||
} else {
|
||||
updateTableContents()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Orientation
|
||||
|
||||
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const NSNotificationNameBackupStateDidChange;
|
||||
|
||||
typedef void (^OWSBackupBoolBlock)(BOOL value);
|
||||
typedef void (^OWSBackupStringListBlock)(NSArray<NSString *> *value);
|
||||
typedef void (^OWSBackupErrorBlock)(NSError *error);
|
||||
|
||||
typedef NS_ENUM(NSUInteger, OWSBackupState) {
|
||||
// Has never backed up, not trying to backup yet.
|
||||
OWSBackupState_Idle = 0,
|
||||
// Backing up.
|
||||
OWSBackupState_InProgress,
|
||||
// Last backup failed.
|
||||
OWSBackupState_Failed,
|
||||
// Last backup succeeded.
|
||||
OWSBackupState_Succeeded,
|
||||
};
|
||||
|
||||
NSString *NSStringForBackupExportState(OWSBackupState state);
|
||||
NSString *NSStringForBackupImportState(OWSBackupState state);
|
||||
|
||||
NSArray<NSString *> *MiscCollectionsToBackup(void);
|
||||
|
||||
NSError *OWSBackupErrorWithDescription(NSString *description);
|
||||
|
||||
@class AnyPromise;
|
||||
@class OWSBackupIO;
|
||||
@class TSAttachmentPointer;
|
||||
@class TSThread;
|
||||
@class YapDatabaseConnection;
|
||||
|
||||
@interface OWSBackup : NSObject
|
||||
|
||||
- (instancetype)init NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
+ (instancetype)sharedManager NS_SWIFT_NAME(shared());
|
||||
|
||||
#pragma mark - Backup Export
|
||||
|
||||
@property (atomic, readonly) OWSBackupState backupExportState;
|
||||
|
||||
// If a "backup export" is in progress (see backupExportState),
|
||||
// backupExportDescription _might_ contain a string that describes
|
||||
// the current phase and backupExportProgress _might_ contain a
|
||||
// 0.0<=x<=1.0 progress value that indicates progress within the
|
||||
// current phase.
|
||||
@property (nonatomic, readonly, nullable) NSString *backupExportDescription;
|
||||
@property (nonatomic, readonly, nullable) NSNumber *backupExportProgress;
|
||||
|
||||
+ (BOOL)isFeatureEnabled;
|
||||
|
||||
- (BOOL)isBackupEnabled;
|
||||
- (void)setIsBackupEnabled:(BOOL)value;
|
||||
|
||||
- (BOOL)hasPendingRestoreDecision;
|
||||
- (void)setHasPendingRestoreDecision:(BOOL)value;
|
||||
|
||||
- (void)tryToExportBackup;
|
||||
- (void)cancelExportBackup;
|
||||
|
||||
#pragma mark - Backup Import
|
||||
|
||||
@property (atomic, readonly) OWSBackupState backupImportState;
|
||||
|
||||
// If a "backup import" is in progress (see backupImportState),
|
||||
// backupImportDescription _might_ contain a string that describes
|
||||
// the current phase and backupImportProgress _might_ contain a
|
||||
// 0.0<=x<=1.0 progress value that indicates progress within the
|
||||
// current phase.
|
||||
@property (nonatomic, readonly, nullable) NSString *backupImportDescription;
|
||||
@property (nonatomic, readonly, nullable) NSNumber *backupImportProgress;
|
||||
|
||||
- (void)allRecipientIdsWithManifestsInCloud:(OWSBackupStringListBlock)success failure:(OWSBackupErrorBlock)failure;
|
||||
|
||||
- (AnyPromise *)ensureCloudKitAccess __attribute__((warn_unused_result));
|
||||
|
||||
- (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure;
|
||||
|
||||
// TODO: After a successful import, we should enable backup and
|
||||
// preserve our PIN and/or private key so that restored users
|
||||
// continues to backup.
|
||||
- (void)tryToImportBackup;
|
||||
- (void)cancelImportBackup;
|
||||
|
||||
- (void)logBackupRecords;
|
||||
- (void)clearAllCloudKitRecords;
|
||||
|
||||
- (void)logBackupMetadataCache:(YapDatabaseConnection *)dbConnection;
|
||||
|
||||
#pragma mark - Lazy Restore
|
||||
|
||||
- (NSArray<NSString *> *)attachmentRecordNamesForLazyRestore;
|
||||
|
||||
- (NSArray<NSString *> *)attachmentIdsForLazyRestore;
|
||||
|
||||
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO __attribute__((warn_unused_result));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,911 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSBackupExportJob.h"
|
||||
#import "OWSBackupIO.h"
|
||||
#import "OWSBackupImportJob.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/Randomness.h>
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
|
||||
|
||||
@import CloudKit;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const NSNotificationNameBackupStateDidChange = @"NSNotificationNameBackupStateDidChange";
|
||||
|
||||
NSString *const OWSPrimaryStorage_OWSBackupCollection = @"OWSPrimaryStorage_OWSBackupCollection";
|
||||
NSString *const OWSBackup_IsBackupEnabledKey = @"OWSBackup_IsBackupEnabledKey";
|
||||
NSString *const OWSBackup_LastExportSuccessDateKey = @"OWSBackup_LastExportSuccessDateKey";
|
||||
NSString *const OWSBackup_LastExportFailureDateKey = @"OWSBackup_LastExportFailureDateKey";
|
||||
NSString *const OWSBackupErrorDomain = @"OWSBackupErrorDomain";
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSString *> *MiscCollectionsToBackup(void)
|
||||
{
|
||||
return @[
|
||||
OWSUserProfile.collection,
|
||||
SSKIncrementingIdFinder.collectionName,
|
||||
OWSPreferencesSignalDatabaseCollection,
|
||||
];
|
||||
}
|
||||
|
||||
typedef NS_ENUM(NSInteger, OWSBackupErrorCode) {
|
||||
OWSBackupErrorCodeAssertionFailure = 0,
|
||||
};
|
||||
|
||||
NSError *OWSBackupErrorWithDescription(NSString *description)
|
||||
{
|
||||
return [NSError errorWithDomain:@"OWSBackupErrorDomain"
|
||||
code:OWSBackupErrorCodeAssertionFailure
|
||||
userInfo:@{ NSLocalizedDescriptionKey : description }];
|
||||
}
|
||||
|
||||
// TODO: Observe Reachability.
|
||||
@interface OWSBackup () <OWSBackupJobDelegate>
|
||||
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
|
||||
// This property should only be accessed on the main thread.
|
||||
@property (nonatomic, nullable) OWSBackupExportJob *backupExportJob;
|
||||
|
||||
// This property should only be accessed on the main thread.
|
||||
@property (nonatomic, nullable) OWSBackupImportJob *backupImportJob;
|
||||
|
||||
@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;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackup
|
||||
|
||||
@synthesize dbConnection = _dbConnection;
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
OWSAssertDebug(AppEnvironment.shared.backup);
|
||||
|
||||
return AppEnvironment.shared.backup;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
self.backupExportState = OWSBackupState_Idle;
|
||||
self.backupImportState = OWSBackupState_Idle;
|
||||
|
||||
OWSSingletonAssert();
|
||||
|
||||
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
|
||||
[self setup];
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setup
|
||||
{
|
||||
if (!OWSBackup.isFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
[OWSBackupAPI setup];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationDidBecomeActive:)
|
||||
name:OWSApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(registrationStateDidChange)
|
||||
name:RegistrationStateDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(ckAccountChanged)
|
||||
name:CKAccountChangedNotification
|
||||
object:nil];
|
||||
|
||||
// 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];
|
||||
});
|
||||
}
|
||||
|
||||
- (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;
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
||||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
+ (BOOL)isFeatureEnabled
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Backup Export
|
||||
|
||||
- (void)tryToExportBackup
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(!self.backupExportJob);
|
||||
|
||||
if (!self.canBackupExport) {
|
||||
// TODO: Offer a reason in the UI.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self recipientId:recipientId];
|
||||
[self.backupExportJob start];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)cancelExportBackup
|
||||
{
|
||||
[self.backupExportJob cancel];
|
||||
self.backupExportJob = nil;
|
||||
|
||||
[self ensureBackupExportState];
|
||||
}
|
||||
|
||||
- (void)setLastExportSuccessDate:(NSDate *)value
|
||||
{
|
||||
OWSAssertDebug(value);
|
||||
|
||||
[self.dbConnection setDate:value
|
||||
forKey:OWSBackup_LastExportSuccessDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
- (nullable NSDate *)lastExportSuccessDate
|
||||
{
|
||||
return [self.dbConnection dateForKey:OWSBackup_LastExportSuccessDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
- (void)setLastExportFailureDate:(NSDate *)value
|
||||
{
|
||||
OWSAssertDebug(value);
|
||||
|
||||
[self.dbConnection setDate:value
|
||||
forKey:OWSBackup_LastExportFailureDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
|
||||
- (nullable NSDate *)lastExportFailureDate
|
||||
{
|
||||
return [self.dbConnection dateForKey:OWSBackup_LastExportFailureDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
|
||||
[self postDidChangeNotification];
|
||||
|
||||
[self ensureBackupExportState];
|
||||
}
|
||||
|
||||
- (BOOL)hasPendingRestoreDecision
|
||||
{
|
||||
return [self.tsAccountManager hasPendingBackupRestoreDecision];
|
||||
}
|
||||
|
||||
- (void)setHasPendingRestoreDecision:(BOOL)value
|
||||
{
|
||||
[self.tsAccountManager setHasPendingBackupRestoreDecision:value];
|
||||
}
|
||||
|
||||
- (BOOL)canBackupExport
|
||||
{
|
||||
if (!self.isBackupEnabled) {
|
||||
return NO;
|
||||
}
|
||||
if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
|
||||
// Don't start backups when app is in the background.
|
||||
return NO;
|
||||
}
|
||||
if (![self.tsAccountManager isRegisteredAndReady]) {
|
||||
return NO;
|
||||
}
|
||||
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;
|
||||
}
|
||||
NSDate *_Nullable lastExportSuccessDate = self.lastExportSuccessDate;
|
||||
NSDate *_Nullable lastExportFailureDate = self.lastExportFailureDate;
|
||||
// Wait N hours before retrying after a success.
|
||||
const NSTimeInterval kRetryAfterSuccess = 24 * kHourInterval;
|
||||
if (lastExportSuccessDate && fabs(lastExportSuccessDate.timeIntervalSinceNow) < kRetryAfterSuccess) {
|
||||
return NO;
|
||||
}
|
||||
// Wait N hours before retrying after a failure.
|
||||
const NSTimeInterval kRetryAfterFailure = 6 * kHourInterval;
|
||||
if (lastExportFailureDate && fabs(lastExportFailureDate.timeIntervalSinceNow) < kRetryAfterFailure) {
|
||||
return NO;
|
||||
}
|
||||
// Don't export backup if there's an import in progress.
|
||||
//
|
||||
// This conflict shouldn't occur in production since we won't enable backup
|
||||
// export until an import is complete, but this could happen in development.
|
||||
if (self.backupImportJob) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// TODO: There's other conditions that affect this decision,
|
||||
// e.g. Reachability, wifi v. cellular, etc.
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)ensureBackupExportState
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
if (!OWSBackup.isFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Start or abort a backup export if neccessary.
|
||||
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];
|
||||
[self.backupExportJob start];
|
||||
}
|
||||
|
||||
// Update the state flag.
|
||||
OWSBackupState backupExportState = OWSBackupState_Idle;
|
||||
if (self.backupExportJob) {
|
||||
backupExportState = OWSBackupState_InProgress;
|
||||
} else {
|
||||
NSDate *_Nullable lastExportSuccessDate = self.lastExportSuccessDate;
|
||||
NSDate *_Nullable lastExportFailureDate = self.lastExportFailureDate;
|
||||
if (!lastExportSuccessDate && !lastExportFailureDate) {
|
||||
backupExportState = OWSBackupState_Idle;
|
||||
} else if (lastExportSuccessDate && lastExportFailureDate) {
|
||||
backupExportState = ([lastExportSuccessDate isAfterDate:lastExportFailureDate] ? OWSBackupState_Succeeded
|
||||
: OWSBackupState_Failed);
|
||||
} else if (lastExportSuccessDate) {
|
||||
backupExportState = OWSBackupState_Succeeded;
|
||||
} else if (lastExportFailureDate) {
|
||||
backupExportState = OWSBackupState_Failed;
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected condition.");
|
||||
}
|
||||
}
|
||||
|
||||
BOOL stateDidChange = self.backupExportState != backupExportState;
|
||||
self.backupExportState = backupExportState;
|
||||
if (stateDidChange) {
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
}
|
||||
|
||||
#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);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (AnyPromise *)ensureCloudKitAccess
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
return [OWSBackupAPI ensureCloudKitAccessObjc];
|
||||
}
|
||||
|
||||
- (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
[[OWSBackupAPI ensureCloudKitAccessObjc]
|
||||
.thenInBackground(^{
|
||||
return [OWSBackupAPI checkForManifestInCloudObjcWithRecipientId:recipientId];
|
||||
})
|
||||
.then(^(NSNumber *value) {
|
||||
success(value.boolValue);
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
failure(error);
|
||||
}) retainUntilComplete];
|
||||
}
|
||||
|
||||
- (void)tryToImportBackup
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(!self.backupImportJob);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.backupImportState = OWSBackupState_InProgress;
|
||||
|
||||
self.backupImportJob = [[OWSBackupImportJob alloc] initWithDelegate:self recipientId:recipientId];
|
||||
[self.backupImportJob start];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)cancelImportBackup
|
||||
{
|
||||
[self.backupImportJob cancel];
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupImportState = OWSBackupState_Idle;
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self ensureBackupExportState];
|
||||
}
|
||||
|
||||
- (void)registrationStateDidChange
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self ensureBackupExportState];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)ckAccountChanged
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self ensureBackupExportState];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - OWSBackupJobDelegate
|
||||
|
||||
// We use a delegate method to avoid storing this key in memory.
|
||||
- (nullable NSData *)backupEncryptionKey
|
||||
{
|
||||
// TODO: Use actual encryption key.
|
||||
return [@"temp" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (void)backupJobDidSucceed:(OWSBackupJob *)backupJob
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@".");
|
||||
|
||||
if (self.backupImportJob == backupJob) {
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupImportState = OWSBackupState_Succeeded;
|
||||
} else if (self.backupExportJob == backupJob) {
|
||||
self.backupExportJob = nil;
|
||||
|
||||
[self setLastExportSuccessDate:[NSDate new]];
|
||||
|
||||
[self ensureBackupExportState];
|
||||
} else {
|
||||
OWSLogWarn(@"obsolete job succeeded: %@", [backupJob class]);
|
||||
return;
|
||||
}
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)backupJobDidFail:(OWSBackupJob *)backupJob error:(NSError *)error
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@": %@", error);
|
||||
|
||||
if (self.backupImportJob == backupJob) {
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupImportState = OWSBackupState_Failed;
|
||||
} else if (self.backupExportJob == backupJob) {
|
||||
self.backupExportJob = nil;
|
||||
|
||||
[self setLastExportFailureDate:[NSDate new]];
|
||||
|
||||
[self ensureBackupExportState];
|
||||
} else {
|
||||
OWSLogInfo(@"obsolete backup job failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)backupJobDidUpdate:(OWSBackupJob *)backupJob
|
||||
description:(nullable NSString *)description
|
||||
progress:(nullable NSNumber *)progress
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
// TODO: Should we consolidate this state?
|
||||
BOOL didChange;
|
||||
if (self.backupImportJob == backupJob) {
|
||||
didChange = !([NSObject isNullableObject:self.backupImportDescription equalTo:description] &&
|
||||
[NSObject isNullableObject:self.backupImportProgress equalTo:progress]);
|
||||
|
||||
self.backupImportDescription = description;
|
||||
self.backupImportProgress = progress;
|
||||
} else if (self.backupExportJob == backupJob) {
|
||||
didChange = !([NSObject isNullableObject:self.backupExportDescription equalTo:description] &&
|
||||
[NSObject isNullableObject:self.backupExportProgress equalTo:progress]);
|
||||
|
||||
self.backupExportDescription = description;
|
||||
self.backupExportProgress = progress;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (didChange) {
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)logBackupRecords
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
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);
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Failed to retrieve backup records: %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)clearAllCloudKitRecords
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
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;
|
||||
}
|
||||
[OWSBackupAPI deleteRecordsFromCloudWithRecordNames:recordNames
|
||||
success:^{
|
||||
OWSLogInfo(@"Clear all CloudKit records succeeded.");
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Clear all CloudKit records failed: %@.", error);
|
||||
}];
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Failed to retrieve CloudKit records: %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Restore
|
||||
|
||||
- (NSArray<NSString *> *)attachmentRecordNamesForLazyRestore
|
||||
{
|
||||
NSMutableArray<NSString *> *recordNames = [NSMutableArray new];
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
id ext = [transaction ext:TSLazyRestoreAttachmentsDatabaseViewExtensionName];
|
||||
if (!ext) {
|
||||
OWSFailDebug(@"Could not load database view.");
|
||||
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);
|
||||
return;
|
||||
}
|
||||
TSAttachmentPointer *attachmentPointer = object;
|
||||
if (!attachmentPointer.lazyRestoreFragment) {
|
||||
OWSFailDebug(
|
||||
@"Invalid object: %@ in collection:%@", [object class], collection);
|
||||
return;
|
||||
}
|
||||
[recordNames addObject:attachmentPointer.lazyRestoreFragment.recordName];
|
||||
}];
|
||||
}];
|
||||
return recordNames;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)attachmentIdsForLazyRestore
|
||||
{
|
||||
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
id ext = [transaction ext:TSLazyRestoreAttachmentsDatabaseViewExtensionName];
|
||||
if (!ext) {
|
||||
OWSFailDebug(@"Could not load database view.");
|
||||
return;
|
||||
}
|
||||
|
||||
[ext enumerateKeysInGroup:TSLazyRestoreAttachmentsGroup
|
||||
usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) {
|
||||
[attachmentIds addObject:key];
|
||||
}];
|
||||
}];
|
||||
return attachmentIds;
|
||||
}
|
||||
|
||||
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO
|
||||
{
|
||||
OWSAssertDebug(attachment);
|
||||
OWSAssertDebug(backupIO);
|
||||
|
||||
OWSBackupFragment *_Nullable lazyRestoreFragment = attachment.lazyRestoreFragment;
|
||||
if (!lazyRestoreFragment) {
|
||||
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.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Incomplete lazy restore metadata.")];
|
||||
}
|
||||
|
||||
// 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.
|
||||
NSString *tempFilePath = [backupIO generateTempFilePath];
|
||||
|
||||
return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:lazyRestoreFragment.recordName
|
||||
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
|
||||
.thenInBackground(^{
|
||||
return [self lazyRestoreAttachment:attachment
|
||||
backupIO:backupIO
|
||||
encryptedFilePath:tempFilePath
|
||||
encryptionKey:lazyRestoreFragment.encryptionKey];
|
||||
});
|
||||
}
|
||||
|
||||
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachmentPointer
|
||||
backupIO:(OWSBackupIO *)backupIO
|
||||
encryptedFilePath:(NSString *)encryptedFilePath
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(attachmentPointer);
|
||||
OWSAssertDebug(backupIO);
|
||||
OWSAssertDebug(encryptedFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:encryptedFilePath];
|
||||
if (!data) {
|
||||
OWSLogError(@"Could not load encrypted file.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load encrypted file.")];
|
||||
}
|
||||
|
||||
NSString *decryptedFilePath = [backupIO generateTempFilePath];
|
||||
|
||||
@autoreleasepool {
|
||||
if (![backupIO decryptFileAsFile:encryptedFilePath dstFilePath:decryptedFilePath encryptionKey:encryptionKey]) {
|
||||
OWSLogError(@"Could not load decrypt file.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load decrypt file.")];
|
||||
}
|
||||
}
|
||||
|
||||
TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithPointer:attachmentPointer];
|
||||
|
||||
NSString *attachmentFilePath = stream.originalFilePath;
|
||||
if (attachmentFilePath.length < 1) {
|
||||
OWSLogError(@"Attachment has invalid file path.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment has invalid file path.")];
|
||||
}
|
||||
|
||||
NSString *attachmentDirPath = [attachmentFilePath stringByDeletingLastPathComponent];
|
||||
if (![OWSFileSystem ensureDirectoryExists:attachmentDirPath]) {
|
||||
OWSLogError(@"Couldn't create directory for attachment file.");
|
||||
return [AnyPromise
|
||||
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't create directory for attachment file.")];
|
||||
}
|
||||
|
||||
if (![OWSFileSystem deleteFileIfExists:attachmentFilePath]) {
|
||||
OWSFailDebug(@"Couldn't delete existing file at attachment path.");
|
||||
return [AnyPromise
|
||||
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't delete existing file at attachment path.")];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
BOOL success =
|
||||
[NSFileManager.defaultManager moveItemAtPath:decryptedFilePath toPath:attachmentFilePath error:&error];
|
||||
if (!success || error) {
|
||||
OWSLogError(@"Attachment file could not be restored: %@.", error);
|
||||
return [AnyPromise promiseWithValue:error];
|
||||
}
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// This should overwrite the attachment pointer with an attachment stream.
|
||||
[stream saveWithTransaction:transaction];
|
||||
}];
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (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]]);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)postDidChangeNotification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationNameBackupStateDidChange
|
||||
object:nil
|
||||
userInfo:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,740 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import CloudKit
|
||||
import PromiseKit
|
||||
|
||||
// We don't worry about atomic writes. Each backup export
|
||||
// will diff against last successful backup.
|
||||
//
|
||||
// Note that all of our CloudKit records are immutable.
|
||||
// "Persistent" records are only uploaded once.
|
||||
// "Ephemeral" records are always uploaded to a new record name.
|
||||
@objc public class OWSBackupAPI: NSObject {
|
||||
|
||||
// If we change the record types, we need to ensure indices
|
||||
// are configured properly in the CloudKit dashboard.
|
||||
//
|
||||
// TODO: Change the record types when we ship to production.
|
||||
static let signalBackupRecordType = "signalBackup"
|
||||
static let manifestRecordNameSuffix = "manifest"
|
||||
static let payloadKey = "payload"
|
||||
static let maxRetries = 5
|
||||
|
||||
private class func database() -> CKDatabase {
|
||||
let myContainer = CKContainer.default()
|
||||
let privateDatabase = myContainer.privateCloudDatabase
|
||||
return privateDatabase
|
||||
}
|
||||
|
||||
private class func invalidServiceResponseError() -> Error {
|
||||
return OWSErrorWithCodeDescription(.backupFailure,
|
||||
NSLocalizedString("BACKUP_EXPORT_ERROR_INVALID_CLOUDKIT_RESPONSE",
|
||||
comment: "Error indicating that the app received an invalid response from CloudKit."))
|
||||
}
|
||||
|
||||
// MARK: - Upload
|
||||
|
||||
@objc
|
||||
public class func recordNameForTestFile(recipientId: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))test-\(NSUUID().uuidString)"
|
||||
}
|
||||
|
||||
// "Ephemeral" files are specific to this backup export and will always need to
|
||||
// be saved. For example, a complete image of the database is exported each time.
|
||||
// We wouldn't want to overwrite previous images until the entire backup export is
|
||||
// complete.
|
||||
@objc
|
||||
public class func recordNameForEphemeralFile(recipientId: String,
|
||||
label: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))ephemeral-\(label)-\(NSUUID().uuidString)"
|
||||
}
|
||||
|
||||
// "Persistent" files may be shared between backup export; they should only be saved
|
||||
// once. For example, attachment files should only be uploaded once. Subsequent
|
||||
// backups can reuse the same record.
|
||||
@objc
|
||||
public class func recordNameForPersistentFile(recipientId: String,
|
||||
fileId: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))persistentFile-\(fileId)"
|
||||
}
|
||||
|
||||
// "Persistent" files may be shared between backup export; they should only be saved
|
||||
// once. For example, attachment files should only be uploaded once. Subsequent
|
||||
// backups can reuse the same record.
|
||||
@objc
|
||||
public class func recordNameForManifest(recipientId: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))\(manifestRecordNameSuffix)"
|
||||
}
|
||||
|
||||
private class func isManifest(recordName: String) -> Bool {
|
||||
return recordName.hasSuffix(manifestRecordNameSuffix)
|
||||
}
|
||||
|
||||
private class func recordNamePrefix(forRecipientId recipientId: String) -> String {
|
||||
return "\(recipientId)-"
|
||||
}
|
||||
|
||||
private class func recipientId(forRecordName recordName: String) -> String? {
|
||||
let recipientIds = self.recipientIds(forRecordNames: [recordName])
|
||||
guard let recipientId = recipientIds.first else {
|
||||
return nil
|
||||
}
|
||||
return recipientId
|
||||
}
|
||||
|
||||
private static var recordNamePrefixRegex = {
|
||||
return try! NSRegularExpression(pattern: "^(\\+[0-9]+)\\-")
|
||||
}()
|
||||
|
||||
private class func recipientIds(forRecordNames recordNames: [String]) -> [String] {
|
||||
var recipientIds = [String]()
|
||||
for recordName in recordNames {
|
||||
let regex = recordNamePrefixRegex
|
||||
guard let match: NSTextCheckingResult = regex.firstMatch(in: recordName, options: [], range: NSRange(location: 0, length: recordName.utf16.count)) else {
|
||||
Logger.warn("no match: \(recordName)")
|
||||
continue
|
||||
}
|
||||
guard match.numberOfRanges > 0 else {
|
||||
// Match must include first group.
|
||||
Logger.warn("invalid match: \(recordName)")
|
||||
continue
|
||||
}
|
||||
let firstRange = match.range(at: 1)
|
||||
guard firstRange.location == 0,
|
||||
firstRange.length > 0 else {
|
||||
// Match must be at start of string and non-empty.
|
||||
Logger.warn("invalid match: \(recordName) \(firstRange)")
|
||||
continue
|
||||
}
|
||||
let recipientId = (recordName as NSString).substring(with: firstRange) as String
|
||||
recipientIds.append(recipientId)
|
||||
}
|
||||
return recipientIds
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func record(forFileUrl fileUrl: URL,
|
||||
recordName: String) -> CKRecord {
|
||||
let recordType = signalBackupRecordType
|
||||
let recordID = CKRecord.ID(recordName: recordName)
|
||||
let record = CKRecord(recordType: recordType, recordID: recordID)
|
||||
let asset = CKAsset(fileURL: fileUrl)
|
||||
record[payloadKey] = asset
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func saveRecordsToCloudObjc(records: [CKRecord]) -> AnyPromise {
|
||||
return AnyPromise(saveRecordsToCloud(records: records))
|
||||
}
|
||||
|
||||
public class func saveRecordsToCloud(records: [CKRecord]) -> Promise<Void> {
|
||||
|
||||
// CloudKit's internal limit is 400, but I haven't found a constant for this.
|
||||
let kMaxBatchSize = 100
|
||||
return records.chunked(by: kMaxBatchSize).reduce(Promise.value(())) { (promise, batch) -> Promise<Void> in
|
||||
return promise.then(on: .global()) {
|
||||
saveRecordsToCloud(records: batch, remainingRetries: maxRetries)
|
||||
}.done {
|
||||
Logger.verbose("Saved batch: \(batch.count)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class func saveRecordsToCloud(records: [CKRecord],
|
||||
remainingRetries: Int) -> Promise<Void> {
|
||||
|
||||
let recordNames = records.map { (record) in
|
||||
return record.recordID.recordName
|
||||
}
|
||||
Logger.verbose("recordNames[\(recordNames.count)] \(recordNames[0..<10])...")
|
||||
|
||||
return Promise { resolver in
|
||||
let saveOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
|
||||
saveOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, _, error) in
|
||||
|
||||
let retry = {
|
||||
// Only retry records which didn't already succeed.
|
||||
var savedRecordNames = [String]()
|
||||
if let savedRecords = savedRecords {
|
||||
savedRecordNames = savedRecords.map { (record) in
|
||||
return record.recordID.recordName
|
||||
}
|
||||
}
|
||||
let retryRecords = records.filter({ (record) in
|
||||
return !savedRecordNames.contains(record.recordID.recordName)
|
||||
})
|
||||
|
||||
saveRecordsToCloud(records: retryRecords,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { _ in
|
||||
resolver.fulfill(())
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Save Records[\(recordNames.count)]")
|
||||
switch outcome {
|
||||
case .success:
|
||||
resolver.fulfill(())
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
resolver.reject(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
retry()
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
retry()
|
||||
}
|
||||
case .unknownItem:
|
||||
owsFailDebug("unexpected CloudKit response.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
saveOperation.isAtomic = false
|
||||
saveOperation.savePolicy = .allKeys
|
||||
|
||||
// TODO: use perRecordProgressBlock and perRecordCompletionBlock.
|
||||
// open var perRecordProgressBlock: ((CKRecord, Double) -> Void)?
|
||||
// open var perRecordCompletionBlock: ((CKRecord, Error?) -> Void)?
|
||||
|
||||
// These APIs are only available in iOS 9.3 and later.
|
||||
if #available(iOS 9.3, *) {
|
||||
saveOperation.isLongLived = true
|
||||
saveOperation.qualityOfService = .background
|
||||
}
|
||||
|
||||
database().add(saveOperation)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete
|
||||
|
||||
@objc
|
||||
public class func deleteRecordsFromCloud(recordNames: [String],
|
||||
success: @escaping () -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
deleteRecordsFromCloud(recordNames: recordNames,
|
||||
remainingRetries: maxRetries,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
private class func deleteRecordsFromCloud(recordNames: [String],
|
||||
remainingRetries: Int,
|
||||
success: @escaping () -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
let recordIDs = recordNames.map { CKRecord.ID(recordName: $0) }
|
||||
let deleteOperation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDs)
|
||||
deleteOperation.modifyRecordsCompletionBlock = { (records, recordIds, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Delete Records")
|
||||
switch outcome {
|
||||
case .success:
|
||||
success()
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
failure(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
deleteRecordsFromCloud(recordNames: recordNames,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
deleteRecordsFromCloud(recordNames: recordNames,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
case .unknownItem:
|
||||
owsFailDebug("unexpected CloudKit response.")
|
||||
failure(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
database().add(deleteOperation)
|
||||
}
|
||||
|
||||
// MARK: - Exists?
|
||||
|
||||
private class func checkForFileInCloud(recordName: String,
|
||||
remainingRetries: Int) -> Promise<CKRecord?> {
|
||||
|
||||
Logger.verbose("checkForFileInCloud \(recordName)")
|
||||
|
||||
let (promise, resolver) = Promise<CKRecord?>.pending()
|
||||
|
||||
let recordId = CKRecord.ID(recordName: recordName)
|
||||
let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ])
|
||||
// Don't download the file; we're just using the fetch to check whether or
|
||||
// not this record already exists.
|
||||
fetchOperation.desiredKeys = []
|
||||
fetchOperation.perRecordCompletionBlock = { (record, recordId, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Check for Record")
|
||||
switch outcome {
|
||||
case .success:
|
||||
guard let record = record else {
|
||||
owsFailDebug("missing fetching record.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
return
|
||||
}
|
||||
// Record found.
|
||||
resolver.fulfill(record)
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
resolver.reject(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
checkForFileInCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (record) in
|
||||
resolver.fulfill(record)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
checkForFileInCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (record) in
|
||||
resolver.fulfill(record)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
case .unknownItem:
|
||||
// Record not found.
|
||||
resolver.fulfill(nil)
|
||||
}
|
||||
}
|
||||
database().add(fetchOperation)
|
||||
return promise
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func checkForManifestInCloudObjc(recipientId: String) -> AnyPromise {
|
||||
return AnyPromise(checkForManifestInCloud(recipientId: recipientId))
|
||||
}
|
||||
|
||||
public class func checkForManifestInCloud(recipientId: String) -> Promise<Bool> {
|
||||
|
||||
let recordName = recordNameForManifest(recipientId: recipientId)
|
||||
return checkForFileInCloud(recordName: recordName,
|
||||
remainingRetries: maxRetries)
|
||||
.map { (record) in
|
||||
return record != nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func allRecipientIdsWithManifestsInCloud(success: @escaping ([String]) -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
let processResults = { (recordNames: [String]) in
|
||||
DispatchQueue.global().async {
|
||||
let manifestRecordNames = recordNames.filter({ (recordName) -> Bool in
|
||||
self.isManifest(recordName: recordName)
|
||||
})
|
||||
let recipientIds = self.recipientIds(forRecordNames: manifestRecordNames)
|
||||
success(recipientIds)
|
||||
}
|
||||
}
|
||||
|
||||
let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true))
|
||||
// Fetch the first page of results for this query.
|
||||
fetchAllRecordNamesStep(recipientId: nil,
|
||||
query: query,
|
||||
previousRecordNames: [String](),
|
||||
cursor: nil,
|
||||
remainingRetries: maxRetries,
|
||||
success: processResults,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func fetchAllRecordNames(recipientId: String,
|
||||
success: @escaping ([String]) -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true))
|
||||
// Fetch the first page of results for this query.
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: [String](),
|
||||
cursor: nil,
|
||||
remainingRetries: maxRetries,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
private class func fetchAllRecordNamesStep(recipientId: String?,
|
||||
query: CKQuery,
|
||||
previousRecordNames: [String],
|
||||
cursor: CKQueryOperation.Cursor?,
|
||||
remainingRetries: Int,
|
||||
success: @escaping ([String]) -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
var allRecordNames = previousRecordNames
|
||||
|
||||
let queryOperation = CKQueryOperation(query: query)
|
||||
// If this isn't the first page of results for this query, resume
|
||||
// where we left off.
|
||||
queryOperation.cursor = cursor
|
||||
// Don't download the file; we're just using the query to get a list of record names.
|
||||
queryOperation.desiredKeys = []
|
||||
queryOperation.recordFetchedBlock = { (record) in
|
||||
assert(record.recordID.recordName.count > 0)
|
||||
|
||||
let recordName = record.recordID.recordName
|
||||
|
||||
if let recipientId = recipientId {
|
||||
let prefix = recordNamePrefix(forRecipientId: recipientId)
|
||||
guard recordName.hasPrefix(prefix) else {
|
||||
Logger.info("Ignoring record: \(recordName)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
allRecordNames.append(recordName)
|
||||
}
|
||||
queryOperation.queryCompletionBlock = { (cursor, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Fetch All Records")
|
||||
switch outcome {
|
||||
case .success:
|
||||
if let cursor = cursor {
|
||||
Logger.verbose("fetching more record names \(allRecordNames.count).")
|
||||
// There are more pages of results, continue fetching.
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: allRecordNames,
|
||||
cursor: cursor,
|
||||
remainingRetries: maxRetries,
|
||||
success: success,
|
||||
failure: failure)
|
||||
return
|
||||
}
|
||||
Logger.info("fetched \(allRecordNames.count) record names.")
|
||||
success(allRecordNames)
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
failure(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: allRecordNames,
|
||||
cursor: cursor,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: allRecordNames,
|
||||
cursor: cursor,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
case .unknownItem:
|
||||
owsFailDebug("unexpected CloudKit response.")
|
||||
failure(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
database().add(queryOperation)
|
||||
}
|
||||
|
||||
// MARK: - Download
|
||||
|
||||
@objc
|
||||
public class func downloadManifestFromCloudObjc(recipientId: String) -> AnyPromise {
|
||||
return AnyPromise(downloadManifestFromCloud(recipientId: recipientId))
|
||||
}
|
||||
|
||||
public class func downloadManifestFromCloud(recipientId: String) -> Promise<Data> {
|
||||
|
||||
let recordName = recordNameForManifest(recipientId: recipientId)
|
||||
return downloadDataFromCloud(recordName: recordName)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func downloadDataFromCloudObjc(recordName: String) -> AnyPromise {
|
||||
return AnyPromise(downloadDataFromCloud(recordName: recordName))
|
||||
}
|
||||
|
||||
public class func downloadDataFromCloud(recordName: String) -> Promise<Data> {
|
||||
return downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: maxRetries)
|
||||
.map { (asset) -> Data in
|
||||
guard let fileURL = asset.fileURL else {
|
||||
throw invalidServiceResponseError()
|
||||
}
|
||||
return try Data(contentsOf: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func downloadFileFromCloudObjc(recordName: String,
|
||||
toFileUrl: URL) -> AnyPromise {
|
||||
return AnyPromise(downloadFileFromCloud(recordName: recordName,
|
||||
toFileUrl: toFileUrl))
|
||||
}
|
||||
|
||||
public class func downloadFileFromCloud(recordName: String,
|
||||
toFileUrl: URL) -> Promise<Void> {
|
||||
|
||||
return downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: maxRetries)
|
||||
.done { asset in
|
||||
guard let fileURL = asset.fileURL else {
|
||||
throw invalidServiceResponseError()
|
||||
}
|
||||
try FileManager.default.copyItem(at: fileURL, to: toFileUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// We return the CKAsset and not its fileUrl because
|
||||
// CloudKit offers no guarantees around how long it'll
|
||||
// keep around the underlying file. Presumably we can
|
||||
// defer cleanup by maintaining a strong reference to
|
||||
// the asset.
|
||||
private class func downloadFromCloud(recordName: String,
|
||||
remainingRetries: Int) -> Promise<CKAsset> {
|
||||
|
||||
Logger.verbose("downloadFromCloud \(recordName)")
|
||||
|
||||
let (promise, resolver) = Promise<CKAsset>.pending()
|
||||
|
||||
let recordId = CKRecord.ID(recordName: recordName)
|
||||
let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ])
|
||||
// Download all keys for this record.
|
||||
fetchOperation.perRecordCompletionBlock = { (record, recordId, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Download Record")
|
||||
switch outcome {
|
||||
case .success:
|
||||
guard let record = record else {
|
||||
Logger.error("missing fetching record.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
return
|
||||
}
|
||||
guard let asset = record[payloadKey] as? CKAsset else {
|
||||
Logger.error("record missing payload.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
return
|
||||
}
|
||||
resolver.fulfill(asset)
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
resolver.reject(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (asset) in
|
||||
resolver.fulfill(asset)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (asset) in
|
||||
resolver.fulfill(asset)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
case .unknownItem:
|
||||
Logger.error("missing fetching record.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
database().add(fetchOperation)
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// MARK: - Access
|
||||
|
||||
@objc public enum BackupError: Int, Error {
|
||||
case couldNotDetermineAccountStatus
|
||||
case noAccount
|
||||
case restrictedAccountStatus
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func ensureCloudKitAccessObjc() -> AnyPromise {
|
||||
return AnyPromise(ensureCloudKitAccess())
|
||||
}
|
||||
|
||||
public class func ensureCloudKitAccess() -> Promise<Void> {
|
||||
let (promise, resolver) = Promise<Void>.pending()
|
||||
CKContainer.default().accountStatus { (accountStatus, error) in
|
||||
if let error = error {
|
||||
Logger.error("Unknown error: \(String(describing: error)).")
|
||||
resolver.reject(error)
|
||||
return
|
||||
}
|
||||
switch accountStatus {
|
||||
case .couldNotDetermine:
|
||||
Logger.error("could not determine CloudKit account status: \(String(describing: error)).")
|
||||
resolver.reject(BackupError.couldNotDetermineAccountStatus)
|
||||
case .noAccount:
|
||||
Logger.error("no CloudKit account.")
|
||||
resolver.reject(BackupError.noAccount)
|
||||
case .restricted:
|
||||
Logger.error("restricted CloudKit account.")
|
||||
resolver.reject(BackupError.restrictedAccountStatus)
|
||||
case .available:
|
||||
Logger.verbose("CloudKit access okay.")
|
||||
resolver.fulfill(())
|
||||
default: resolver.fulfill(())
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func errorMessage(forCloudKitAccessError error: Error) -> String {
|
||||
if let backupError = error as? BackupError {
|
||||
Logger.error("Backup error: \(String(describing: backupError)).")
|
||||
switch backupError {
|
||||
case .couldNotDetermineAccountStatus:
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_COULD_NOT_DETERMINE", comment: "Error indicating that the app could not determine that user's iCloud account status")
|
||||
case .noAccount:
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_NO_ACCOUNT", comment: "Error indicating that user does not have an iCloud account.")
|
||||
case .restrictedAccountStatus:
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_RESTRICTED", comment: "Error indicating that the app was prevented from accessing the user's iCloud account.")
|
||||
}
|
||||
} else {
|
||||
Logger.error("Unknown error: \(String(describing: error)).")
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_COULD_NOT_DETERMINE", comment: "Error indicating that the app could not determine that user's iCloud account status")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Retry
|
||||
|
||||
private enum APIOutcome {
|
||||
case success
|
||||
case failureDoNotRetry(error:Error)
|
||||
case failureRetryAfterDelay(retryDelay: TimeInterval)
|
||||
case failureRetryWithoutDelay
|
||||
// This only applies to fetches.
|
||||
case unknownItem
|
||||
}
|
||||
|
||||
private class func outcomeForCloudKitError(error: Error?,
|
||||
remainingRetries: Int,
|
||||
label: String) -> APIOutcome {
|
||||
if let error = error as? CKError {
|
||||
if error.code == CKError.unknownItem {
|
||||
// This is not always an error for our purposes.
|
||||
Logger.verbose("\(label) unknown item.")
|
||||
return .unknownItem
|
||||
}
|
||||
|
||||
Logger.error("\(label) failed: \(error)")
|
||||
|
||||
if remainingRetries < 1 {
|
||||
Logger.verbose("\(label) no more retries.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
}
|
||||
|
||||
if #available(iOS 11, *) {
|
||||
if error.code == CKError.serverResponseLost {
|
||||
Logger.verbose("\(label) retry without delay.")
|
||||
return .failureRetryWithoutDelay
|
||||
}
|
||||
}
|
||||
|
||||
switch error {
|
||||
case CKError.requestRateLimited, CKError.serviceUnavailable, CKError.zoneBusy:
|
||||
let retryDelay = error.retryAfterSeconds ?? 3.0
|
||||
Logger.verbose("\(label) retry with delay: \(retryDelay).")
|
||||
return .failureRetryAfterDelay(retryDelay:retryDelay)
|
||||
case CKError.networkFailure:
|
||||
Logger.verbose("\(label) retry without delay.")
|
||||
return .failureRetryWithoutDelay
|
||||
default:
|
||||
Logger.verbose("\(label) unknown CKError.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
}
|
||||
} else if let error = error {
|
||||
Logger.error("\(label) failed: \(error)")
|
||||
if remainingRetries < 1 {
|
||||
Logger.verbose("\(label) no more retries.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
}
|
||||
Logger.verbose("\(label) unknown error.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
} else {
|
||||
Logger.info("\(label) succeeded.")
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public class func setup() {
|
||||
cancelAllLongLivedOperations()
|
||||
}
|
||||
|
||||
private class func cancelAllLongLivedOperations() {
|
||||
// These APIs are only available in iOS 9.3 and later.
|
||||
guard #available(iOS 9.3, *) else {
|
||||
return
|
||||
}
|
||||
|
||||
let container = CKContainer.default()
|
||||
container.fetchAllLongLivedOperationIDs { (operationIds, error) in
|
||||
if let error = error {
|
||||
Logger.error("Could not get all long lived operations: \(error)")
|
||||
return
|
||||
}
|
||||
guard let operationIds = operationIds else {
|
||||
Logger.error("No operation ids.")
|
||||
return
|
||||
}
|
||||
|
||||
for operationId in operationIds {
|
||||
container.fetchLongLivedOperation(withID: operationId, completionHandler: { (operation, error) in
|
||||
if let error = error {
|
||||
Logger.error("Could not get long lived operation [\(operationId)]: \(error)")
|
||||
return
|
||||
}
|
||||
guard let operation = operation else {
|
||||
Logger.error("No operation.")
|
||||
return
|
||||
}
|
||||
operation.cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupJob.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupExportJob : OWSBackupJob
|
||||
|
||||
- (void)start;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
File diff suppressed because it is too large
Load Diff
@ -1,61 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupEncryptedItem : NSObject
|
||||
|
||||
@property (nonatomic) NSString *filePath;
|
||||
|
||||
@property (nonatomic) NSData *encryptionKey;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupIO : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithJobTempDirPath:(NSString *)jobTempDirPath;
|
||||
|
||||
- (NSString *)generateTempFilePath;
|
||||
|
||||
- (nullable NSString *)createTempFile;
|
||||
|
||||
#pragma mark - Encrypt
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath;
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath
|
||||
encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData;
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
#pragma mark - Decrypt
|
||||
|
||||
- (BOOL)decryptFileAsFile:(NSString *)srcFilePath
|
||||
dstFilePath:(NSString *)dstFilePath
|
||||
encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable NSData *)decryptDataAsData:(NSData *)srcData encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
#pragma mark - Compression
|
||||
|
||||
- (nullable NSData *)compressData:(NSData *)srcData;
|
||||
|
||||
// I'm using the (new in iOS 9) compressionlib. One of its weaknesses is that it
|
||||
// requires you to pre-allocate output buffers during compression and decompression.
|
||||
// During decompression this is particularly tricky since there's no way to safely
|
||||
// predict how large the output will be based on the input. So, we store the
|
||||
// uncompressed size for compressed backup items.
|
||||
- (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,273 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupIO.h"
|
||||
#import <SignalCoreKit/Randomness.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
|
||||
@import Compression;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// TODO:
|
||||
static const NSUInteger kOWSBackupKeyLength = 32;
|
||||
|
||||
// LZMA algorithm significantly outperforms the other compressionlib options
|
||||
// for our database snapshots and is a widely adopted standard.
|
||||
static const compression_algorithm SignalCompressionAlgorithm = COMPRESSION_LZMA;
|
||||
|
||||
@implementation OWSBackupEncryptedItem
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupIO ()
|
||||
|
||||
@property (nonatomic) NSString *jobTempDirPath;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupIO
|
||||
|
||||
- (instancetype)initWithJobTempDirPath:(NSString *)jobTempDirPath
|
||||
{
|
||||
if (!(self = [super init])) {
|
||||
return self;
|
||||
}
|
||||
|
||||
self.jobTempDirPath = jobTempDirPath;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)generateTempFilePath
|
||||
{
|
||||
return [self.jobTempDirPath stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
|
||||
}
|
||||
|
||||
- (nullable NSString *)createTempFile
|
||||
{
|
||||
NSString *filePath = [self generateTempFilePath];
|
||||
if (![OWSFileSystem ensureFileExists:filePath]) {
|
||||
OWSFailDebug(@"could not create temp file.");
|
||||
return nil;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
#pragma mark - Encrypt
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
|
||||
NSData *encryptionKey = [Randomness generateRandomBytes:(int)kOWSBackupKeyLength];
|
||||
|
||||
return [self encryptFileAsTempFile:srcFilePath encryptionKey:encryptionKey];
|
||||
}
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:srcFilePath]) {
|
||||
OWSFailDebug(@"Missing source file.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// TODO: Encrypt the file without loading it into memory.
|
||||
NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath];
|
||||
if (srcData.length < 1) {
|
||||
OWSFailDebug(@"could not load file into memory for encryption.");
|
||||
return nil;
|
||||
}
|
||||
return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData
|
||||
{
|
||||
OWSAssertDebug(srcData);
|
||||
|
||||
NSData *encryptionKey = [Randomness generateRandomBytes:(int)kOWSBackupKeyLength];
|
||||
|
||||
return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey];
|
||||
}
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)unencryptedData
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(unencryptedData);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
// TODO: Encrypt the data using key;
|
||||
NSData *encryptedData = unencryptedData;
|
||||
|
||||
NSString *_Nullable dstFilePath = [self createTempFile];
|
||||
if (!dstFilePath) {
|
||||
return nil;
|
||||
}
|
||||
NSError *error;
|
||||
BOOL success = [encryptedData writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
|
||||
if (!success || error) {
|
||||
OWSFailDebug(@"error writing encrypted data: %@", error);
|
||||
return nil;
|
||||
}
|
||||
[OWSFileSystem protectFileOrFolderAtPath:dstFilePath];
|
||||
OWSBackupEncryptedItem *item = [OWSBackupEncryptedItem new];
|
||||
item.filePath = dstFilePath;
|
||||
item.encryptionKey = encryptionKey;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Decrypt
|
||||
|
||||
- (BOOL)decryptFileAsFile:(NSString *)srcFilePath
|
||||
dstFilePath:(NSString *)dstFilePath
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
// TODO: Decrypt the file without loading it into memory.
|
||||
NSData *data = [self decryptFileAsData:srcFilePath encryptionKey:encryptionKey];
|
||||
|
||||
if (data.length < 1) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
BOOL success = [data writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
|
||||
if (!success || error) {
|
||||
OWSFailDebug(@"error writing decrypted data: %@", error);
|
||||
return NO;
|
||||
}
|
||||
[OWSFileSystem protectFileOrFolderAtPath:dstFilePath];
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:srcFilePath]) {
|
||||
OWSLogError(@"missing downloaded file.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath];
|
||||
if (srcData.length < 1) {
|
||||
OWSFailDebug(@"could not load file into memory for decryption.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable dstData = [self decryptDataAsData:srcData encryptionKey:encryptionKey];
|
||||
return dstData;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)decryptDataAsData:(NSData *)encryptedData encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(encryptedData);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
// TODO: Decrypt the data using key;
|
||||
NSData *unencryptedData = encryptedData;
|
||||
|
||||
return unencryptedData;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Compression
|
||||
|
||||
- (nullable NSData *)compressData:(NSData *)srcData
|
||||
{
|
||||
OWSAssertDebug(srcData);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (!srcData) {
|
||||
OWSFailDebug(@"missing unencrypted data.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t srcLength = [srcData length];
|
||||
|
||||
// This assumes that dst will always be smaller than src.
|
||||
//
|
||||
// We slightly pad the buffer size to account for the worst case.
|
||||
size_t dstBufferLength = srcLength + 64 * 1024;
|
||||
NSMutableData *dstBufferData = [NSMutableData dataWithLength:dstBufferLength];
|
||||
if (!dstBufferData) {
|
||||
OWSFailDebug(@"Failed to allocate buffer.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t dstLength = compression_encode_buffer(
|
||||
dstBufferData.mutableBytes, dstBufferLength, srcData.bytes, srcLength, NULL, SignalCompressionAlgorithm);
|
||||
NSData *compressedData = [dstBufferData subdataWithRange:NSMakeRange(0, dstLength)];
|
||||
|
||||
OWSLogVerbose(@"compressed %zd -> %zd = %0.2f",
|
||||
srcLength,
|
||||
dstLength,
|
||||
(srcLength > 0 ? (dstLength / (CGFloat)srcLength) : 0));
|
||||
|
||||
return compressedData;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength
|
||||
{
|
||||
OWSAssertDebug(srcData);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (!srcData) {
|
||||
OWSFailDebug(@"missing unencrypted data.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t srcLength = [srcData length];
|
||||
|
||||
// We pad the buffer to be defensive.
|
||||
size_t dstBufferLength = uncompressedDataLength + 1024;
|
||||
NSMutableData *dstBufferData = [NSMutableData dataWithLength:dstBufferLength];
|
||||
if (!dstBufferData) {
|
||||
OWSFailDebug(@"Failed to allocate buffer.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t dstLength = compression_decode_buffer(
|
||||
dstBufferData.mutableBytes, dstBufferLength, srcData.bytes, srcLength, NULL, SignalCompressionAlgorithm);
|
||||
NSData *decompressedData = [dstBufferData subdataWithRange:NSMakeRange(0, dstLength)];
|
||||
OWSAssertDebug(decompressedData.length == uncompressedDataLength);
|
||||
OWSLogVerbose(@"decompressed %zd -> %zd = %0.2f",
|
||||
srcLength,
|
||||
dstLength,
|
||||
(dstLength > 0 ? (srcLength / (CGFloat)dstLength) : 0));
|
||||
|
||||
return decompressedData;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,15 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupJob.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupImportJob : OWSBackupJob
|
||||
|
||||
- (void)start;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,635 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupImportJob.h"
|
||||
#import "OWSBackupIO.h"
|
||||
#import "OWSDatabaseMigration.h"
|
||||
#import "OWSDatabaseMigrationRunner.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/NSData+OWS.h>
|
||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SessionMessagingKit/TSAttachment.h>
|
||||
#import <SessionMessagingKit/TSMessage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKeySpec";
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupImportJob ()
|
||||
|
||||
@property (nonatomic, nullable) OWSBackgroundTask *backgroundTask;
|
||||
|
||||
@property (nonatomic) OWSBackupIO *backupIO;
|
||||
|
||||
@property (nonatomic) OWSBackupManifestContents *manifest;
|
||||
|
||||
@property (nonatomic, nullable) YapDatabaseConnection *dbConnection;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupImportJob
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.primaryStorage);
|
||||
|
||||
return SSKEnvironment.shared.primaryStorage;
|
||||
}
|
||||
|
||||
- (OWSProfileManager *)profileManager
|
||||
{
|
||||
return [OWSProfileManager sharedManager];
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
||||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
- (OWSBackup *)backup
|
||||
{
|
||||
OWSAssertDebug(AppEnvironment.shared.backup);
|
||||
|
||||
return AppEnvironment.shared.backup;
|
||||
}
|
||||
|
||||
- (OWSBackupLazyRestore *)backupLazyRestore
|
||||
{
|
||||
return AppEnvironment.shared.backupLazyRestore;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSArray<OWSBackupFragment *> *)databaseItems
|
||||
{
|
||||
OWSAssertDebug(self.manifest);
|
||||
|
||||
return self.manifest.databaseItems;
|
||||
}
|
||||
|
||||
- (NSArray<OWSBackupFragment *> *)attachmentsItems
|
||||
{
|
||||
OWSAssertDebug(self.manifest);
|
||||
|
||||
return self.manifest.attachmentsItems;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
self.backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
|
||||
|
||||
self.dbConnection = self.primaryStorage.newDatabaseConnection;
|
||||
|
||||
[self updateProgressWithDescription:nil progress:nil];
|
||||
|
||||
[[self.backup ensureCloudKitAccess]
|
||||
.thenInBackground(^{
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_CONFIGURATION",
|
||||
@"Indicates that the backup import is being configured.")
|
||||
progress:nil];
|
||||
|
||||
return [self configureImport];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
if (self.isComplete) {
|
||||
return
|
||||
[AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_IMPORT",
|
||||
@"Indicates that the backup import data is being imported.")
|
||||
progress:nil];
|
||||
|
||||
return [self downloadAndProcessManifestWithBackupIO:self.backupIO];
|
||||
})
|
||||
.thenInBackground(^(OWSBackupManifestContents *manifest) {
|
||||
OWSCAssertDebug(manifest.databaseItems.count > 0);
|
||||
OWSCAssertDebug(manifest.attachmentsItems);
|
||||
|
||||
self.manifest = manifest;
|
||||
|
||||
return [self downloadAndProcessImport];
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
[self failWithErrorDescription:
|
||||
NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
|
||||
@"Error indicating the backup import could not import the user's data.")];
|
||||
}) retainUntilComplete];
|
||||
}
|
||||
|
||||
- (AnyPromise *)downloadAndProcessImport
|
||||
{
|
||||
OWSAssertDebug(self.databaseItems);
|
||||
OWSAssertDebug(self.attachmentsItems);
|
||||
|
||||
// These items should be downloaded immediately.
|
||||
NSMutableArray<OWSBackupFragment *> *allItems = [NSMutableArray new];
|
||||
[allItems addObjectsFromArray:self.databaseItems];
|
||||
|
||||
// Make a copy of the blockingItems before we add
|
||||
// the optional items.
|
||||
NSArray<OWSBackupFragment *> *blockingItems = [allItems copy];
|
||||
|
||||
// Local profile avatars are optional in the sense that if their
|
||||
// download fails, we want to proceed with the import.
|
||||
if (self.manifest.localProfileAvatarItem) {
|
||||
[allItems addObject:self.manifest.localProfileAvatarItem];
|
||||
}
|
||||
|
||||
// Attachment items can be downloaded later;
|
||||
// they will can be lazy-restored.
|
||||
[allItems addObjectsFromArray:self.attachmentsItems];
|
||||
|
||||
// Record metadata for all items, so that we can re-use them in incremental backups after the restore.
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (OWSBackupFragment *item in allItems) {
|
||||
[item saveWithTransaction:transaction];
|
||||
}
|
||||
}];
|
||||
|
||||
return [self downloadFilesFromCloud:blockingItems]
|
||||
.thenInBackground(^{
|
||||
return [self restoreDatabase];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self ensureMigrations];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self restoreLocalProfile];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self restoreAttachmentFiles];
|
||||
})
|
||||
.then(^{
|
||||
// Kick off lazy restore on main thread.
|
||||
[self.backupLazyRestore clearCompleteAndRunIfNecessary];
|
||||
|
||||
// Make sure backup is enabled once we complete
|
||||
// a backup restore.
|
||||
[OWSBackup.sharedManager setIsBackupEnabled:YES];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self.tsAccountManager updateAccountAttributes];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
[self succeed];
|
||||
});
|
||||
}
|
||||
|
||||
- (AnyPromise *)configureImport
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (![self ensureJobTempDir]) {
|
||||
OWSFailDebug(@"Could not create jobTempDirPath.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not create jobTempDirPath.")];
|
||||
}
|
||||
|
||||
self.backupIO = [[OWSBackupIO alloc] initWithJobTempDirPath:self.jobTempDirPath];
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (AnyPromise *)downloadFilesFromCloud:(NSArray<OWSBackupFragment *> *)items
|
||||
{
|
||||
OWSAssertDebug(items.count > 0);
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
NSUInteger recordCount = items.count;
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
if (items.count < 1) {
|
||||
// All downloads are complete; exit.
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
AnyPromise *promise = [AnyPromise promiseWithValue:@(1)];
|
||||
for (OWSBackupFragment *item in items) {
|
||||
promise = promise
|
||||
.thenInBackground(^{
|
||||
CGFloat progress
|
||||
= (recordCount > 0 ? ((recordCount - items.count) / (CGFloat)recordCount) : 0.f);
|
||||
[self updateProgressWithDescription:
|
||||
NSLocalizedString(@"BACKUP_IMPORT_PHASE_DOWNLOAD",
|
||||
@"Indicates that the backup import data is being downloaded.")
|
||||
progress:@(progress)];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self downloadFileFromCloud:item];
|
||||
});
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
- (AnyPromise *)downloadFileFromCloud:(OWSBackupFragment *)item
|
||||
{
|
||||
OWSAssertDebug(item);
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
// TODO: 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.
|
||||
NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:item.recordName];
|
||||
|
||||
// Skip redundant file download.
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) {
|
||||
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
|
||||
|
||||
item.downloadFilePath = tempFilePath;
|
||||
|
||||
return resolve(@(1));
|
||||
}
|
||||
|
||||
[OWSBackupAPI downloadFileFromCloudObjcWithRecordName:item.recordName
|
||||
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
|
||||
.thenInBackground(^{
|
||||
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
|
||||
item.downloadFilePath = tempFilePath;
|
||||
|
||||
resolve(@(1));
|
||||
})
|
||||
.catchInBackground(^(NSError *error) {
|
||||
resolve(error);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (AnyPromise *)restoreLocalProfile
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
AnyPromise *promise = [AnyPromise promiseWithValue:@(1)];
|
||||
|
||||
if (self.manifest.localProfileAvatarItem) {
|
||||
promise = promise.thenInBackground(^{
|
||||
return
|
||||
[self downloadFileFromCloud:self.manifest.localProfileAvatarItem].catchInBackground(^(NSError *error) {
|
||||
OWSLogInfo(@"Ignoring error; profiles are optional: %@", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
promise = promise.thenInBackground(^{
|
||||
return [self applyLocalProfile];
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
- (AnyPromise *)applyLocalProfile
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
NSString *_Nullable localProfileName = self.manifest.localProfileName;
|
||||
UIImage *_Nullable localProfileAvatar = [self tryToLoadLocalProfileAvatar];
|
||||
|
||||
OWSLogVerbose(@"local profile name: %@, avatar: %d", localProfileName, localProfileAvatar != nil);
|
||||
|
||||
if (localProfileName.length < 1 && !localProfileAvatar) {
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
[self.profileManager updateLocalProfileName:localProfileName
|
||||
avatarImage:localProfileAvatar
|
||||
success:^{
|
||||
resolve(@(1));
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
// Ignore errors related to local profile.
|
||||
resolve(@(1));
|
||||
}
|
||||
requiresSync:YES];
|
||||
}];
|
||||
}
|
||||
|
||||
- (nullable UIImage *)tryToLoadLocalProfileAvatar
|
||||
{
|
||||
if (!self.manifest.localProfileAvatarItem) {
|
||||
return nil;
|
||||
}
|
||||
if (!self.manifest.localProfileAvatarItem.downloadFilePath) {
|
||||
// Download of the avatar failed.
|
||||
// We can safely ignore errors related to local profile.
|
||||
OWSLogError(@"local profile avatar was not downloaded.");
|
||||
return nil;
|
||||
}
|
||||
OWSBackupFragment *item = self.manifest.localProfileAvatarItem;
|
||||
if (item.recordName.length < 1) {
|
||||
OWSFailDebug(@"item missing record name.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
NSData *_Nullable data =
|
||||
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
|
||||
if (!data) {
|
||||
OWSLogError(@"could not decrypt local profile avatar.");
|
||||
// Ignore errors related to local profile.
|
||||
return nil;
|
||||
}
|
||||
// TODO: Verify that we're not compressing the profile avatar data.
|
||||
UIImage *_Nullable image = [UIImage imageWithData:data];
|
||||
if (!image) {
|
||||
OWSLogError(@"could not decrypt local profile avatar.");
|
||||
// Ignore errors related to local profile.
|
||||
return nil;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
- (AnyPromise *)restoreAttachmentFiles
|
||||
{
|
||||
OWSLogVerbose(@": %zd", self.attachmentsItems.count);
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
__block NSUInteger count = 0;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (OWSBackupFragment *item in self.attachmentsItems) {
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (item.recordName.length < 1) {
|
||||
OWSLogError(@"attachment was not downloaded.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
if (item.attachmentId.length < 1) {
|
||||
OWSLogError(@"attachment missing attachment id.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
if (item.relativeFilePath.length < 1) {
|
||||
OWSLogError(@"attachment missing relative file path.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
TSAttachmentPointer *_Nullable attachment =
|
||||
[TSAttachmentPointer fetchObjectWithUniqueID:item.attachmentId transaction:transaction];
|
||||
if (!attachment) {
|
||||
OWSLogError(@"attachment to restore could not be found.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentPointer class]]) {
|
||||
OWSFailDebug(@"attachment has unexpected type: %@.", attachment.class);
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
[attachment markForLazyRestoreWithFragment:item transaction:transaction];
|
||||
count++;
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_RESTORING_FILES",
|
||||
@"Indicates that the backup import data is being restored.")
|
||||
progress:@(count / (CGFloat)self.attachmentsItems.count)];
|
||||
}
|
||||
}];
|
||||
|
||||
OWSLogError(@"enqueued lazy restore of %zd files.", count);
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (AnyPromise *)restoreDatabase
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
// Order matters here.
|
||||
NSArray<NSString *> *collectionsToRestore = @[
|
||||
[TSThread collection],
|
||||
[TSAttachment collection],
|
||||
// Interactions refer to threads and attachments,
|
||||
// so copy them afterward.
|
||||
[TSInteraction collection],
|
||||
[OWSDatabaseMigration collection],
|
||||
];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *restoredEntityCounts = [NSMutableDictionary new];
|
||||
__block unsigned long long copiedEntities = 0;
|
||||
__block BOOL aborted = NO;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
if ([collection isEqualToString:[OWSDatabaseMigration collection]]) {
|
||||
// It's okay if there are existing migrations; we'll clear those
|
||||
// before restoring.
|
||||
continue;
|
||||
}
|
||||
if ([transaction numberOfKeysInCollection:collection] > 0) {
|
||||
OWSLogError(@"unexpected contents in database (%@).", collection);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing database contents.
|
||||
//
|
||||
// This should be safe since we only ever import into an empty database.
|
||||
//
|
||||
// Note that if the app receives a message after registering and before restoring
|
||||
// backup, it will be lost.
|
||||
//
|
||||
// Note that this will clear all migrations.
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
[transaction removeAllObjectsInCollection:collection];
|
||||
}
|
||||
|
||||
NSUInteger count = 0;
|
||||
for (OWSBackupFragment *item in self.databaseItems) {
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (item.recordName.length < 1) {
|
||||
OWSLogError(@"database snapshot was not downloaded.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) {
|
||||
OWSLogError(@"database snapshot missing size.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
count++;
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_RESTORING_DATABASE",
|
||||
@"Indicates that the backup database is being restored.")
|
||||
progress:@(count / (CGFloat)self.databaseItems.count)];
|
||||
|
||||
@autoreleasepool {
|
||||
NSData *_Nullable compressedData =
|
||||
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
|
||||
if (!compressedData) {
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
NSData *_Nullable uncompressedData =
|
||||
[self.backupIO decompressData:compressedData
|
||||
uncompressedDataLength:item.uncompressedDataLength.unsignedIntValue];
|
||||
if (!uncompressedData) {
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
NSError *error;
|
||||
SignalIOSProtoBackupSnapshot *_Nullable entities =
|
||||
[SignalIOSProtoBackupSnapshot parseData:uncompressedData error:&error];
|
||||
if (!entities || error) {
|
||||
OWSLogError(@"could not parse proto: %@.", error);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
if (!entities || entities.entity.count < 1) {
|
||||
OWSLogError(@"missing entities.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
for (SignalIOSProtoBackupSnapshotBackupEntity *entity in entities.entity) {
|
||||
NSData *_Nullable entityData = entity.entityData;
|
||||
if (entityData.length < 1) {
|
||||
OWSLogError(@"missing entity data.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *_Nullable collection = entity.collection;
|
||||
if (collection.length < 1) {
|
||||
OWSLogError(@"missing collection.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *_Nullable key = entity.key;
|
||||
if (key.length < 1) {
|
||||
OWSLogError(@"missing key.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
__block NSObject *object = nil;
|
||||
@try {
|
||||
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:entityData];
|
||||
object = [unarchiver decodeObjectForKey:@"root"];
|
||||
if (![object isKindOfClass:[object class]]) {
|
||||
OWSLogError(@"invalid decoded entity: %@.", [object class]);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
OWSLogError(@"could not decode entity.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
[transaction setObject:object forKey:key inCollection:collection];
|
||||
copiedEntities++;
|
||||
NSUInteger restoredEntityCount = restoredEntityCounts[collection].unsignedIntValue;
|
||||
restoredEntityCounts[collection] = @(restoredEntityCount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
if (aborted) {
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import failed.")];
|
||||
}
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
for (NSString *collection in restoredEntityCounts) {
|
||||
OWSLogInfo(@"copied %@: %@", collection, restoredEntityCounts[collection]);
|
||||
}
|
||||
OWSLogInfo(@"copiedEntities: %llu", copiedEntities);
|
||||
|
||||
[self.primaryStorage logFileSizes];
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (AnyPromise *)ensureMigrations
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_FINALIZING",
|
||||
@"Indicates that the backup import data is being finalized.")
|
||||
progress:nil];
|
||||
|
||||
|
||||
// It's okay that we do this in a separate transaction from the
|
||||
// restoration of backup contents. If some of migrations don't
|
||||
// complete, they'll be run the next time the app launches.
|
||||
AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:^(BOOL successful, BOOL needsConfigSync){
|
||||
resolve(@(1));
|
||||
}];
|
||||
});
|
||||
}];
|
||||
return promise;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,92 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSYapDatabaseObject.h"
|
||||
#import <SessionMessagingKit/OWSBackupFragment.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kOWSBackup_ManifestKey_DatabaseFiles;
|
||||
extern NSString *const kOWSBackup_ManifestKey_AttachmentFiles;
|
||||
extern NSString *const kOWSBackup_ManifestKey_RecordName;
|
||||
extern NSString *const kOWSBackup_ManifestKey_EncryptionKey;
|
||||
extern NSString *const kOWSBackup_ManifestKey_RelativeFilePath;
|
||||
extern NSString *const kOWSBackup_ManifestKey_AttachmentId;
|
||||
extern NSString *const kOWSBackup_ManifestKey_DataSize;
|
||||
extern NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar;
|
||||
extern NSString *const kOWSBackup_ManifestKey_LocalProfileName;
|
||||
|
||||
@class AnyPromise;
|
||||
@class OWSBackupIO;
|
||||
@class OWSBackupJob;
|
||||
@class OWSBackupManifestContents;
|
||||
|
||||
typedef void (^OWSBackupJobBoolCompletion)(BOOL success);
|
||||
typedef void (^OWSBackupJobCompletion)(NSError *_Nullable error);
|
||||
typedef void (^OWSBackupJobManifestSuccess)(OWSBackupManifestContents *manifest);
|
||||
typedef void (^OWSBackupJobManifestFailure)(NSError *error);
|
||||
|
||||
@interface OWSBackupManifestContents : NSObject
|
||||
|
||||
@property (nonatomic) NSArray<OWSBackupFragment *> *databaseItems;
|
||||
@property (nonatomic) NSArray<OWSBackupFragment *> *attachmentsItems;
|
||||
@property (nonatomic, nullable) OWSBackupFragment *localProfileAvatarItem;
|
||||
@property (nonatomic, nullable) NSString *localProfileName;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@protocol OWSBackupJobDelegate <NSObject>
|
||||
|
||||
- (nullable NSData *)backupEncryptionKey;
|
||||
|
||||
// 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 -
|
||||
|
||||
@interface OWSBackupJob : NSObject
|
||||
|
||||
@property (nonatomic, weak, readonly) id<OWSBackupJobDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) NSString *recipientId;
|
||||
|
||||
// Indicates that the backup succeeded, failed or was cancelled.
|
||||
@property (atomic, readonly) BOOL isComplete;
|
||||
|
||||
@property (nonatomic, readonly) NSString *jobTempDirPath;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)delegate recipientId:(NSString *)recipientId;
|
||||
|
||||
#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 - Manifest
|
||||
|
||||
- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO __attribute__((warn_unused_result));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,316 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupJob.h"
|
||||
#import "OWSBackupIO.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/Randomness.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_RecordName = @"record_name";
|
||||
NSString *const kOWSBackup_ManifestKey_EncryptionKey = @"encryption_key";
|
||||
NSString *const kOWSBackup_ManifestKey_RelativeFilePath = @"relative_file_path";
|
||||
NSString *const kOWSBackup_ManifestKey_AttachmentId = @"attachment_id";
|
||||
NSString *const kOWSBackup_ManifestKey_DataSize = @"data_size";
|
||||
NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar = @"local_profile_avatar";
|
||||
NSString *const kOWSBackup_ManifestKey_LocalProfileName = @"local_profile_name";
|
||||
|
||||
NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
|
||||
|
||||
@implementation OWSBackupManifestContents
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupJob ()
|
||||
|
||||
@property (nonatomic, weak) id<OWSBackupJobDelegate> delegate;
|
||||
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
|
||||
@property (atomic) BOOL isComplete;
|
||||
@property (atomic) BOOL hasSucceeded;
|
||||
|
||||
@property (nonatomic) NSString *jobTempDirPath;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupJob
|
||||
|
||||
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)delegate recipientId:(NSString *)recipientId
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
OWSAssertDebug([OWSStorage isStorageReady]);
|
||||
|
||||
self.delegate = delegate;
|
||||
self.recipientId = recipientId;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// Surface memory leaks by logging the deallocation.
|
||||
OWSLogVerbose(@"Dealloc: %@", self.class);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
if (self.jobTempDirPath) {
|
||||
[OWSFileSystem deleteFileIfExists:self.jobTempDirPath];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)ensureJobTempDir
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
// TODO: Exports should use a new directory each time, but imports
|
||||
// might want to use a predictable directory so that repeated
|
||||
// import attempts can reuse downloads from previous attempts.
|
||||
NSString *temporaryDirectory = OWSTemporaryDirectory();
|
||||
self.jobTempDirPath = [temporaryDirectory stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
|
||||
|
||||
if (![OWSFileSystem ensureDirectoryExists:self.jobTempDirPath]) {
|
||||
OWSFailDebug(@"Could not create jobTempDirPath.");
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
self.isComplete = YES;
|
||||
}
|
||||
|
||||
- (void)succeed
|
||||
{
|
||||
OWSLogInfo(@"");
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.isComplete) {
|
||||
OWSAssertDebug(!self.hasSucceeded);
|
||||
return;
|
||||
}
|
||||
self.isComplete = YES;
|
||||
|
||||
// There's a lot of asynchrony in these backup jobs;
|
||||
// ensure we only end up finishing these jobs once.
|
||||
OWSAssertDebug(!self.hasSucceeded);
|
||||
self.hasSucceeded = YES;
|
||||
|
||||
[self.delegate backupJobDidSucceed:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)failWithErrorDescription:(NSString *)description
|
||||
{
|
||||
[self failWithError:OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed, description)];
|
||||
}
|
||||
|
||||
- (void)failWithError:(NSError *)error
|
||||
{
|
||||
OWSFailDebug(@"%@", error);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
OWSAssertDebug(!self.hasSucceeded);
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
self.isComplete = YES;
|
||||
[self.delegate backupJobDidFail:self error:error];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress
|
||||
{
|
||||
OWSLogInfo(@"");
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
[self.delegate backupJobDidUpdate:self description:description progress:progress];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Manifest
|
||||
|
||||
- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO
|
||||
{
|
||||
OWSAssertDebug(backupIO);
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
|
||||
}
|
||||
|
||||
return
|
||||
[OWSBackupAPI downloadManifestFromCloudObjcWithRecipientId:self.recipientId].thenInBackground(^(NSData *data) {
|
||||
return [self processManifest:data backupIO:backupIO];
|
||||
});
|
||||
}
|
||||
|
||||
- (AnyPromise *)processManifest:(NSData *)manifestDataEncrypted backupIO:(OWSBackupIO *)backupIO
|
||||
{
|
||||
OWSAssertDebug(manifestDataEncrypted.length > 0);
|
||||
OWSAssertDebug(backupIO);
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
|
||||
}
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
NSData *_Nullable manifestDataDecrypted =
|
||||
[backupIO decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey];
|
||||
if (!manifestDataDecrypted) {
|
||||
OWSFailDebug(@"Could not decrypt manifest.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not decrypt manifest.")];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSDictionary<NSString *, id> *_Nullable json =
|
||||
[NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error];
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"Could not download manifest.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not download manifest.")];
|
||||
}
|
||||
|
||||
OWSLogVerbose(@"json: %@", json);
|
||||
|
||||
NSArray<OWSBackupFragment *> *_Nullable databaseItems =
|
||||
[self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
|
||||
if (!databaseItems) {
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")];
|
||||
}
|
||||
NSArray<OWSBackupFragment *> *_Nullable attachmentsItems =
|
||||
[self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
|
||||
if (!attachmentsItems) {
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")];
|
||||
}
|
||||
|
||||
NSArray<OWSBackupFragment *> *_Nullable localProfileAvatarItems;
|
||||
if ([self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileAvatar]) {
|
||||
localProfileAvatarItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_LocalProfileAvatar];
|
||||
}
|
||||
|
||||
NSString *_Nullable localProfileName = [self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileName];
|
||||
|
||||
OWSBackupManifestContents *contents = [OWSBackupManifestContents new];
|
||||
contents.databaseItems = databaseItems;
|
||||
contents.attachmentsItems = attachmentsItems;
|
||||
contents.localProfileAvatarItem = localProfileAvatarItems.firstObject;
|
||||
if ([localProfileName isKindOfClass:[NSString class]]) {
|
||||
contents.localProfileName = localProfileName;
|
||||
} else if (localProfileName) {
|
||||
OWSFailDebug(@"Invalid localProfileName: %@", [localProfileName class]);
|
||||
}
|
||||
|
||||
return [AnyPromise promiseWithValue:contents];
|
||||
}
|
||||
|
||||
- (nullable id)parseManifestItem:(id)json key:(NSString *)key
|
||||
{
|
||||
OWSAssertDebug(json);
|
||||
OWSAssertDebug(key.length);
|
||||
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"manifest has invalid data.");
|
||||
return nil;
|
||||
}
|
||||
id _Nullable value = json[key];
|
||||
return value;
|
||||
}
|
||||
|
||||
- (nullable NSArray<OWSBackupFragment *> *)parseManifestItems:(id)json key:(NSString *)key
|
||||
{
|
||||
OWSAssertDebug(json);
|
||||
OWSAssertDebug(key.length);
|
||||
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"manifest has invalid data.");
|
||||
return nil;
|
||||
}
|
||||
NSArray *itemMaps = json[key];
|
||||
if (![itemMaps isKindOfClass:[NSArray class]]) {
|
||||
OWSFailDebug(@"manifest has invalid data.");
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray<OWSBackupFragment *> *items = [NSMutableArray new];
|
||||
for (NSDictionary *itemMap in itemMaps) {
|
||||
if (![itemMap isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"manifest has invalid item.");
|
||||
return nil;
|
||||
}
|
||||
NSString *_Nullable recordName = itemMap[kOWSBackup_ManifestKey_RecordName];
|
||||
NSString *_Nullable encryptionKeyString = itemMap[kOWSBackup_ManifestKey_EncryptionKey];
|
||||
NSString *_Nullable relativeFilePath = itemMap[kOWSBackup_ManifestKey_RelativeFilePath];
|
||||
NSString *_Nullable attachmentId = itemMap[kOWSBackup_ManifestKey_AttachmentId];
|
||||
NSNumber *_Nullable uncompressedDataLength = itemMap[kOWSBackup_ManifestKey_DataSize];
|
||||
if (![recordName isKindOfClass:[NSString class]]) {
|
||||
OWSFailDebug(@"manifest has invalid recordName: %@.", recordName);
|
||||
return nil;
|
||||
}
|
||||
if (![encryptionKeyString isKindOfClass:[NSString class]]) {
|
||||
OWSFailDebug(@"manifest has invalid encryptionKey.");
|
||||
return nil;
|
||||
}
|
||||
// relativeFilePath is an optional field.
|
||||
if (relativeFilePath && ![relativeFilePath isKindOfClass:[NSString class]]) {
|
||||
OWSLogDebug(@"manifest has invalid relativeFilePath: %@.", relativeFilePath);
|
||||
OWSFailDebug(@"manifest has invalid relativeFilePath");
|
||||
return nil;
|
||||
}
|
||||
// attachmentId is an optional field.
|
||||
if (attachmentId && ![attachmentId isKindOfClass:[NSString class]]) {
|
||||
OWSLogDebug(@"manifest has invalid attachmentId: %@.", attachmentId);
|
||||
OWSFailDebug(@"manifest has invalid attachmentId");
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable encryptionKey = [NSData dataFromBase64String:encryptionKeyString];
|
||||
if (!encryptionKey) {
|
||||
OWSFailDebug(@"manifest has corrupt encryptionKey");
|
||||
return nil;
|
||||
}
|
||||
// uncompressedDataLength is an optional field.
|
||||
if (uncompressedDataLength && ![uncompressedDataLength isKindOfClass:[NSNumber class]]) {
|
||||
OWSFailDebug(@"manifest has invalid uncompressedDataLength: %@.", uncompressedDataLength);
|
||||
return nil;
|
||||
}
|
||||
|
||||
OWSBackupFragment *item = [[OWSBackupFragment alloc] initWithUniqueId:recordName];
|
||||
item.recordName = recordName;
|
||||
item.encryptionKey = encryptionKey;
|
||||
item.relativeFilePath = relativeFilePath;
|
||||
item.attachmentId = attachmentId;
|
||||
item.uncompressedDataLength = uncompressedDataLength;
|
||||
[items addObject:item];
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,176 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc(OWSBackupLazyRestore)
|
||||
public class BackupLazyRestore: NSObject {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var backup: OWSBackup {
|
||||
return AppEnvironment.shared.backup
|
||||
}
|
||||
|
||||
private var primaryStorage: OWSPrimaryStorage {
|
||||
return SSKEnvironment.shared.primaryStorage
|
||||
}
|
||||
|
||||
private var tsAccountManager: TSAccountManager {
|
||||
return TSAccountManager.sharedInstance()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private var isRunning = false
|
||||
private var isComplete = false
|
||||
|
||||
@objc
|
||||
public required override init() {
|
||||
super.init()
|
||||
|
||||
SwiftSingletons.register(self)
|
||||
|
||||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||
self.runIfNecessary()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .OWSApplicationDidBecomeActive, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .RegistrationStateDidChange, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: NSNotification.Name(NSNotificationNameBackupStateDidChange), object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let backgroundQueue = DispatchQueue.global(qos: .background)
|
||||
|
||||
@objc
|
||||
public func clearCompleteAndRunIfNecessary() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
isComplete = false
|
||||
|
||||
runIfNecessary()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func isBackupImportInProgress() -> Bool {
|
||||
return backup.backupImportState == .inProgress
|
||||
}
|
||||
|
||||
@objc
|
||||
public func runIfNecessary() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard !CurrentAppContext().isRunningTests else {
|
||||
return
|
||||
}
|
||||
guard AppReadiness.isAppReady() else {
|
||||
return
|
||||
}
|
||||
guard CurrentAppContext().isMainAppAndActive else {
|
||||
return
|
||||
}
|
||||
guard tsAccountManager.isRegisteredAndReady() else {
|
||||
return
|
||||
}
|
||||
guard !isBackupImportInProgress() else {
|
||||
return
|
||||
}
|
||||
guard !isRunning, !isComplete else {
|
||||
return
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
|
||||
backgroundQueue.async {
|
||||
self.restoreAttachments()
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreAttachments() {
|
||||
let temporaryDirectory = OWSTemporaryDirectory()
|
||||
let jobTempDirPath = (temporaryDirectory as NSString).appendingPathComponent(NSUUID().uuidString)
|
||||
|
||||
guard OWSFileSystem.ensureDirectoryExists(jobTempDirPath) else {
|
||||
Logger.error("could not create temp directory.")
|
||||
complete(errorCount: 1)
|
||||
return
|
||||
}
|
||||
|
||||
let backupIO = OWSBackupIO(jobTempDirPath: jobTempDirPath)
|
||||
|
||||
let attachmentIds = backup.attachmentIdsForLazyRestore()
|
||||
guard attachmentIds.count > 0 else {
|
||||
Logger.info("No attachments need lazy restore.")
|
||||
complete(errorCount: 0)
|
||||
return
|
||||
}
|
||||
Logger.info("Lazy restoring \(attachmentIds.count) attachments.")
|
||||
tryToRestoreNextAttachment(attachmentIds: attachmentIds, errorCount: 0, backupIO: backupIO)
|
||||
}
|
||||
|
||||
private func tryToRestoreNextAttachment(attachmentIds: [String], errorCount: UInt, backupIO: OWSBackupIO) {
|
||||
guard !isBackupImportInProgress() else {
|
||||
Logger.verbose("A backup import is in progress; abort.")
|
||||
complete(errorCount: errorCount + 1)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentIdsCopy = attachmentIds
|
||||
guard let attachmentId = attachmentIdsCopy.popLast() else {
|
||||
// This job is done.
|
||||
Logger.verbose("job is done.")
|
||||
complete(errorCount: errorCount)
|
||||
return
|
||||
}
|
||||
guard let attachmentPointer = TSAttachment.fetch(uniqueId: attachmentId) as? TSAttachmentPointer else {
|
||||
Logger.warn("could not load attachment.")
|
||||
// Not necessarily an error.
|
||||
// The attachment might have been deleted since the job began.
|
||||
// Continue trying to restore the other attachments.
|
||||
tryToRestoreNextAttachment(attachmentIds: attachmentIds, errorCount: errorCount + 1, backupIO: backupIO)
|
||||
return
|
||||
}
|
||||
backup.lazyRestoreAttachment(attachmentPointer,
|
||||
backupIO: backupIO)
|
||||
.done(on: self.backgroundQueue) { _ in
|
||||
Logger.info("Restored attachment.")
|
||||
|
||||
// Continue trying to restore the other attachments.
|
||||
self.tryToRestoreNextAttachment(attachmentIds: attachmentIdsCopy, errorCount: errorCount, backupIO: backupIO)
|
||||
}.catch(on: self.backgroundQueue) { (error) in
|
||||
Logger.error("Could not restore attachment: \(error)")
|
||||
|
||||
// Continue trying to restore the other attachments.
|
||||
self.tryToRestoreNextAttachment(attachmentIds: attachmentIdsCopy, errorCount: errorCount + 1, backupIO: backupIO)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
private func complete(errorCount: UInt) {
|
||||
Logger.verbose("")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isRunning = false
|
||||
|
||||
if errorCount == 0 {
|
||||
self.isComplete = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSTableViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupSettingsViewController : OWSTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,214 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupSettingsViewController.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "Session-Swift.h"
|
||||
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/MIMETypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupSettingsViewController ()
|
||||
|
||||
@property (nonatomic, nullable) NSError *iCloudError;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupSettingsViewController
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSBackup *)backup
|
||||
{
|
||||
OWSAssertDebug(AppEnvironment.shared.backup);
|
||||
|
||||
return AppEnvironment.shared.backup;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings.");
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(backupStateDidChange:)
|
||||
name:NSNotificationNameBackupStateDidChange
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationDidBecomeActive:)
|
||||
name:OWSApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[self updateTableContents];
|
||||
[self updateICloudStatus];
|
||||
}
|
||||
|
||||
- (void)updateICloudStatus
|
||||
{
|
||||
__weak OWSBackupSettingsViewController *weakSelf = self;
|
||||
[[self.backup ensureCloudKitAccess]
|
||||
.then(^{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
weakSelf.iCloudError = nil;
|
||||
[weakSelf updateTableContents];
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
weakSelf.iCloudError = error;
|
||||
[weakSelf updateTableContents];
|
||||
}) retainUntilComplete];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
BOOL isBackupEnabled = [OWSBackup.sharedManager isBackupEnabled];
|
||||
|
||||
if (self.iCloudError) {
|
||||
OWSTableSection *iCloudSection = [OWSTableSection new];
|
||||
iCloudSection.headerTitle = NSLocalizedString(
|
||||
@"SETTINGS_BACKUP_ICLOUD_STATUS", @"Label for iCloud status row in the in the backup settings view.");
|
||||
[iCloudSection
|
||||
addItem:[OWSTableItem
|
||||
longDisclosureItemWithText:[OWSBackupAPI errorMessageForCloudKitAccessError:self.iCloudError]
|
||||
actionBlock:^{
|
||||
[[UIApplication sharedApplication]
|
||||
openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
|
||||
}]];
|
||||
[contents addSection:iCloudSection];
|
||||
}
|
||||
|
||||
// TODO: This UI is temporary.
|
||||
// Enabling backup will involve entering and registering a PIN.
|
||||
OWSTableSection *enableSection = [OWSTableSection new];
|
||||
enableSection.headerTitle = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings.");
|
||||
[enableSection
|
||||
addItem:[OWSTableItem switchItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_BACKUP_ENABLING_SWITCH",
|
||||
@"Label for switch in settings that controls whether or not backup is enabled.")
|
||||
isOnBlock:^{
|
||||
return [OWSBackup.sharedManager isBackupEnabled];
|
||||
}
|
||||
target:self
|
||||
selector:@selector(isBackupEnabledDidChange:)]];
|
||||
[contents addSection:enableSection];
|
||||
|
||||
if (isBackupEnabled) {
|
||||
// TODO: This UI is temporary.
|
||||
// Enabling backup will involve entering and registering a PIN.
|
||||
OWSTableSection *progressSection = [OWSTableSection new];
|
||||
[progressSection
|
||||
addItem:[OWSTableItem
|
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_STATUS",
|
||||
@"Label for backup status row in the in the backup settings view.")
|
||||
accessoryText:NSStringForBackupExportState(OWSBackup.sharedManager.backupExportState)]];
|
||||
if (OWSBackup.sharedManager.backupExportState == OWSBackupState_InProgress) {
|
||||
if (OWSBackup.sharedManager.backupExportDescription) {
|
||||
[progressSection
|
||||
addItem:[OWSTableItem
|
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PHASE",
|
||||
@"Label for phase row in the in the backup settings view.")
|
||||
accessoryText:OWSBackup.sharedManager.backupExportDescription]];
|
||||
if (OWSBackup.sharedManager.backupExportProgress) {
|
||||
NSUInteger progressPercent
|
||||
= (NSUInteger)round(OWSBackup.sharedManager.backupExportProgress.floatValue * 100);
|
||||
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
|
||||
[numberFormatter setNumberStyle:NSNumberFormatterPercentStyle];
|
||||
[numberFormatter setMaximumFractionDigits:0];
|
||||
[numberFormatter setMultiplier:@1];
|
||||
NSString *progressString = [numberFormatter stringFromNumber:@(progressPercent)];
|
||||
[progressSection
|
||||
addItem:[OWSTableItem
|
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PROGRESS",
|
||||
@"Label for phase row in the in the backup settings view.")
|
||||
accessoryText:progressString]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (OWSBackup.sharedManager.backupExportState) {
|
||||
case OWSBackupState_Idle:
|
||||
case OWSBackupState_Failed:
|
||||
case OWSBackupState_Succeeded:
|
||||
[progressSection
|
||||
addItem:[OWSTableItem disclosureItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_BACKUP_BACKUP_NOW",
|
||||
@"Label for 'backup now' button in the backup settings view.")
|
||||
actionBlock:^{
|
||||
[OWSBackup.sharedManager tryToExportBackup];
|
||||
}]];
|
||||
break;
|
||||
case OWSBackupState_InProgress:
|
||||
[progressSection
|
||||
addItem:[OWSTableItem disclosureItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_BACKUP_CANCEL_BACKUP",
|
||||
@"Label for 'cancel backup' button in the backup settings view.")
|
||||
actionBlock:^{
|
||||
[OWSBackup.sharedManager cancelExportBackup];
|
||||
}]];
|
||||
break;
|
||||
}
|
||||
|
||||
[contents addSection:progressSection];
|
||||
}
|
||||
|
||||
self.contents = contents;
|
||||
}
|
||||
|
||||
- (void)isBackupEnabledDidChange:(UISwitch *)sender
|
||||
{
|
||||
[OWSBackup.sharedManager setIsBackupEnabled:sender.isOn];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)backupStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateICloudStatus];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,23 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Notes:
|
||||
//
|
||||
// * On disk, we only bother cleaning up files, not directories.
|
||||
@interface OWSOrphanDataCleaner : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
// This is exposed for the debug UI.
|
||||
+ (void)auditAndCleanup:(BOOL)shouldCleanup;
|
||||
// This is exposed for the tests.
|
||||
+ (void)auditAndCleanup:(BOOL)shouldCleanup completion:(dispatch_block_t)completion;
|
||||
|
||||
+ (void)auditOnLaunchIfNecessary;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,737 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSOrphanDataCleaner.h"
|
||||
#import "DateUtil.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SessionMessagingKit/OWSUserProfile.h>
|
||||
#import <SessionMessagingKit/AppReadiness.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSInteraction.h>
|
||||
#import <SessionMessagingKit/TSMessage.h>
|
||||
#import <SessionMessagingKit/TSQuotedMessage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/YapDatabaseTransaction+OWS.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// LOG_ALL_FILE_PATHS can be used to determine if there are other kinds of files
|
||||
// that we're not cleaning up.
|
||||
//#define LOG_ALL_FILE_PATHS
|
||||
|
||||
#define ENABLE_ORPHAN_DATA_CLEANER
|
||||
|
||||
NSString *const OWSOrphanDataCleaner_Collection = @"OWSOrphanDataCleaner_Collection";
|
||||
NSString *const OWSOrphanDataCleaner_LastCleaningVersionKey = @"OWSOrphanDataCleaner_LastCleaningVersionKey";
|
||||
NSString *const OWSOrphanDataCleaner_LastCleaningDateKey = @"OWSOrphanDataCleaner_LastCleaningDateKey";
|
||||
|
||||
@interface OWSOrphanData : NSObject
|
||||
|
||||
@property (nonatomic) NSSet<NSString *> *interactionIds;
|
||||
@property (nonatomic) NSSet<NSString *> *attachmentIds;
|
||||
@property (nonatomic) NSSet<NSString *> *filePaths;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSOrphanData
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
typedef void (^OrphanDataBlock)(OWSOrphanData *);
|
||||
|
||||
@implementation OWSOrphanDataCleaner
|
||||
|
||||
// Unlike CurrentAppContext().isMainAppAndActive, this method can be safely
|
||||
// invoked off the main thread.
|
||||
+ (BOOL)isMainAppAndActive
|
||||
{
|
||||
return CurrentAppContext().reportedApplicationState == UIApplicationStateActive;
|
||||
}
|
||||
|
||||
+ (void)printPaths:(NSArray<NSString *> *)paths label:(NSString *)label
|
||||
{
|
||||
for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
OWSLogDebug(@"%@: %@", label, path);
|
||||
}
|
||||
}
|
||||
|
||||
+ (long long)fileSizeOfFilePath:(NSString *)filePath
|
||||
{
|
||||
NSError *error;
|
||||
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize];
|
||||
if (error) {
|
||||
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == 260) {
|
||||
OWSLogWarn(@"can't find size of missing file.");
|
||||
OWSLogDebug(@"can't find size of missing file: %@", filePath);
|
||||
} else {
|
||||
OWSFailDebug(@"attributesOfItemAtPath: %@ error: %@", filePath, error);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return fileSize.longLongValue;
|
||||
}
|
||||
|
||||
+ (nullable NSNumber *)fileSizeOfFilePathsSafe:(NSArray<NSString *> *)filePaths
|
||||
{
|
||||
long long result = 0;
|
||||
for (NSString *filePath in filePaths) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
result += [self fileSizeOfFilePath:filePath];
|
||||
}
|
||||
return @(result);
|
||||
}
|
||||
|
||||
+ (nullable NSSet<NSString *> *)filePathsInDirectorySafe:(NSString *)dirPath
|
||||
{
|
||||
NSMutableSet *filePaths = [NSMutableSet new];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) {
|
||||
return filePaths;
|
||||
}
|
||||
NSError *error;
|
||||
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
|
||||
if (error) {
|
||||
OWSFailDebug(@"contentsOfDirectoryAtPath error: %@", error);
|
||||
return [NSSet new];
|
||||
}
|
||||
for (NSString *fileName in fileNames) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
|
||||
BOOL isDirectory;
|
||||
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
|
||||
if (isDirectory) {
|
||||
NSSet<NSString *> *_Nullable dirPaths = [self filePathsInDirectorySafe:filePath];
|
||||
if (!dirPaths) {
|
||||
return nil;
|
||||
}
|
||||
[filePaths unionSet:dirPaths];
|
||||
} else {
|
||||
[filePaths addObject:filePath];
|
||||
}
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// This method finds (but does not delete):
|
||||
//
|
||||
// * Orphan TSInteractions (with no thread).
|
||||
// * Orphan TSAttachments (with no message).
|
||||
// * Orphan attachment files (with no corresponding TSAttachment).
|
||||
// * Orphan profile avatars.
|
||||
// * Temporary files (all).
|
||||
//
|
||||
// It also finds (we don't clean these up).
|
||||
//
|
||||
// * Missing attachment files (cannot be cleaned up).
|
||||
// These are attachments which have no file on disk. They should be extremely rare -
|
||||
// the only cases I have seen are probably due to debugging.
|
||||
// They can't be cleaned up - we don't want to delete the TSAttachmentStream or
|
||||
// its corresponding message. Better that the broken message shows up in the
|
||||
// conversation view.
|
||||
+ (void)findOrphanDataWithRetries:(NSInteger)remainingRetries
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
success:(OrphanDataBlock)success
|
||||
failure:(dispatch_block_t)failure
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
if (remainingRetries < 1) {
|
||||
OWSLogInfo(@"Aborting orphan data search.");
|
||||
dispatch_async(self.workQueue, ^{
|
||||
failure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until the app is active...
|
||||
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
|
||||
// ...but perform the work off the main thread.
|
||||
dispatch_async(self.workQueue, ^{
|
||||
OWSOrphanData *_Nullable orphanData = [self findOrphanDataSync:databaseConnection];
|
||||
if (orphanData) {
|
||||
success(orphanData);
|
||||
} else {
|
||||
[self findOrphanDataWithRetries:remainingRetries - 1
|
||||
databaseConnection:databaseConnection
|
||||
success:success
|
||||
failure:failure];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns nil on failure, usually indicating that the search
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (nullable OWSOrphanData *)findOrphanDataSync:(YapDatabaseConnection *)databaseConnection
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
__block BOOL shouldAbort = NO;
|
||||
|
||||
#ifdef LOG_ALL_FILE_PATHS
|
||||
{
|
||||
NSString *documentDirPath = [OWSFileSystem appDocumentDirectoryPath];
|
||||
NSArray<NSString *> *_Nullable allDocumentFilePaths =
|
||||
[self filePathsInDirectorySafe:documentDirPath].allObjects;
|
||||
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
|
||||
for (NSString *filePath in allDocumentFilePaths) {
|
||||
if ([filePath hasPrefix:attachmentsFolder]) {
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(@"non-attachment file: %@", filePath);
|
||||
}
|
||||
}
|
||||
{
|
||||
NSString *documentDirPath = [OWSFileSystem appSharedDataDirectoryPath];
|
||||
NSArray<NSString *> *_Nullable allDocumentFilePaths =
|
||||
[self filePathsInDirectorySafe:documentDirPath].allObjects;
|
||||
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
|
||||
for (NSString *filePath in allDocumentFilePaths) {
|
||||
if ([filePath hasPrefix:attachmentsFolder]) {
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(@"non-attachment file: %@", filePath);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// We treat _all_ temp files as orphan files. This is safe
|
||||
// because temp files only need to be retained for the
|
||||
// a single launch of the app. Since our "date threshold"
|
||||
// for deletion is relative to the current launch time,
|
||||
// all temp files currently in use should be safe.
|
||||
NSArray<NSString *> *_Nullable tempFilePaths = [self getTempFilePaths];
|
||||
if (!tempFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#ifdef LOG_ALL_FILE_PATHS
|
||||
{
|
||||
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||
[dateFormatter setDateStyle:NSDateFormatterLongStyle];
|
||||
[dateFormatter setTimeStyle:NSDateFormatterLongStyle];
|
||||
|
||||
tempFilePaths = [tempFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSString *filePath in tempFilePaths) {
|
||||
NSError *error;
|
||||
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
|
||||
if (!attributes || error) {
|
||||
OWSLogDebug(@"Could not get attributes of file at: %@", filePath);
|
||||
OWSFailDebug(@"Could not get attributes of file");
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(
|
||||
@"temp file: %@, %@", filePath, [dateFormatter stringFromDate:attributes.fileModificationDate]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
NSString *legacyAttachmentsDirPath = TSAttachmentStream.legacyAttachmentsDirPath;
|
||||
NSString *sharedDataAttachmentsDirPath = TSAttachmentStream.sharedDataAttachmentsDirPath;
|
||||
NSSet<NSString *> *_Nullable legacyAttachmentFilePaths = [self filePathsInDirectorySafe:legacyAttachmentsDirPath];
|
||||
if (!legacyAttachmentFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSSet<NSString *> *_Nullable sharedDataAttachmentFilePaths =
|
||||
[self filePathsInDirectorySafe:sharedDataAttachmentsDirPath];
|
||||
if (!sharedDataAttachmentFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *legacyProfileAvatarsDirPath = OWSUserProfile.legacyProfileAvatarsDirPath;
|
||||
NSString *sharedDataProfileAvatarsDirPath = OWSUserProfile.sharedDataProfileAvatarsDirPath;
|
||||
NSSet<NSString *> *_Nullable legacyProfileAvatarsFilePaths =
|
||||
[self filePathsInDirectorySafe:legacyProfileAvatarsDirPath];
|
||||
if (!legacyProfileAvatarsFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSSet<NSString *> *_Nullable sharedDataProfileAvatarFilePaths =
|
||||
[self filePathsInDirectorySafe:sharedDataProfileAvatarsDirPath];
|
||||
if (!sharedDataProfileAvatarFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableSet<NSString *> *allOnDiskFilePaths = [NSMutableSet new];
|
||||
[allOnDiskFilePaths unionSet:legacyAttachmentFilePaths];
|
||||
[allOnDiskFilePaths unionSet:sharedDataAttachmentFilePaths];
|
||||
[allOnDiskFilePaths unionSet:legacyProfileAvatarsFilePaths];
|
||||
[allOnDiskFilePaths unionSet:sharedDataProfileAvatarFilePaths];
|
||||
[allOnDiskFilePaths addObjectsFromArray:tempFilePaths];
|
||||
|
||||
NSSet<NSString *> *profileAvatarFilePaths = [OWSUserProfile allProfileAvatarFilePaths];
|
||||
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSNumber *_Nullable totalFileSize = [self fileSizeOfFilePathsSafe:allOnDiskFilePaths.allObjects];
|
||||
|
||||
if (!totalFileSize || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUInteger fileCount = allOnDiskFilePaths.count;
|
||||
|
||||
// Attachments
|
||||
__block int attachmentStreamCount = 0;
|
||||
NSMutableSet<NSString *> *allAttachmentFilePaths = [NSMutableSet new];
|
||||
NSMutableSet<NSString *> *allAttachmentIds = [NSMutableSet new];
|
||||
// Threads
|
||||
__block NSSet *threadIds;
|
||||
// Messages
|
||||
NSMutableSet<NSString *> *orphanInteractionIds = [NSMutableSet new];
|
||||
NSMutableSet<NSString *> *allMessageAttachmentIds = [NSMutableSet new];
|
||||
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
[transaction
|
||||
enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection
|
||||
usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
return;
|
||||
}
|
||||
[allAttachmentIds addObject:attachment.uniqueId];
|
||||
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
attachmentStreamCount++;
|
||||
NSString *_Nullable filePath = [attachmentStream originalFilePath];
|
||||
if (filePath) {
|
||||
[allAttachmentFilePaths addObject:filePath];
|
||||
} else {
|
||||
OWSFailDebug(@"attachment has no file path.");
|
||||
}
|
||||
|
||||
[allAttachmentFilePaths
|
||||
addObjectsFromArray:attachmentStream.allThumbnailPaths];
|
||||
}];
|
||||
|
||||
if (shouldAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
threadIds = [NSSet setWithArray:[transaction allKeysInCollection:TSThread.collection]];
|
||||
|
||||
[transaction
|
||||
enumerateKeysAndObjectsInCollection:TSMessage.collection
|
||||
usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (interaction.uniqueThreadId.length < 1
|
||||
|| ![threadIds containsObject:interaction.uniqueThreadId]) {
|
||||
[orphanInteractionIds addObject:interaction.uniqueId];
|
||||
}
|
||||
|
||||
if (![interaction isKindOfClass:[TSMessage class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
TSMessage *message = (TSMessage *)interaction;
|
||||
[allMessageAttachmentIds addObjectsFromArray:message.allAttachmentIds];
|
||||
}];
|
||||
}];
|
||||
if (shouldAbort) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
OWSLogDebug(@"fileCount: %zu", fileCount);
|
||||
OWSLogDebug(@"totalFileSize: %lld", totalFileSize.longLongValue);
|
||||
OWSLogDebug(@"attachmentStreams: %d", attachmentStreamCount);
|
||||
OWSLogDebug(@"attachmentStreams with file paths: %zu", allAttachmentFilePaths.count);
|
||||
|
||||
NSMutableSet<NSString *> *orphanFilePaths = [allOnDiskFilePaths mutableCopy];
|
||||
[orphanFilePaths minusSet:allAttachmentFilePaths];
|
||||
[orphanFilePaths minusSet:profileAvatarFilePaths];
|
||||
NSMutableSet<NSString *> *missingAttachmentFilePaths = [allAttachmentFilePaths mutableCopy];
|
||||
[missingAttachmentFilePaths minusSet:allOnDiskFilePaths];
|
||||
|
||||
OWSLogDebug(@"orphan file paths: %zu", orphanFilePaths.count);
|
||||
OWSLogDebug(@"missing attachment file paths: %zu", missingAttachmentFilePaths.count);
|
||||
|
||||
[self printPaths:orphanFilePaths.allObjects label:@"orphan file paths"];
|
||||
[self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"];
|
||||
|
||||
OWSLogDebug(@"attachmentIds: %zu", allAttachmentIds.count);
|
||||
OWSLogDebug(@"allMessageAttachmentIds: %zu", allMessageAttachmentIds.count);
|
||||
|
||||
NSMutableSet<NSString *> *orphanAttachmentIds = [allAttachmentIds mutableCopy];
|
||||
[orphanAttachmentIds minusSet:allMessageAttachmentIds];
|
||||
NSMutableSet<NSString *> *missingAttachmentIds = [allMessageAttachmentIds mutableCopy];
|
||||
[missingAttachmentIds minusSet:allAttachmentIds];
|
||||
|
||||
OWSLogDebug(@"orphan attachmentIds: %zu", orphanAttachmentIds.count);
|
||||
OWSLogDebug(@"missing attachmentIds: %zu", missingAttachmentIds.count);
|
||||
OWSLogDebug(@"orphan interactions: %zu", orphanInteractionIds.count);
|
||||
|
||||
OWSOrphanData *result = [OWSOrphanData new];
|
||||
result.interactionIds = [orphanInteractionIds copy];
|
||||
result.attachmentIds = [orphanAttachmentIds copy];
|
||||
result.filePaths = [orphanFilePaths copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldAuditOnLaunch:(YapDatabaseConnection *)databaseConnection {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
#ifndef ENABLE_ORPHAN_DATA_CLEANER
|
||||
return NO;
|
||||
#endif
|
||||
|
||||
__block NSString *_Nullable lastCleaningVersion;
|
||||
__block NSDate *_Nullable lastCleaningDate;
|
||||
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
lastCleaningVersion = [transaction stringForKey:OWSOrphanDataCleaner_LastCleaningVersionKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
lastCleaningDate = [transaction dateForKey:OWSOrphanDataCleaner_LastCleaningDateKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
}];
|
||||
|
||||
// Clean up once per app version.
|
||||
NSString *currentAppVersion = AppVersion.sharedInstance.currentAppVersion;
|
||||
if (!lastCleaningVersion || ![lastCleaningVersion isEqualToString:currentAppVersion]) {
|
||||
OWSLogVerbose(@"Performing orphan data cleanup; new version: %@.", currentAppVersion);
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Clean up once per N days.
|
||||
if (lastCleaningDate) {
|
||||
#ifdef DEBUG
|
||||
BOOL shouldAudit = [DateUtil dateIsOlderThanToday:lastCleaningDate];
|
||||
#else
|
||||
BOOL shouldAudit = [DateUtil dateIsOlderThanOneWeek:lastCleaningDate];
|
||||
#endif
|
||||
|
||||
if (shouldAudit) {
|
||||
OWSLogVerbose(@"Performing orphan data cleanup; time has passed.");
|
||||
}
|
||||
return shouldAudit;
|
||||
}
|
||||
|
||||
// Has never audited before.
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void)auditOnLaunchIfNecessary {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
|
||||
YapDatabaseConnection *databaseConnection = [primaryStorage newDatabaseConnection];
|
||||
|
||||
if (![self shouldAuditOnLaunch:databaseConnection]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we want to be cautious, we can disable orphan deletion using
|
||||
// flag - the cleanup will just be a dry run with logging.
|
||||
BOOL shouldRemoveOrphans = YES;
|
||||
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection completion:nil];
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans
|
||||
{
|
||||
[self auditAndCleanup:shouldRemoveOrphans
|
||||
completion:^ {
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans completion:(dispatch_block_t)completion
|
||||
{
|
||||
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
|
||||
YapDatabaseConnection *databaseConnection = [primaryStorage newDatabaseConnection];
|
||||
|
||||
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection completion:completion];
|
||||
}
|
||||
|
||||
// We use the lowest priority possible.
|
||||
+ (dispatch_queue_t)workQueue
|
||||
{
|
||||
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
completion:(nullable dispatch_block_t)completion
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
if (!AppReadiness.isAppReady) {
|
||||
OWSFailDebug(@"can't audit orphan data until app is ready.");
|
||||
return;
|
||||
}
|
||||
if (!CurrentAppContext().isMainApp) {
|
||||
OWSFailDebug(@"can't audit orphan data in app extensions.");
|
||||
return;
|
||||
}
|
||||
if (CurrentAppContext().isRunningTests) {
|
||||
OWSLogVerbose(@"Ignoring audit orphan data in tests.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Orphan cleanup has two risks:
|
||||
//
|
||||
// * As a long-running process that involves access to the
|
||||
// shared data container, it could cause 0xdead10cc.
|
||||
// * It could accidentally delete data still in use,
|
||||
// e.g. a profile avatar which has been saved to disk
|
||||
// but whose OWSUserProfile hasn't been saved yet.
|
||||
//
|
||||
// To prevent 0xdead10cc, the cleaner continually checks
|
||||
// whether the app has resigned active. If so, it aborts.
|
||||
// Each phase (search, re-search, processing) retries N times,
|
||||
// then gives up until the next app launch.
|
||||
//
|
||||
// To prevent accidental data deletion, we take the following
|
||||
// measures:
|
||||
//
|
||||
// * Only cleanup data of the following types (which should
|
||||
// include all relevant app data): profile avatar,
|
||||
// attachment, temporary files (including temporary
|
||||
// attachments).
|
||||
// * We don't delete any data created more recently than N seconds
|
||||
// _before_ when the app launched. This prevents any stray data
|
||||
// currently in use by the app from being accidentally cleaned
|
||||
// up.
|
||||
const NSInteger kMaxRetries = 3;
|
||||
[self findOrphanDataWithRetries:kMaxRetries
|
||||
databaseConnection:databaseConnection
|
||||
success:^(OWSOrphanData *orphanData) {
|
||||
[self processOrphans:orphanData
|
||||
remainingRetries:kMaxRetries
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans
|
||||
success:^{
|
||||
OWSLogInfo(@"Completed orphan data cleanup.");
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[transaction setObject:AppVersion.sharedInstance.currentAppVersion
|
||||
forKey:OWSOrphanDataCleaner_LastCleaningVersionKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
[transaction setDate:[NSDate new]
|
||||
forKey:OWSOrphanDataCleaner_LastCleaningDateKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
}];
|
||||
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}
|
||||
failure:^{
|
||||
OWSLogInfo(@"Aborting orphan data cleanup.");
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
failure:^{
|
||||
OWSLogInfo(@"Aborting orphan data cleanup.");
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns NO on failure, usually indicating that orphan processing
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (void)processOrphans:(OWSOrphanData *)orphanData
|
||||
remainingRetries:(NSInteger)remainingRetries
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
|
||||
success:(dispatch_block_t)success
|
||||
failure:(dispatch_block_t)failure
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
OWSAssertDebug(orphanData);
|
||||
|
||||
if (remainingRetries < 1) {
|
||||
OWSLogInfo(@"Aborting orphan data audit.");
|
||||
dispatch_async(self.workQueue, ^{
|
||||
failure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until the app is active...
|
||||
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
|
||||
// ...but perform the work off the main thread.
|
||||
dispatch_async(self.workQueue, ^{
|
||||
if ([self processOrphansSync:orphanData
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans]) {
|
||||
success();
|
||||
return;
|
||||
} else {
|
||||
[self processOrphans:orphanData
|
||||
remainingRetries:remainingRetries - 1
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans
|
||||
success:success
|
||||
failure:failure];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns NO on failure, usually indicating that orphan processing
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (BOOL)processOrphansSync:(OWSOrphanData *)orphanData
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
OWSAssertDebug(orphanData);
|
||||
|
||||
__block BOOL shouldAbort = NO;
|
||||
|
||||
// We need to avoid cleaning up new files that are still in the process of
|
||||
// being created/written, so we don't clean up anything recent.
|
||||
const NSTimeInterval kMinimumOrphanAgeSeconds = CurrentAppContext().isRunningTests ? 0.f : 15 * kMinuteInterval;
|
||||
NSDate *appLaunchTime = CurrentAppContext().appLaunchTime;
|
||||
NSTimeInterval thresholdTimestamp = appLaunchTime.timeIntervalSince1970 - kMinimumOrphanAgeSeconds;
|
||||
NSDate *thresholdDate = [NSDate dateWithTimeIntervalSince1970:thresholdTimestamp];
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSUInteger interactionsRemoved = 0;
|
||||
for (NSString *interactionId in orphanData.interactionIds) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
return;
|
||||
}
|
||||
TSInteraction *_Nullable interaction =
|
||||
[TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
|
||||
if (!interaction) {
|
||||
// This could just be a race condition, but it should be very unlikely.
|
||||
OWSLogWarn(@"Could not load interaction: %@", interactionId);
|
||||
continue;
|
||||
}
|
||||
// Don't delete interactions which were created in the last N minutes.
|
||||
NSDate *creationDate = [NSDate ows_dateWithMillisecondsSince1970:interaction.timestamp];
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping orphan interaction due to age: %f", fabs(creationDate.timeIntervalSinceNow));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Removing orphan message: %@", interaction.uniqueId);
|
||||
interactionsRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[interaction removeWithTransaction:transaction];
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan interactions: %zu", interactionsRemoved);
|
||||
|
||||
NSUInteger attachmentsRemoved = 0;
|
||||
for (NSString *attachmentId in orphanData.attachmentIds) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
return;
|
||||
}
|
||||
TSAttachment *_Nullable attachment =
|
||||
[TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
|
||||
if (!attachment) {
|
||||
// This can happen on launch since we sync contacts/groups, especially if you have a lot of attachments
|
||||
// to churn through, it's likely it's been deleted since starting this job.
|
||||
OWSLogWarn(@"Could not load attachment: %@", attachmentId);
|
||||
continue;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
continue;
|
||||
}
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
// Don't delete attachments which were created in the last N minutes.
|
||||
NSDate *creationDate = attachmentStream.creationTimestamp;
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping orphan attachment due to age: %f", fabs(creationDate.timeIntervalSinceNow));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Removing orphan attachmentStream: %@", attachmentStream.uniqueId);
|
||||
attachmentsRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[attachmentStream removeWithTransaction:transaction];
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan attachments: %zu", attachmentsRemoved);
|
||||
}];
|
||||
|
||||
if (shouldAbort) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUInteger filesRemoved = 0;
|
||||
NSArray<NSString *> *filePaths = [orphanData.filePaths.allObjects sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSString *filePath in filePaths) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
|
||||
if (!attributes || error) {
|
||||
// This is fine; the file may have been deleted since we found it.
|
||||
OWSLogWarn(@"Could not get attributes of file at: %@", filePath);
|
||||
continue;
|
||||
}
|
||||
// Don't delete files which were created in the last N minutes.
|
||||
NSDate *creationDate = attributes.fileModificationDate;
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping file due to age: %f", fabs([creationDate timeIntervalSinceNow]));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Deleting file: %@", filePath);
|
||||
filesRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
||||
if (error) {
|
||||
OWSLogDebug(@"Could not remove orphan file at: %@", filePath);
|
||||
OWSFailDebug(@"Could not remove orphan file");
|
||||
}
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan files: %zu", filesRemoved);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (nullable NSArray<NSString *> *)getTempFilePaths
|
||||
{
|
||||
NSString *dir1 = OWSTemporaryDirectory();
|
||||
NSArray<NSString *> *_Nullable paths1 = [[self filePathsInDirectorySafe:dir1].allObjects mutableCopy];
|
||||
|
||||
NSString *dir2 = OWSTemporaryDirectoryAccessibleAfterFirstAuth();
|
||||
NSArray<NSString *> *_Nullable paths2 = [[self filePathsInDirectorySafe:dir2].allObjects mutableCopy];
|
||||
|
||||
if (paths1 && paths2) {
|
||||
return [paths1 arrayByAddingObjectsFromArray:paths2];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,37 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public class AppPreferences: NSObject {
|
||||
// Never instantiate this class.
|
||||
private override init() {}
|
||||
|
||||
private static let collection = "AppPreferences"
|
||||
|
||||
// MARK: -
|
||||
|
||||
private static let hasDimissedFirstConversationCueKey = "hasDimissedFirstConversationCue"
|
||||
|
||||
@objc
|
||||
public static var hasDimissedFirstConversationCue: Bool {
|
||||
get {
|
||||
return getBool(key: hasDimissedFirstConversationCueKey)
|
||||
}
|
||||
set {
|
||||
setBool(newValue, key: hasDimissedFirstConversationCueKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class func getBool(key: String, defaultValue: Bool = false) -> Bool {
|
||||
return OWSPrimaryStorage.dbReadConnection().bool(forKey: key, inCollection: collection, defaultValue: defaultValue)
|
||||
}
|
||||
|
||||
private class func setBool(_ value: Bool, key: String) {
|
||||
OWSPrimaryStorage.dbReadWriteConnection().setBool(value, forKey: key, inCollection: collection)
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
|
||||
extension Storage : SessionMessagingKitStorageProtocol, SessionSnodeKitStorageProtocol {
|
||||
|
||||
public func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
OWSPrimaryStorage.shared().updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, in: transaction)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Signal_TSStorageHeaders_h
|
||||
#define Signal_TSStorageHeaders_h
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+keyFromIntLong.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
|
||||
#endif
|
@ -1,32 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#ifndef TextSecureKit_TSStorageKeys_h
|
||||
#define TextSecureKit_TSStorageKeys_h
|
||||
|
||||
/**
|
||||
* Preferences exposed to the user
|
||||
*/
|
||||
|
||||
#pragma mark User Preferences
|
||||
|
||||
#define TSStorageUserPreferencesCollection @"TSStorageUserPreferencesCollection"
|
||||
|
||||
|
||||
/**
|
||||
* Internal settings of the application, not exposed to the user.
|
||||
*/
|
||||
|
||||
#pragma mark Internal Settings
|
||||
|
||||
#define TSStorageInternalSettingsCollection @"TSStorageInternalSettingsCollection"
|
||||
#define TSStorageInternalSettingsVersion @"TSLastLaunchedVersion"
|
||||
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSSearchBar : UISearchBar
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,118 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSSearchBar.h"
|
||||
#import "Theme.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation OWSSearchBar
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)ows_configure
|
||||
{
|
||||
[self ows_applyTheme];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(themeDidChange:)
|
||||
name:ThemeDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)ows_applyTheme
|
||||
{
|
||||
[self.class applyThemeToSearchBar:self];
|
||||
}
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
UIColor *foregroundColor = UIColor.lokiLightestGray;
|
||||
searchBar.barTintColor = Theme.backgroundColor;
|
||||
searchBar.barStyle = Theme.barStyle;
|
||||
searchBar.tintColor = UIColor.lokiGreen;
|
||||
|
||||
// Hide searchBar border.
|
||||
// Alternatively we could hide the border by using `UISearchBarStyleMinimal`, but that causes an issue when toggling
|
||||
// from light -> dark -> light theme wherein the textField background color appears darker than it should
|
||||
// (regardless of our re-setting textfield.backgroundColor below).
|
||||
searchBar.backgroundImage = [UIImage new];
|
||||
|
||||
if (Theme.isDarkThemeEnabled) {
|
||||
UIImage *clearImage = [UIImage imageNamed:@"searchbar_clear"];
|
||||
[searchBar setImage:[clearImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconClear
|
||||
state:UIControlStateNormal];
|
||||
|
||||
UIImage *searchImage = [UIImage imageNamed:@"searchbar_search"];
|
||||
[searchBar setImage:[searchImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconSearch
|
||||
state:UIControlStateNormal];
|
||||
} else {
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
|
||||
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
|
||||
}
|
||||
|
||||
[searchBar traverseViewHierarchyWithVisitor:^(UIView *view) {
|
||||
if ([view isKindOfClass:[UITextField class]]) {
|
||||
UITextField *textField = (UITextField *)view;
|
||||
textField.backgroundColor = Theme.searchFieldBackgroundColor;
|
||||
textField.textColor = Theme.primaryColor;
|
||||
NSString *placeholder = textField.placeholder;
|
||||
if (placeholder != nil) {
|
||||
NSMutableAttributedString *attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:placeholder];
|
||||
[attributedPlaceholder addAttribute:NSForegroundColorAttributeName value:foregroundColor range:NSMakeRange(0, placeholder.length)];
|
||||
textField.attributedPlaceholder = attributedPlaceholder;
|
||||
}
|
||||
textField.keyboardAppearance = LKAppModeUtilities.isLightMode ? UIKeyboardAppearanceDefault : UIKeyboardAppearanceDark;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)themeDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self ows_applyTheme];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,13 +0,0 @@
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <Curve25519Kit/Ed25519.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSPrimaryStorage (Loki)
|
||||
|
||||
- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet<NSString *> *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(updateMessageIDCollectionByPruningMessagesWithIDs(_:in:));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,26 +0,0 @@
|
||||
#import "OWSPrimaryStorage+Loki.h"
|
||||
#import "OWSPrimaryStorage+keyFromIntLong.h"
|
||||
#import "OWSIdentityManager.h"
|
||||
#import "NSDate+OWS.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import "YapDatabaseConnection+OWS.h"
|
||||
#import "YapDatabaseTransaction+OWS.h"
|
||||
#import "NSObject+Casting.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
#define LKMessageIDCollection @"LKMessageIDCollection"
|
||||
|
||||
@implementation OWSPrimaryStorage (Loki)
|
||||
|
||||
- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet<NSString *> *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
NSMutableArray<NSString *> *serverIDs = [NSMutableArray new];
|
||||
[transaction enumerateRowsInCollection:LKMessageIDCollection usingBlock:^(NSString *key, id object, id metadata, BOOL *stop) {
|
||||
if (![object isKindOfClass:NSString.class]) { return; }
|
||||
NSString *messageID = (NSString *)object;
|
||||
if (![targetMessageIDs containsObject:messageID]) { return; }
|
||||
[serverIDs addObject:key];
|
||||
}];
|
||||
[transaction removeObjectsForKeys:serverIDs inCollection:LKMessageIDCollection];
|
||||
}
|
||||
|
||||
@end
|
@ -1,68 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const ThemeDidChangeNotification;
|
||||
|
||||
@interface Theme : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@property (class, readonly, atomic) BOOL isDarkThemeEnabled;
|
||||
|
||||
+ (void)setIsDarkThemeEnabled:(BOOL)value;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *backgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *primaryColor;
|
||||
@property (class, readonly, nonatomic) UIColor *secondaryColor;
|
||||
@property (class, readonly, nonatomic) UIColor *boldColor;
|
||||
@property (class, readonly, nonatomic) UIColor *offBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *middleGrayColor;
|
||||
@property (class, readonly, nonatomic) UIColor *placeholderColor;
|
||||
@property (class, readonly, nonatomic) UIColor *hairlineColor;
|
||||
|
||||
#pragma mark - Global App Colors
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *navbarBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *navbarIconColor;
|
||||
@property (class, readonly, nonatomic) UIColor *navbarTitleColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *toolbarBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *conversationButtonBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *cellSelectedColor;
|
||||
@property (class, readonly, nonatomic) UIColor *cellSeparatorColor;
|
||||
|
||||
// In some contexts, e.g. media viewing/sending, we always use "dark theme" UI regardless of the
|
||||
// users chosen theme.
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeNavbarIconColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeNavbarBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemePrimaryColor;
|
||||
@property (class, readonly, nonatomic) UIBlurEffect *darkThemeBarBlurEffect;
|
||||
@property (class, readonly, nonatomic) UIColor *galleryHighlightColor;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@property (class, readonly, nonatomic) UIBarStyle barStyle;
|
||||
@property (class, readonly, nonatomic) UIColor *searchFieldBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIBlurEffect *barBlurEffect;
|
||||
@property (class, readonly, nonatomic) UIKeyboardAppearance keyboardAppearance;
|
||||
@property (class, readonly, nonatomic) UIKeyboardAppearance darkThemeKeyboardAppearance;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *toastForegroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *toastBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *scrollButtonBackgroundColor;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,217 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Theme.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIUtil.h"
|
||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
|
||||
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const ThemeDidChangeNotification = @"ThemeDidChangeNotification";
|
||||
|
||||
NSString *const ThemeCollection = @"ThemeCollection";
|
||||
NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
|
||||
|
||||
|
||||
@interface Theme ()
|
||||
|
||||
@property (nonatomic) NSNumber *isDarkThemeEnabledNumber;
|
||||
|
||||
@end
|
||||
|
||||
@implementation Theme
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static Theme *instance;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [Theme new];
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
+ (BOOL)isDarkThemeEnabled
|
||||
{
|
||||
return LKAppModeUtilities.isDarkMode;
|
||||
}
|
||||
|
||||
- (BOOL)isDarkThemeEnabled
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
return LKAppModeUtilities.isDarkMode;
|
||||
}
|
||||
|
||||
+ (void)setIsDarkThemeEnabled:(BOOL)value
|
||||
{
|
||||
return [self.sharedInstance setIsDarkThemeEnabled:value];
|
||||
}
|
||||
|
||||
- (void)setIsDarkThemeEnabled:(BOOL)value
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
+ (UIColor *)backgroundColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)offBackgroundColor
|
||||
{
|
||||
return LKColors.unimportant;
|
||||
}
|
||||
|
||||
+ (UIColor *)primaryColor
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)secondaryColor
|
||||
{
|
||||
return LKColors.separator;
|
||||
}
|
||||
|
||||
+ (UIColor *)boldColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.blackColor);
|
||||
}
|
||||
|
||||
+ (UIColor *)middleGrayColor
|
||||
{
|
||||
return [UIColor colorWithWhite:0.5f alpha:1.f];
|
||||
}
|
||||
|
||||
+ (UIColor *)placeholderColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)hairlineColor
|
||||
{
|
||||
return LKColors.separator;
|
||||
}
|
||||
|
||||
#pragma mark - Global App Colors
|
||||
|
||||
+ (UIColor *)navbarBackgroundColor
|
||||
{
|
||||
return UIColor.lokiDarkestGray;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeNavbarBackgroundColor
|
||||
{
|
||||
return UIColor.ows_blackColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)navbarIconColor
|
||||
{
|
||||
return UIColor.lokiGreen;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeNavbarIconColor;
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)navbarTitleColor
|
||||
{
|
||||
return Theme.primaryColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)toolbarBackgroundColor
|
||||
{
|
||||
return self.navbarBackgroundColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)cellSelectedColor
|
||||
{
|
||||
return UIColor.lokiDarkGray;
|
||||
}
|
||||
|
||||
+ (UIColor *)cellSeparatorColor
|
||||
{
|
||||
return Theme.hairlineColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeBackgroundColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemePrimaryColor
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)galleryHighlightColor
|
||||
{
|
||||
return UIColor.lokiGreen;
|
||||
}
|
||||
|
||||
+ (UIColor *)conversationButtonBackgroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? [UIColor colorWithWhite:0.35f alpha:1.f] : UIColor.ows_gray02Color);
|
||||
}
|
||||
|
||||
+ (UIBlurEffect *)barBlurEffect
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? self.darkThemeBarBlurEffect
|
||||
: [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
}
|
||||
|
||||
+ (UIBlurEffect *)darkThemeBarBlurEffect
|
||||
{
|
||||
return [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
|
||||
}
|
||||
|
||||
+ (UIKeyboardAppearance)keyboardAppearance
|
||||
{
|
||||
return LKAppModeUtilities.isLightMode ? UIKeyboardAppearanceDefault : UIKeyboardAppearanceDark;
|
||||
}
|
||||
|
||||
+ (UIKeyboardAppearance)darkThemeKeyboardAppearance;
|
||||
{
|
||||
return UIKeyboardAppearanceDark;
|
||||
}
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
+ (UIBarStyle)barStyle
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? UIBarStyleBlack : UIBarStyleDefault;
|
||||
}
|
||||
|
||||
+ (UIColor *)searchFieldBackgroundColor
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? Theme.offBackgroundColor : UIColor.ows_gray05Color;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (UIColor *)toastForegroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_whiteColor);
|
||||
}
|
||||
|
||||
+ (UIColor *)toastBackgroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_gray75Color : UIColor.ows_gray60Color);
|
||||
}
|
||||
|
||||
+ (UIColor *)scrollButtonBackgroundColor
|
||||
{
|
||||
return UIColor.lokiDarkerGray;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
Loading…
Reference in New Issue