From a1e6d90e31dde0c5fbe77bdcd81242861b51284c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 24 May 2023 14:22:12 +0400 Subject: [PATCH] mobile: archive import errors (#2496) --- .../java/chat/simplex/app/model/SimpleXAPI.kt | 19 +++++++++++++++++-- .../app/views/database/DatabaseView.kt | 15 ++++++++++----- .../app/src/main/res/values/strings.xml | 1 + apps/ios/Shared/Model/SimpleXAPI.swift | 7 ++++--- .../Shared/Views/Database/DatabaseView.swift | 16 +++++++++++++--- .../Database/MigrateToAppGroupView.swift | 2 +- apps/ios/SimpleXChat/APITypes.swift | 9 +++++++++ 7 files changed, 55 insertions(+), 14 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 57ff629426..721fc48df6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -545,9 +545,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a throw Error("failed to export archive: ${r.responseType} ${r.details}") } - suspend fun apiImportArchive(config: ArchiveConfig) { + suspend fun apiImportArchive(config: ArchiveConfig): List { val r = sendCmd(CC.ApiImportArchive(config)) - if (r is CR.CmdOk) return + if (r is CR.ArchiveImported) return r.archiveErrors throw Error("failed to import archive: ${r.responseType} ${r.details}") } @@ -3369,6 +3369,7 @@ sealed class CR { @Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR() @Serializable @SerialName("chatError") class ChatRespError(val user_: User?, val chatError: ChatError): CR() + @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() @Serializable class Response(val type: String, val json: String): CR() @Serializable class Invalid(val str: String): CR() @@ -3478,6 +3479,7 @@ sealed class CR { is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" + is ArchiveImported -> "archiveImported" is Response -> "* $type" is Invalid -> "* invalid json" } @@ -3590,6 +3592,7 @@ sealed class CR { is CmdOk -> withUser(user, noDetails()) is ChatCmdError -> withUser(user_, chatError.string) is ChatRespError -> withUser(user_, chatError.string) + is ArchiveImported -> "${archiveErrors.map { it.string } }" is Response -> json is Invalid -> str } @@ -3678,6 +3681,7 @@ sealed class ChatErrorType { is InvalidConnReq -> "invalidConnReq" is FileAlreadyReceiving -> "fileAlreadyReceiving" is СommandError -> "commandError $message" + is CEException -> "exception $message" } @Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType() @Serializable @SerialName("differentActiveUser") class DifferentActiveUser: ChatErrorType() @@ -3685,6 +3689,7 @@ sealed class ChatErrorType { @Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType() @Serializable @SerialName("fileAlreadyReceiving") class FileAlreadyReceiving: ChatErrorType() @Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType() + @Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType() } @Serializable @@ -3896,3 +3901,13 @@ sealed class XFTPErrorType { @Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType() @Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType() } + +@Serializable +sealed class ArchiveError { + val string: String get() = when (this) { + is ArchiveErrorImport -> "import ${chatError.string}" + is ArchiveErrorImportFile -> "importFile $file ${chatError.string}" + } + @Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError() + @Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError() +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index a9cfc1b906..99293f76ef 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -178,8 +178,7 @@ fun DatabaseLayout( val unencrypted = chatDbEncrypted == false SettingsActionItem( if (unencrypted) painterResource(R.drawable.ic_lock_open) else if (useKeyChain) painterResource(R.drawable.ic_vpn_key_filled) - else painterResource(R - .drawable.ic_lock), + else painterResource(R.drawable.ic_lock), stringResource(R.string.database_passphrase), click = showSettingsModal() { DatabaseEncryptionView(it) }, iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary, @@ -574,11 +573,17 @@ private fun importArchive( m.controller.apiDeleteStorage() try { val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString()) - m.controller.apiImportArchive(config) + val archiveErrors = m.controller.apiImportArchive(config) DatabaseUtils.ksDatabasePassword.remove() appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context)) - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) + if (archiveErrors.isEmpty()) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), text = generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) + } + } else { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), text = generalGetString(R.string.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(R.string.non_fatal_errors_occured_during_import)) + } } } catch (e: Error) { operationEnded(m, progressIndicator) { diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 6c6be6e764..2017bae844 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -892,6 +892,7 @@ Error importing chat database Chat database imported Restart the app to use imported chat database. + Some non-fatal errors occurred during import - you may see Chat console for more details. Delete chat profile? This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Chat database deleted diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 90dfda8d3b..a84f847f01 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -243,8 +243,10 @@ func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } -func apiImportArchive(config: ArchiveConfig) async throws { - try await sendCommandOkResp(.apiImportArchive(config: config)) +func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { + let r = await chatSendCmd(.apiImportArchive(config: config)) + if case let .archiveImported(archiveErrors) = r { return archiveErrors } + throw r } func apiDeleteStorage() async throws { @@ -538,7 +540,6 @@ func apiConnect_(connReq: String) async -> (ConnReqType?, Alert?) { return (nil, nil) } let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq)) - let am = AlertManager.shared switch r { case .sentConfirmation: return (.invitation, nil) case .sentInvitation: return (.contact, nil) diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 15efd5955d..65ec9ef944 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -14,6 +14,7 @@ enum DatabaseAlert: Identifiable { case exportProhibited case importArchive case archiveImported + case archiveImportedWithErrors(archiveErrors: [ArchiveError]) case deleteChat case chatDeleted case deleteLegacyDatabase @@ -27,6 +28,7 @@ enum DatabaseAlert: Identifiable { case .exportProhibited: return "exportProhibited" case .importArchive: return "importArchive" case .archiveImported: return "archiveImported" + case .archiveImportedWithErrors: return "archiveImportedWithErrors" case .deleteChat: return "deleteChat" case .chatDeleted: return "chatDeleted" case .deleteLegacyDatabase: return "deleteLegacyDatabase" @@ -251,7 +253,11 @@ struct DatabaseView: View { title: Text("Chat database imported"), message: Text("Restart the app to use imported chat database") ) - + case .archiveImportedWithErrors: + return Alert( + title: Text("Chat database imported"), + message: Text("Restart the app to use imported chat database") + Text("\n") + Text("Some non-fatal errors occurred during import - you may see Chat console for more details.") + ) case .deleteChat: return Alert( title: Text("Delete chat profile?"), @@ -351,9 +357,13 @@ struct DatabaseView: View { try await apiDeleteStorage() do { let config = ArchiveConfig(archivePath: archivePath.path) - try await apiImportArchive(config: config) + let archiveErrors = try await apiImportArchive(config: config) _ = kcDatabasePassword.remove() - await operationEnded(.archiveImported) + if archiveErrors.isEmpty { + await operationEnded(.archiveImported) + } else { + await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors)) + } } catch let error { await operationEnded(.error(title: "Error importing chat database", error: responseError(error))) } diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index 79b5ea52c8..17b8faacb0 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -203,7 +203,7 @@ struct MigrateToAppGroupView: View { dbContainerGroupDefault.set(.group) resetChatCtrl() try await MainActor.run { try initializeChat(start: false) } - try await apiImportArchive(config: config) + let _ = try await apiImportArchive(config: config) await MainActor.run { setV3DBMigration(.migrated) } } catch let error { dbContainerGroupDefault.set(.documents) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 8e4f87a408..f2759e8cec 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -495,6 +495,7 @@ public enum ChatResponse: Decodable, Error { case cmdOk(user: User?) case chatCmdError(user_: User?, chatError: ChatError) case chatError(user_: User?, chatError: ChatError) + case archiveImported(archiveErrors: [ArchiveError]) public var responseType: String { get { @@ -609,6 +610,7 @@ public enum ChatResponse: Decodable, Error { case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" + case .archiveImported: return "archiveImported" } } } @@ -726,6 +728,7 @@ public enum ChatResponse: Decodable, Error { case .cmdOk: return noDetails case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) + case let .archiveImported(archiveErrors): return String(describing: archiveErrors) } } } @@ -1248,6 +1251,7 @@ public enum ChatErrorType: Decodable { case invalidChatItemDelete case agentVersion case commandError(message: String) + case exception(message: String) } public enum StoreError: Decodable { @@ -1386,3 +1390,8 @@ public enum SMPAgentError: Decodable { case A_VERSION case A_ENCRYPTION } + +public enum ArchiveError: Decodable { + case `import`(chatError: ChatError) + case importFile(file: String, chatError: ChatError) +}