Removed the unused legacy OWSBackup code

This commit is contained in:
Morgan Pretty 2022-03-24 14:35:23 +11:00
parent b1cfa4f50a
commit 1a6c34e3b8
21 changed files with 1 additions and 5019 deletions

View File

@ -14,7 +14,6 @@
340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; };
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87E204DAC8C007AEB0F /* PrivacySettingsTableViewController.m */; };
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC883204DAC8C007AEB0F /* OWSSoundSettingsViewController.m */; };
340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */; };
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */; };
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC89A204DAC8D007AEB0F /* OWSConversationSettingsViewController.m */; };
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 341341EE2187467900192D59 /* ConversationViewModel.m */; };
@ -22,7 +21,6 @@
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; };
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */; };
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; };
346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; };
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; };
@ -35,13 +33,6 @@
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */; };
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */; };
3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3496955F21A2FC8100DCFE74 /* CloudKit.framework */; };
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956221A301A100DCFE74 /* OWSBackupExportJob.m */; };
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */; };
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956521A301A100DCFE74 /* OWSBackupIO.m */; };
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956621A301A100DCFE74 /* OWSBackupImportJob.m */; };
3496957221A301A100DCFE74 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956921A301A100DCFE74 /* OWSBackup.m */; };
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956A21A301A100DCFE74 /* OWSBackupJob.m */; };
3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */; };
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; };
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaAlbumView.swift */; };
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */; };
@ -956,12 +947,10 @@
340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = "<group>"; };
340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsViewController.m; sourceTree = "<group>"; };
340FC87E204DAC8C007AEB0F /* PrivacySettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrivacySettingsTableViewController.m; sourceTree = "<group>"; };
340FC87F204DAC8C007AEB0F /* OWSBackupSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupSettingsViewController.h; sourceTree = "<group>"; };
340FC883204DAC8C007AEB0F /* OWSSoundSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSoundSettingsViewController.m; sourceTree = "<group>"; };
340FC888204DAC8C007AEB0F /* OWSQRCodeScanningViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQRCodeScanningViewController.h; sourceTree = "<group>"; };
340FC88A204DAC8C007AEB0F /* NotificationSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationSettingsViewController.h; sourceTree = "<group>"; };
340FC88B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationSettingsOptionsViewController.h; sourceTree = "<group>"; };
340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupSettingsViewController.m; sourceTree = "<group>"; };
340FC88F204DAC8C007AEB0F /* PrivacySettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivacySettingsTableViewController.h; sourceTree = "<group>"; };
340FC894204DAC8C007AEB0F /* OWSSoundSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSoundSettingsViewController.h; sourceTree = "<group>"; };
340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQRCodeScanningViewController.m; sourceTree = "<group>"; };
@ -976,7 +965,6 @@
34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = "<group>"; };
34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = "<group>"; };
34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = "<group>"; };
3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupRestoreViewController.swift; sourceTree = "<group>"; };
34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = "<group>"; };
34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = "<group>"; };
344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = "<group>"; };
@ -991,18 +979,6 @@
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerController.swift; sourceTree = "<group>"; };
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibrary.swift; sourceTree = "<group>"; };
3496955F21A2FC8100DCFE74 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
3496956221A301A100DCFE74 /* OWSBackupExportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupExportJob.m; sourceTree = "<group>"; };
3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestore.swift; sourceTree = "<group>"; };
3496956421A301A100DCFE74 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = "<group>"; };
3496956521A301A100DCFE74 /* OWSBackupIO.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupIO.m; sourceTree = "<group>"; };
3496956621A301A100DCFE74 /* OWSBackupImportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupImportJob.m; sourceTree = "<group>"; };
3496956721A301A100DCFE74 /* OWSBackupJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupJob.h; sourceTree = "<group>"; };
3496956821A301A100DCFE74 /* OWSBackupExportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupExportJob.h; sourceTree = "<group>"; };
3496956921A301A100DCFE74 /* OWSBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackup.m; sourceTree = "<group>"; };
3496956A21A301A100DCFE74 /* OWSBackupJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupJob.m; sourceTree = "<group>"; };
3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupAPI.swift; sourceTree = "<group>"; };
3496956C21A301A100DCFE74 /* OWSBackupImportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImportJob.h; sourceTree = "<group>"; };
3496956D21A301A100DCFE74 /* OWSBackupIO.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupIO.h; sourceTree = "<group>"; };
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = "<group>"; };
34A8B3502190A40E00218A25 /* MediaAlbumView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaAlbumView.swift; sourceTree = "<group>"; };
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationMessageMapping.swift; sourceTree = "<group>"; };
@ -2962,28 +2938,6 @@
path = Notifications;
sourceTree = "<group>";
};
C36096BC25AD1C3E008B62B2 /* Backups */ = {
isa = PBXGroup;
children = (
340FC87F204DAC8C007AEB0F /* OWSBackupSettingsViewController.h */,
340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */,
3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */,
3496956421A301A100DCFE74 /* OWSBackup.h */,
3496956921A301A100DCFE74 /* OWSBackup.m */,
3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */,
3496956821A301A100DCFE74 /* OWSBackupExportJob.h */,
3496956221A301A100DCFE74 /* OWSBackupExportJob.m */,
3496956C21A301A100DCFE74 /* OWSBackupImportJob.h */,
3496956621A301A100DCFE74 /* OWSBackupImportJob.m */,
3496956D21A301A100DCFE74 /* OWSBackupIO.h */,
3496956521A301A100DCFE74 /* OWSBackupIO.m */,
3496956721A301A100DCFE74 /* OWSBackupJob.h */,
3496956A21A301A100DCFE74 /* OWSBackupJob.m */,
3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */,
);
path = Backups;
sourceTree = "<group>";
};
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */ = {
isa = PBXGroup;
children = (
@ -3607,7 +3561,6 @@
isa = PBXGroup;
children = (
C3F0A58F255C8E3D007BE2A3 /* Meta */,
C36096BC25AD1C3E008B62B2 /* Backups */,
C360969C25AD18BA008B62B2 /* Closed Groups */,
B835246C25C38AA20089A44F /* Conversations */,
C32B405424A961E1001117B5 /* Dependencies */,
@ -4854,16 +4807,13 @@
B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */,
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */,
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */,
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */,
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */,
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */,
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */,
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */,
340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */,
34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */,
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */,
34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */,
@ -4899,7 +4849,6 @@
C3548F0624456447009433A8 /* PNModeVC.swift in Sources */,
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
D221A09A169C9E5E00537ABF /* main.m in Sources */,
3496957221A301A100DCFE74 /* OWSBackup.m in Sources */,
B835247925C38D880089A44F /* MessageCell.swift in Sources */,
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */,
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */,
@ -4909,7 +4858,6 @@
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */,
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
@ -4957,7 +4905,6 @@
B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */,
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */,
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */,
@ -4979,7 +4926,6 @@
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */,
4C21D5D6223A9DC500EF8A77 /* UIAlerts+iOS9.m in Sources */,
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */,
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */,
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */,
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
@ -4999,7 +4945,6 @@
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */,
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */,
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */,
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
@ -5016,7 +4961,6 @@
7BA7F4BD27A216B600B3A466 /* Storage+RecentSearchResults.swift in Sources */,
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */,
C374EEE225DA26740073A857 /* LinkPreviewModal.swift in Sources */,
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -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
}
}

View File

@ -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

View File

@ -1,912 +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 @[
kOWSBlockingManager_BlockListCollection,
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

View File

@ -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()
})
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:^{
resolve(@(1));
}];
});
}];
return promise;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -4,7 +4,6 @@
#import "AppDelegate.h"
#import "MainAppContext.h"
#import "OWSBackup.h"
#import "OWSOrphanDataCleaner.h"
#import "OWSScreenLockUI.h"
#import "Session-Swift.h"
@ -101,11 +100,6 @@ static NSTimeInterval launchStartedAt;
return Environment.shared.windowManager;
}
- (OWSBackup *)backup
{
return AppEnvironment.shared.backup;
}
- (OWSNotificationPresenter *)notificationPresenter
{
return AppEnvironment.shared.notificationPresenter;
@ -552,11 +546,7 @@ static NSTimeInterval launchStartedAt;
UIViewController *rootViewController;
BOOL navigationBarHidden = NO;
if ([self.tsAccountManager isRegistered]) {
if (self.backup.hasPendingRestoreDecision) {
rootViewController = [BackupRestoreViewController new];
} else {
rootViewController = [HomeVC new];
}
rootViewController = [HomeVC new];
} else {
rootViewController = [LandingVC new];
navigationBarHidden = NO;

View File

@ -34,9 +34,6 @@ import SignalUtilitiesKit
@objc
public var pushRegistrationManager: PushRegistrationManager
@objc
public var backup: OWSBackup
@objc
public var fileLogger: DDFileLogger
@ -49,15 +46,10 @@ import SignalUtilitiesKit
return _userNotificationActionHandler as! UserNotificationActionHandler
}
@objc
public var backupLazyRestore: BackupLazyRestore
private override init() {
self.accountManager = AccountManager()
self.notificationPresenter = NotificationPresenter()
self.pushRegistrationManager = PushRegistrationManager()
self.backup = OWSBackup()
self.backupLazyRestore = BackupLazyRestore()
self._userNotificationActionHandler = UserNotificationActionHandler()
self.fileLogger = DDFileLogger()

View File

@ -19,8 +19,6 @@
#import "NotificationSettingsViewController.h"
#import "OWSAnyTouchGestureRecognizer.h"
#import "OWSAudioPlayer.h"
#import "OWSBackup.h"
#import "OWSBackupIO.h"
#import "OWSBezierPathView.h"
#import "OWSConversationSettingsViewController.h"
#import "OWSDatabaseMigration.h"

View File

@ -140,9 +140,6 @@ typedef NS_ENUM(NSUInteger, OWSRegistrationState) {
- (BOOL)isDeregistered;
- (void)setIsDeregistered:(BOOL)isDeregistered;
- (BOOL)hasPendingBackupRestoreDecision;
- (void)setHasPendingBackupRestoreDecision:(BOOL)value;
#pragma mark - Re-registration
// Re-registration is the process of re-registering _with the same phone number_.

View File

@ -418,22 +418,6 @@ NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountMa
inCollection:TSAccountManager_UserAccountCollection];
}
- (BOOL)hasPendingBackupRestoreDecision
{
return [self.dbConnection boolForKey:TSAccountManager_HasPendingRestoreDecisionKey
inCollection:TSAccountManager_UserAccountCollection
defaultValue:NO];
}
- (void)setHasPendingBackupRestoreDecision:(BOOL)value
{
[self.dbConnection setBool:value
forKey:TSAccountManager_HasPendingRestoreDecisionKey
inCollection:TSAccountManager_UserAccountCollection];
[self postRegistrationStateDidChangeNotification];
}
- (BOOL)isManualMessageFetchEnabled
{
return [self.dbConnection boolForKey:TSAccountManager_ManualMessageFetchKey