mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
ios: restore db (#1063)
* wip * wip * wip * refactor * clean up * simplify * simplify * refactor * rename * rename consts * refactor Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
3351503744
commit
2eca3e789c
5 changed files with 107 additions and 12 deletions
|
@ -663,6 +663,10 @@ func initializeChat(start: Bool, dbKey: String? = nil) throws {
|
|||
let m = ChatModel.shared
|
||||
(m.chatDbEncrypted, m.chatDbStatus) = migrateChatDatabase(dbKey)
|
||||
if m.chatDbStatus != .ok { return }
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if encryptionStartedDefault.get() {
|
||||
encryptionStartedDefault.set(false)
|
||||
}
|
||||
let _ = getChatCtrl(dbKey)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetIncognito(incognito: incognitoGroupDefault.get())
|
||||
|
|
|
@ -133,7 +133,10 @@ struct DatabaseEncryptionView: View {
|
|||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
encryptionStartedDefault.set(true)
|
||||
encryptionStartedAtDefault.set(Date.now)
|
||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||
encryptionStartedDefault.set(false)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if useKeychain {
|
||||
if setDatabaseKey(newKey) {
|
||||
|
|
|
@ -15,6 +15,7 @@ struct DatabaseErrorView: View {
|
|||
@State private var dbKey = ""
|
||||
@State private var storedDBKey = getDatabaseKey()
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var showRestoreDbButton = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
|
@ -25,7 +26,6 @@ struct DatabaseErrorView: View {
|
|||
Text("Database passphrase is different from saved in the keychain.")
|
||||
databaseKeyField(onSubmit: saveAndRunChat)
|
||||
saveAndOpenButton()
|
||||
Spacer()
|
||||
Text("File: \(dbFile)")
|
||||
} else {
|
||||
Text("Encrypted database").font(.title)
|
||||
|
@ -37,30 +37,32 @@ struct DatabaseErrorView: View {
|
|||
databaseKeyField(onSubmit: runChat)
|
||||
openChatButton()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
case let .error(dbFile, migrationError):
|
||||
Text("Database error")
|
||||
.font(.title)
|
||||
Text("File: \(dbFile)")
|
||||
Text("Error: \(migrationError)")
|
||||
Spacer()
|
||||
case .errorKeychain:
|
||||
Text("Keychain error")
|
||||
.font(.title)
|
||||
Text("Cannot access keychain to save database password")
|
||||
Spacer()
|
||||
case let .unknown(json):
|
||||
Text("Database error")
|
||||
.font(.title)
|
||||
Text("Unknown database error: \(json)")
|
||||
Spacer()
|
||||
case .ok:
|
||||
EmptyView()
|
||||
}
|
||||
if showRestoreDbButton {
|
||||
Spacer().frame(height: 10)
|
||||
Text("The attempt to change database passphrase was not completed.")
|
||||
restoreDbButton()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity)
|
||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||
.onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() }
|
||||
}
|
||||
|
||||
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
|
||||
|
@ -118,6 +120,42 @@ struct DatabaseErrorView: View {
|
|||
logger.error("initializeChat \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldShowRestoreDbButton() -> Bool {
|
||||
if !encryptionStartedDefault.get() { return false }
|
||||
let startedAt = encryptionStartedAtDefault.get()
|
||||
// In case there is a small difference between saved encryptionStartedAt time and last modified timestamp on a file
|
||||
let safeDiffInTime = TimeInterval(10)
|
||||
return hasBackup(newerThan: startedAt - safeDiffInTime)
|
||||
}
|
||||
|
||||
private func restoreDbButton() -> some View {
|
||||
Button() {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Restore database backup?"),
|
||||
message: Text("Please enter the previous password after restoring database backup. This action can not be undone."),
|
||||
primaryButton: .destructive(Text("Restore")) {
|
||||
restoreDb()
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
} label: {
|
||||
Text("Restore database backup").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreDb() {
|
||||
do {
|
||||
try restoreBackup()
|
||||
showRestoreDbButton = false
|
||||
encryptionStartedDefault.set(false)
|
||||
} catch {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Restore database error"),
|
||||
message: Text(error.localizedDescription)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DatabaseErrorView_Previews: PreviewProvider {
|
||||
|
|
|
@ -26,6 +26,8 @@ let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
|||
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
||||
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
|
||||
let DEFAULT_DEVELOPER_TOOLS = "developerTools"
|
||||
let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted"
|
||||
let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
|
||||
let DEFAULT_ACCENT_COLOR_RED = "accentColorRed"
|
||||
let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen"
|
||||
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue"
|
||||
|
@ -41,6 +43,7 @@ let appDefaults: [String: Any] = [
|
|||
DEFAULT_EXPERIMENTAL_CALLS: false,
|
||||
DEFAULT_CHAT_V3_DB_MIGRATION: "offer",
|
||||
DEFAULT_DEVELOPER_TOOLS: false,
|
||||
DEFAULT_ENCRYPTION_STARTED: false,
|
||||
DEFAULT_ACCENT_COLOR_RED: 0.000,
|
||||
DEFAULT_ACCENT_COLOR_GREEN: 0.533,
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
|
@ -51,6 +54,10 @@ private var indent: CGFloat = 36
|
|||
|
||||
let chatArchiveTimeDefault = DateDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CHAT_ARCHIVE_TIME)
|
||||
|
||||
let encryptionStartedDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_ENCRYPTION_STARTED)
|
||||
|
||||
let encryptionStartedAtDefault = DateDefault(defaults: UserDefaults.standard, forKey: DEFAULT_ENCRYPTION_STARTED_AT)
|
||||
|
||||
func setGroupDefaults() {
|
||||
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
|
||||
}
|
||||
|
|
|
@ -17,6 +17,14 @@ public let maxImageSize: Int64 = 236700
|
|||
|
||||
public let maxFileSize: Int64 = 8000000
|
||||
|
||||
private let CHAT_DB: String = "_chat.db"
|
||||
|
||||
private let AGENT_DB: String = "_agent.db"
|
||||
|
||||
private let CHAT_DB_BAK: String = "_chat.db.bak"
|
||||
|
||||
private let AGENT_DB_BAK: String = "_agent.db.bak"
|
||||
|
||||
public func getDocumentsDirectory() -> URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
}
|
||||
|
@ -44,6 +52,41 @@ public func getAppDatabasePath() -> URL {
|
|||
: getLegacyDatabasePath()
|
||||
}
|
||||
|
||||
func fileModificationDate(_ path: String) -> Date? {
|
||||
do {
|
||||
let attr = try FileManager.default.attributesOfItem(atPath: path)
|
||||
return attr[FileAttributeKey.modificationDate] as? Date
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func hasBackup(newerThan date: Date) -> Bool {
|
||||
let dbPath = getAppDatabasePath().path
|
||||
return hasBackupFile(dbPath + AGENT_DB_BAK, newerThan: date)
|
||||
&& hasBackupFile(dbPath + CHAT_DB_BAK, newerThan: date)
|
||||
}
|
||||
|
||||
private func hasBackupFile(_ path: String, newerThan date: Date) -> Bool {
|
||||
let fm = FileManager.default
|
||||
return fm.fileExists(atPath: path)
|
||||
&& date <= (fileModificationDate(path) ?? Date.distantPast)
|
||||
}
|
||||
|
||||
public func restoreBackup() throws {
|
||||
let dbPath = getAppDatabasePath().path
|
||||
try restoreBackupFile(fromPath: dbPath + AGENT_DB_BAK, toPath: dbPath + AGENT_DB)
|
||||
try restoreBackupFile(fromPath: dbPath + CHAT_DB_BAK, toPath: dbPath + CHAT_DB)
|
||||
}
|
||||
|
||||
private func restoreBackupFile(fromPath: String, toPath: String) throws {
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: toPath) {
|
||||
try fm.removeItem(atPath: toPath)
|
||||
}
|
||||
try fm.copyItem(atPath: fromPath, toPath: toPath)
|
||||
}
|
||||
|
||||
public func hasLegacyDatabase() -> Bool {
|
||||
hasDatabaseAtPath(getLegacyDatabasePath())
|
||||
}
|
||||
|
@ -54,18 +97,18 @@ public func hasDatabase() -> Bool {
|
|||
|
||||
func hasDatabaseAtPath(_ dbPath: URL) -> Bool {
|
||||
let fm = FileManager.default
|
||||
return fm.isReadableFile(atPath: dbPath.path + "_agent.db") &&
|
||||
fm.isReadableFile(atPath: dbPath.path + "_chat.db")
|
||||
return fm.isReadableFile(atPath: dbPath.path + AGENT_DB) &&
|
||||
fm.isReadableFile(atPath: dbPath.path + CHAT_DB)
|
||||
}
|
||||
|
||||
public func removeLegacyDatabaseAndFiles() -> Bool {
|
||||
let dbPath = getLegacyDatabasePath()
|
||||
let appFiles = getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
|
||||
let fm = FileManager.default
|
||||
let r1 = nil != (try? fm.removeItem(atPath: dbPath.path + "_agent.db"))
|
||||
let r2 = nil != (try? fm.removeItem(atPath: dbPath.path + "_chat.db"))
|
||||
try? fm.removeItem(atPath: dbPath.path + "_agent.db.bak")
|
||||
try? fm.removeItem(atPath: dbPath.path + "_chat.db.bak")
|
||||
let r1 = nil != (try? fm.removeItem(atPath: dbPath.path + AGENT_DB))
|
||||
let r2 = nil != (try? fm.removeItem(atPath: dbPath.path + CHAT_DB))
|
||||
try? fm.removeItem(atPath: dbPath.path + AGENT_DB_BAK)
|
||||
try? fm.removeItem(atPath: dbPath.path + CHAT_DB_BAK)
|
||||
try? fm.removeItem(at: appFiles)
|
||||
return r1 && r2
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue