diff --git a/Mixin.xcodeproj/project.pbxproj b/Mixin.xcodeproj/project.pbxproj index 2ae9acf1a3..47b614eb80 100644 --- a/Mixin.xcodeproj/project.pbxproj +++ b/Mixin.xcodeproj/project.pbxproj @@ -595,6 +595,7 @@ 7C5DFE34284F3EA3008733FC /* UserCenterTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5DFE33284F3EA3008733FC /* UserCenterTableHeaderView.swift */; }; 7C6132B627953B15002777EE /* DeleteAccountAbortWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6132B527953B15002777EE /* DeleteAccountAbortWindow.swift */; }; 7C6132B827953B4F002777EE /* DeleteAccountAbortWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7C6132B727953B4F002777EE /* DeleteAccountAbortWindow.xib */; }; + 7C62922229B9699000B3596C /* DatabaseBackupJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C62922129B9699000B3596C /* DatabaseBackupJob.swift */; }; 7C66E7B82743988500FF24C1 /* ProfileDescriptionLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C66E7B72743988500FF24C1 /* ProfileDescriptionLabel.swift */; }; 7C66F0272689D1FE006D8462 /* HomeAppsDragInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C66F0262689D1FE006D8462 /* HomeAppsDragInteraction.swift */; }; 7C66F029268A0384006D8462 /* AppPageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C66F028268A0384006D8462 /* AppPageCell.swift */; }; @@ -636,6 +637,9 @@ 7CCC801C292DC68E000B4200 /* ImageCropViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCC801A292DC68E000B4200 /* ImageCropViewController.swift */; }; 7CCE65A828D69D1D00FE944A /* TransferActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCE65A628D69D1D00FE944A /* TransferActionView.swift */; }; 7CCE65A928D69D1D00FE944A /* TransferActionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CCE65A728D69D1D00FE944A /* TransferActionView.xib */; }; + 7CD9C17229B9BE94008F8D85 /* DatabaseBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD9C17129B9BE94008F8D85 /* DatabaseBackupManager.swift */; }; + 7CD9C17429B9DDE0008F8D85 /* DatabaseRepairViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD9C17329B9DDE0008F8D85 /* DatabaseRepairViewController.swift */; }; + 7CD9C17829BB306F008F8D85 /* DatabaseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD9C17729BB306F008F8D85 /* DatabaseFile.swift */; }; 7CDBA58A28F64E5000AC3777 /* WalletTransferSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDBA58928F64E5000AC3777 /* WalletTransferSearchResultsViewController.swift */; }; 7CDBA58E28F7B6CB00AC3777 /* TransferSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDBA58D28F7B6CB00AC3777 /* TransferSearchViewController.swift */; }; 7CDF316C29890FB200421808 /* ConversationFontSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDF316B29890FB200421808 /* ConversationFontSet.swift */; }; @@ -1622,6 +1626,7 @@ 7C5DFE33284F3EA3008733FC /* UserCenterTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCenterTableHeaderView.swift; sourceTree = ""; }; 7C6132B527953B15002777EE /* DeleteAccountAbortWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountAbortWindow.swift; sourceTree = ""; }; 7C6132B727953B4F002777EE /* DeleteAccountAbortWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DeleteAccountAbortWindow.xib; sourceTree = ""; }; + 7C62922129B9699000B3596C /* DatabaseBackupJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseBackupJob.swift; sourceTree = ""; }; 7C66E7B72743988500FF24C1 /* ProfileDescriptionLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDescriptionLabel.swift; sourceTree = ""; }; 7C66F0262689D1FE006D8462 /* HomeAppsDragInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAppsDragInteraction.swift; sourceTree = ""; }; 7C66F028268A0384006D8462 /* AppPageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPageCell.swift; sourceTree = ""; }; @@ -1663,6 +1668,9 @@ 7CCC801A292DC68E000B4200 /* ImageCropViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCropViewController.swift; sourceTree = ""; }; 7CCE65A628D69D1D00FE944A /* TransferActionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferActionView.swift; sourceTree = ""; }; 7CCE65A728D69D1D00FE944A /* TransferActionView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TransferActionView.xib; sourceTree = ""; }; + 7CD9C17129B9BE94008F8D85 /* DatabaseBackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseBackupManager.swift; sourceTree = ""; }; + 7CD9C17329B9DDE0008F8D85 /* DatabaseRepairViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRepairViewController.swift; sourceTree = ""; }; + 7CD9C17729BB306F008F8D85 /* DatabaseFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFile.swift; sourceTree = ""; }; 7CDBA58928F64E5000AC3777 /* WalletTransferSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTransferSearchResultsViewController.swift; sourceTree = ""; }; 7CDBA58D28F7B6CB00AC3777 /* TransferSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSearchViewController.swift; sourceTree = ""; }; 7CDF316B29890FB200421808 /* ConversationFontSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationFontSet.swift; sourceTree = ""; }; @@ -2368,6 +2376,8 @@ 7BA24D7225342575004906AD /* HomeOverlaysCoordinator.swift */, 7BA24D76253438B9004906AD /* ViewPanningController.swift */, 94D9DF6025F89D6E00FC2F28 /* BulletinContent.swift */, + 7CD9C17729BB306F008F8D85 /* DatabaseFile.swift */, + 7CD9C17129B9BE94008F8D85 /* DatabaseBackupManager.swift */, ); path = Model; sourceTree = ""; @@ -3288,6 +3298,7 @@ DF75FF4324FE61E1008A7CF3 /* UpdateViewController.swift */, 7BDB4724215A666B008B21F9 /* SignalLoadingViewController.swift */, DFD1FBC62302CB7A00C570D4 /* DatabaseUpgradeViewController.swift */, + 7CD9C17329B9DDE0008F8D85 /* DatabaseRepairViewController.swift */, DFB2062721ABC088006E4341 /* RestoreViewController.swift */, 7B63A8622431C9EE00D0F7C7 /* CirclesViewController.swift */, 7BB0F90F2434821000BEDA97 /* CircleEditorViewController.swift */, @@ -3723,6 +3734,7 @@ 947F4AD525866D6C00B0A5F9 /* InitializeFTSJob.swift */, 842347ED2695BA6400009A39 /* InitializeBotJob.swift */, 94FCB83A264683D900CCC8FD /* TranscriptAttachmentUploadJob.swift */, + 7C62922129B9699000B3596C /* DatabaseBackupJob.swift */, ); path = Job; sourceTree = ""; @@ -4442,6 +4454,7 @@ files = ( 7BF49DD320C3DBAC00A8510E /* CaptchaManager.swift in Sources */, DF8CECE11FC3054700E40064 /* TransferTypeCell.swift in Sources */, + 7CD9C17829BB306F008F8D85 /* DatabaseFile.swift in Sources */, 7BCB8C8422BB56B8002A13CC /* DataAndStorageSettingsViewController.swift in Sources */, 9BB351671FB19ECB00EDDD2C /* ConversationDateHeaderView.swift in Sources */, DF2819752014669E001EE5FA /* RefreshAccountJob.swift in Sources */, @@ -4674,6 +4687,7 @@ E041064B23C5C3BC00A6F08E /* CoreTextLabelDelegate.swift in Sources */, 7B915F74215FB0C100A562C6 /* GiphySearchViewController.swift in Sources */, 7B7DACA623505793006AA2AC /* AudioCell.swift in Sources */, + 7CD9C17429B9DDE0008F8D85 /* DatabaseRepairViewController.swift in Sources */, 5E5CA86D2674B09100C1E113 /* ScreenLockSettingViewController.swift in Sources */, DF5D9F251F9C79E10036D5FD /* LocalizedExtension.swift in Sources */, 7B4FCCE02440A66600360F65 /* SolidBackgroundColorImageView.swift in Sources */, @@ -4792,6 +4806,7 @@ 7BFDB73920DA41E3005673CC /* Quote.swift in Sources */, 7B51DDB5223A489F008ACDBB /* LoginContext.swift in Sources */, DF5D9F281F9C79E10036D5FD /* UIApplicationExtension.swift in Sources */, + 7C62922229B9699000B3596C /* DatabaseBackupJob.swift in Sources */, DF7A4B4A1FCE6EE200F21BCB /* UIViewExtension.swift in Sources */, 7C5823D5268966A1003AA142 /* HomeAppsFolderViewController.swift in Sources */, 7BD00F1E2559711A004D8814 /* WalletSearchResultsViewController.swift in Sources */, @@ -4991,6 +5006,7 @@ 7BEE5351222D0E5C008D3911 /* ConversationExtensionCell.swift in Sources */, DF8CECF21FC4256D00E40064 /* BlockUserCell.swift in Sources */, 7CC730502745F95D002780F5 /* StickerStore.swift in Sources */, + 7CD9C17229B9BE94008F8D85 /* DatabaseBackupManager.swift in Sources */, 7B2E56B0244EA6BB0073102C /* SettingsFooterView.swift in Sources */, 7BD7534E2182CDCE00BAC172 /* IconPrefixedTextMessageCell.swift in Sources */, 7B369206233A3314007321A7 /* SharedMediaViewController.swift in Sources */, diff --git a/Mixin/AppDelegate.swift b/Mixin/AppDelegate.swift index c21f5bd335..0c013025a6 100644 --- a/Mixin/AppDelegate.swift +++ b/Mixin/AppDelegate.swift @@ -33,12 +33,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if #available(iOS 15.0, *), !ProcessInfo.processInfo.isiOSAppOnMac { UITableView.appearance().sectionHeaderTopPadding = 0 } + addObservers() checkLogin() ScreenLockManager.shared.lockScreenIfNeeded() checkJailbreak() configAnalytics() pendingShortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem - addObservers() Logger.general.info(category: "AppDelegate", message: "App \(Bundle.main.shortVersion)(\(Bundle.main.bundleVersion)) did finish launching with state: \(UIApplication.shared.applicationStateString), device: \(Device.current.machineName) \(ProcessInfo.processInfo.operatingSystemVersionString), id: \(Device.current.id)") if UIApplication.shared.applicationState == .background { MixinService.isStopProcessMessages = false @@ -197,6 +197,7 @@ extension AppDelegate { NotificationCenter.default.addObserver(self, selector: #selector(handleClockSkew), name: MixinService.clockSkewDetectedNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(webSocketDidConnect), name: WebSocketService.didConnectNotification, object: nil) NotificationCenter.default.addObserver(JobService.shared, selector: #selector(JobService.restoreJobs), name: WebSocketService.didSendListPendingMessageNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(databaseCorrupted), name: AppGroupUserDefaults.User.databaseCorruptedNotification, object: nil) } @objc func webSocketDidConnect() { @@ -257,6 +258,10 @@ extension AppDelegate { } } + @objc func databaseCorrupted() { + mainWindow.rootViewController = makeInitialViewController() + } + } extension AppDelegate { diff --git a/Mixin/Assets.xcassets/ic_repair_database.imageset/Contents.json b/Mixin/Assets.xcassets/ic_repair_database.imageset/Contents.json new file mode 100644 index 0000000000..4a0113883a --- /dev/null +++ b/Mixin/Assets.xcassets/ic_repair_database.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_repair_database@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_repair_database@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@2x.png b/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@2x.png new file mode 100644 index 0000000000..1cbfba295d Binary files /dev/null and b/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@2x.png differ diff --git a/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@3x.png b/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@3x.png new file mode 100644 index 0000000000..9e3aeea250 Binary files /dev/null and b/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@3x.png differ diff --git a/Mixin/Resources/en.lproj/Localizable.strings b/Mixin/Resources/en.lproj/Localizable.strings index 0d50370ab8..e2ace46793 100644 --- a/Mixin/Resources/en.lproj/Localizable.strings +++ b/Mixin/Resources/en.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "Remove emergency contact for sure?"; "remove_from_group" = "Remove from Group"; "remove_stickers" = "Remove Stickers"; +"repair" = "Repair"; +"repair_chat_history" = "Repair Chat History"; +"repair_chat_history_hint" = "The database file is found to be damaged, some messages may be lost, please try to repair it."; +"repair_chat_history_success" = "The chat history has been repaired successfully! Chat records before %@ have been restored."; +"repairing" = "Repairing"; "reply" = "Reply"; "report" = "Report"; "report_and_block" = "Report and block?"; diff --git a/Mixin/Resources/es.lproj/Localizable.strings b/Mixin/Resources/es.lproj/Localizable.strings index fdc860bd91..501ed14aff 100644 --- a/Mixin/Resources/es.lproj/Localizable.strings +++ b/Mixin/Resources/es.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "¿Eliminar contacto de emergencia seguro?"; "remove_from_group" = "Eliminar del grupo"; "remove_stickers" = "Eliminar pegatinas"; +"repair" = "Reparando"; +"repair_chat_history" = "Restaurar el historial de chat"; +"repair_chat_history_hint" = "El archivo de la base de datos ha sido encontrado corrupto, es posible que se pierdan algunos datos. Por favor, intente repararlo."; +"repair_chat_history_success" = "¡La reparación del historial de chat se ha completado con éxito! Los registros de chat anteriores a %@ han sido restaurados."; +"repairing" = "Reparación en proceso"; "reply" = "Responder"; "report" = "Informe"; "report_and_block" = "¿Denunciar y bloquear?"; diff --git a/Mixin/Resources/ja.lproj/Localizable.strings b/Mixin/Resources/ja.lproj/Localizable.strings index ef37cd4ccb..ee0de9dba9 100644 --- a/Mixin/Resources/ja.lproj/Localizable.strings +++ b/Mixin/Resources/ja.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "本当に緊急連絡先を削除しますか?"; "remove_from_group" = "グループから退会させる"; "remove_stickers" = "スタンプの削除"; +"repair" = "Repair"; +"repair_chat_history" = "Repair Chat History"; +"repair_chat_history_hint" = "The database file is found to be damaged, some messages may be lost, please try to repair it."; +"repair_chat_history_success" = "The chat history has been repaired successfully! Chat records before %@ have been restored."; +"repairing" = "Repairing"; "reply" = "返信"; "report" = "報告"; "report_and_block" = "報告してブロックしますか?"; diff --git a/Mixin/Resources/ru.lproj/Localizable.strings b/Mixin/Resources/ru.lproj/Localizable.strings index d8de88cbeb..7eb08f3e96 100644 --- a/Mixin/Resources/ru.lproj/Localizable.strings +++ b/Mixin/Resources/ru.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "Точно удалить экстренный контакт?"; "remove_from_group" = "Удалить из группы"; "remove_stickers" = "Удалить наклейки"; +"repair" = "Repair"; +"repair_chat_history" = "Repair Chat History"; +"repair_chat_history_hint" = "The database file is found to be damaged, some messages may be lost, please try to repair it."; +"repair_chat_history_success" = "The chat history has been repaired successfully! Chat records before %@ have been restored."; +"repairing" = "Repairing"; "reply" = "Ответить"; "report" = "Отчет"; "report_and_block" = "Пожаловаться и заблокировать?"; diff --git a/Mixin/Resources/zh-Hans.lproj/Localizable.strings b/Mixin/Resources/zh-Hans.lproj/Localizable.strings index dc254f9fbd..929705b756 100644 --- a/Mixin/Resources/zh-Hans.lproj/Localizable.strings +++ b/Mixin/Resources/zh-Hans.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "确定删除紧急联系人?"; "remove_from_group" = "从群组中移除"; "remove_stickers" = "移除所有表情"; +"repair" = "修复"; +"repair_chat_history" = "修复聊天记录"; +"repair_chat_history_hint" = "数据库文件被发现损坏,可能会丢失一些信息,请尝试修复它。"; +"repair_chat_history_success" = "聊天记录修复成功!%@ 之前的聊天记录已经恢复。"; +"repairing" = "修复中"; "reply" = "回复"; "report" = "举报"; "report_and_block" = "举报并屏蔽?"; diff --git a/Mixin/Resources/zh-Hant.lproj/Localizable.strings b/Mixin/Resources/zh-Hant.lproj/Localizable.strings index f38f39490b..b635001b6d 100644 --- a/Mixin/Resources/zh-Hant.lproj/Localizable.strings +++ b/Mixin/Resources/zh-Hant.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "確定刪除緊急聯絡人?"; "remove_from_group" = "從群組中移除"; "remove_stickers" = "移除所有表情"; +"repair" = "修復"; +"repair_chat_history" = "修復聊天記錄"; +"repair_chat_history_hint" = "資料庫檔案被發現損壞,可能會丟失一些資訊,請嘗試修復它。"; +"repair_chat_history_success" = "聊天記錄修復成功!%@ 之前的聊天記錄已經恢復。"; +"repairing" = "修复中"; "reply" = "回覆"; "report" = "舉報"; "report_and_block" = "舉報並封鎖?"; diff --git a/Mixin/Service/Job/DatabaseBackupJob.swift b/Mixin/Service/Job/DatabaseBackupJob.swift new file mode 100644 index 0000000000..5e0e985ce3 --- /dev/null +++ b/Mixin/Service/Job/DatabaseBackupJob.swift @@ -0,0 +1,32 @@ +import Foundation +import MixinServices + +class DatabaseBackupJob: AsynchronousJob { + + override func getJobId() -> String { + "database-backup" + } + + override func execute() -> Bool { + do { + try UserDatabase.current.writeWithoutTransaction { _ in + try DatabaseFile.removeIfExists(.temp) + try DatabaseFile.copy(at: .original, to: .temp) + } + + try DatabaseFile.checkIntegrity(.temp) + + try DatabaseFile.removeIfExists(.backup) + try DatabaseFile.copy(at: .temp, to: .backup) + try DatabaseFile.removeIfExists(.temp) + + AppGroupUserDefaults.User.lastDatabaseBackupDate = Date() + finishJob() + } catch { + Logger.general.error(category: "BackupDatabaseJob", message: "Backup database failed: \(error)") + reporter.report(error: error) + } + return true + } + +} diff --git a/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift b/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift index 1abccc7397..87d363ffa5 100644 --- a/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift +++ b/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift @@ -2,7 +2,9 @@ import UIKit import MixinServices func makeInitialViewController(isUsernameJustInitialized: Bool = false) -> UIViewController { - if AppGroupUserDefaults.Account.isClockSkewed { + if AppGroupUserDefaults.User.isDatabaseCorrupted { + return DatabaseRepairViewController.instance() + } else if AppGroupUserDefaults.Account.isClockSkewed { if let viewController = AppDelegate.current.mainWindow.rootViewController as? ClockSkewViewController { viewController.checkFailed() return viewController diff --git a/Mixin/UserInterface/Controllers/Home/DatabaseRepairViewController.swift b/Mixin/UserInterface/Controllers/Home/DatabaseRepairViewController.swift new file mode 100644 index 0000000000..d713025e58 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Home/DatabaseRepairViewController.swift @@ -0,0 +1,36 @@ +import UIKit +import MixinServices + +class DatabaseRepairViewController: UIViewController { + + @IBOutlet weak var repairButton: RoundedButton! + @IBOutlet weak var stackView: UIStackView! + @IBOutlet weak var activityIndicator: ActivityIndicatorView! + + class func instance() -> DatabaseRepairViewController { + R.storyboard.home.repair_database()! + } + + @IBAction func repairAction(_ sender: Any) { + repairButton.isHidden = true + stackView.isHidden = false + activityIndicator.startAnimating() + if DatabaseFile.exists(.backup) { + do { + try DatabaseFile.removeIfExists(.original) + try DatabaseFile.copy(at: .backup, to: .original) + let lastBackupDate = AppGroupUserDefaults.User.lastDatabaseBackupDate ?? Date() + let formattedDate = DateFormatter.dateFull.string(from: lastBackupDate) + stackView.isHidden = true + alert(nil, message: R.string.localizable.repair_chat_history_success(formattedDate)) { _ in + AppGroupUserDefaults.User.isDatabaseCorrupted = false + } + } catch { + LoginManager.shared.logout(reason: "Failed to repair database") + } + } else { + LoginManager.shared.logout(reason: "Failed to repair database") + } + } + +} diff --git a/Mixin/UserInterface/Controllers/Home/HomeViewController.swift b/Mixin/UserInterface/Controllers/Home/HomeViewController.swift index 278894e032..86559d1838 100644 --- a/Mixin/UserInterface/Controllers/Home/HomeViewController.swift +++ b/Mixin/UserInterface/Controllers/Home/HomeViewController.swift @@ -151,6 +151,7 @@ class HomeViewController: UIViewController { if SpotlightManager.isAvailable { SpotlightManager.shared.indexIfNeeded() } + DatabaseBackupManager.shared.backupIfNeeded() } UIApplication.homeContainerViewController?.clipSwitcher.loadClipsFromPreviousSession() } diff --git a/Mixin/UserInterface/Controllers/Home/Model/DatabaseBackupManager.swift b/Mixin/UserInterface/Controllers/Home/Model/DatabaseBackupManager.swift new file mode 100644 index 0000000000..2709c2a5c4 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Home/Model/DatabaseBackupManager.swift @@ -0,0 +1,28 @@ +import Foundation +import MixinServices + +class DatabaseBackupManager: NSObject { + + static let shared = DatabaseBackupManager() + + override init() { + super.init() + NotificationCenter.default.addObserver(self, selector: #selector(backupIfNeeded), name: UIApplication.didBecomeActiveNotification, object: nil) + } + + @objc func backupIfNeeded() { + guard LoginManager.shared.isLoggedIn else { + return + } + let needsBackup: Bool + if let date = AppGroupUserDefaults.User.lastDatabaseBackupDate { + needsBackup = -date.timeIntervalSinceNow > TimeInterval.hour * 2 + } else { + needsBackup = true + } + if needsBackup { + ConcurrentJobQueue.shared.addJob(job: DatabaseBackupJob()) + } + } + +} diff --git a/Mixin/UserInterface/Controllers/Home/Model/DatabaseFile.swift b/Mixin/UserInterface/Controllers/Home/Model/DatabaseFile.swift new file mode 100644 index 0000000000..cd134cb283 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Home/Model/DatabaseFile.swift @@ -0,0 +1,62 @@ +import Foundation +import MixinServices +import GRDB + +enum DatabaseFile { + + case original + case backup + case temp + + private var name: String { + switch self { + case .original: + return "mixin.db" + case .backup: + return "mixin-backup.db" + case .temp: + return "mixin-backup-temp.db" + } + } + + private var db: URL { + AppGroupContainer.accountUrl.appendingPathComponent("\(name)", isDirectory: false) + } + + private var shm: URL { + AppGroupContainer.accountUrl.appendingPathComponent("\(name)-shm", isDirectory: false) + } + + private var wal: URL { + AppGroupContainer.accountUrl.appendingPathComponent("\(name)-wal", isDirectory: false) + } + + static func removeIfExists(_ file: DatabaseFile) throws { + if FileManager.default.fileExists(atPath: file.db.path) { + try FileManager.default.removeItem(at: file.db) + } + if FileManager.default.fileExists(atPath: file.wal.path) { + try FileManager.default.removeItem(at: file.wal) + } + if FileManager.default.fileExists(atPath: file.shm.path) { + try FileManager.default.removeItem(at: file.shm) + } + } + + static func copy(at srcFile: DatabaseFile, to dstFile: DatabaseFile) throws { + try FileManager.default.copyItem(at: srcFile.db, to: dstFile.db) + } + + static func exists(_ file: DatabaseFile) -> Bool { + FileManager.default.fileExists(atPath: file.db.path) + } + + static func checkIntegrity(_ file: DatabaseFile) throws { + let dbQueue = try DatabaseQueue(path: file.db.path) + try dbQueue.write { db in + try db.execute(sql: "PRAGMA integrity_check") + } + } + +} + diff --git a/Mixin/UserInterface/Storyboard/Home.storyboard b/Mixin/UserInterface/Storyboard/Home.storyboard index fe8c8960aa..40dc2c1096 100644 --- a/Mixin/UserInterface/Storyboard/Home.storyboard +++ b/Mixin/UserInterface/Storyboard/Home.storyboard @@ -1,9 +1,9 @@ - + - + @@ -38,7 +38,7 @@ - + @@ -56,22 +56,22 @@ - + - + - + - + @@ -785,7 +785,7 @@ - + @@ -1556,7 +1556,7 @@