ios: UI to export/import/delete chat database (#743)

* ios: UI to export/import/delete chat database

* move files

* ui for database migration

* migration screen layout

* ios: export archive and delete chat database

* import archive

* refactor, update texts

* database migration (almost works)

* fix missing import

* delete legacy database

* update migration errors
This commit is contained in:
Evgeny Poberezkin
2022-06-24 13:52:20 +01:00
committed by GitHub
parent 4d9e446489
commit 6a2f2a512f
21 changed files with 1097 additions and 206 deletions
+48 -9
View File
@@ -12,21 +12,61 @@ private var chatController: chat_ctrl?
public func getChatCtrl() -> chat_ctrl {
if let controller = chatController { return controller }
let dataDir = getDocumentsDirectory().path + "/mobile_v1"
logger.debug("documents directory \(dataDir)")
var cstr = dataDir.cString(using: .utf8)!
let dbPath = getAppDatabasePath().path
logger.debug("getChatCtrl DB path: \(dbPath)")
var cstr = dbPath.cString(using: .utf8)!
chatController = chat_init(&cstr)
logger.debug("getChatCtrl: chat_init")
return chatController!
}
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
var c = cmd.cmdString.cString(using: .utf8)!
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
public func resetChatCtrl() {
chatController = nil
}
public func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
let s = String.init(cString: cjson)
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
var c = cmd.cmdString.cString(using: .utf8)!
let cjson = chat_send_cmd(getChatCtrl(), &c)!
return chatResponse(fromCString(cjson))
}
// in microseconds
let MESSAGE_TIMEOUT: Int32 = 15_000_000
public func recvSimpleXMsg() -> ChatResponse? {
if let cjson = chat_recv_msg_wait(getChatCtrl(), MESSAGE_TIMEOUT) {
let s = fromCString(cjson)
return s == "" ? nil : chatResponse(s)
}
return nil
}
public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
var c = s.cString(using: .utf8)!
if let cjson = chat_parse_markdown(&c) {
if let d = fromCString(cjson).data(using: .utf8) {
do {
let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d)
return r.formattedText
} catch {
logger.error("parseSimpleXMarkdown jsonDecoder.decode error: \(error.localizedDescription)")
}
}
}
return nil
}
struct ParsedMarkdown: Decodable {
var formattedText: [FormattedText]?
}
private func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
let s = String.init(cString: c)
free(c)
return s
}
public func chatResponse(_ s: String) -> ChatResponse {
let d = s.data(using: .utf8)!
// TODO is there a way to do it without copying the data? e.g:
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
@@ -46,7 +86,6 @@ public func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
}
json = prettyJSON(j)
}
free(cjson)
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
}
+19 -6
View File
@@ -18,6 +18,9 @@ public enum ChatCommand {
case apiStopChat
case apiSetAppPhase(appPhase: AgentPhase)
case setFilesFolder(filesFolder: String)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
case apiDeleteStorage
case apiGetChats
case apiGetChat(type: ChatType, id: Int64)
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent)
@@ -35,7 +38,6 @@ public enum ChatCommand {
case apiDeleteChat(type: ChatType, id: Int64)
case apiClearChat(type: ChatType, id: Int64)
case apiUpdateProfile(profile: Profile)
case apiParseMarkdown(text: String)
case createMyAddress
case deleteMyAddress
case showMyAddress
@@ -62,6 +64,9 @@ public enum ChatCommand {
case .apiStopChat: return "/_stop"
case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase)"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
case .apiDeleteStorage: return "/_db delete"
case .apiGetChats: return "/_get chats pcc=on"
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
case let .apiSendMessage(type, id, file, quotedItemId, mc):
@@ -81,7 +86,6 @@ public enum ChatCommand {
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))"
case let .apiParseMarkdown(text): return "/_parse \(text)"
case .createMyAddress: return "/address"
case .deleteMyAddress: return "/delete_address"
case .showMyAddress: return "/show_address"
@@ -110,6 +114,9 @@ public enum ChatCommand {
case .apiStopChat: return "apiStopChat"
case .apiSetAppPhase: return "apiSetAppPhase"
case .setFilesFolder: return "setFilesFolder"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
case .apiDeleteStorage: return "apiDeleteStorage"
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
case .apiSendMessage: return "apiSendMessage"
@@ -127,7 +134,6 @@ public enum ChatCommand {
case .apiDeleteChat: return "apiDeleteChat"
case .apiClearChat: return "apiClearChat"
case .apiUpdateProfile: return "apiUpdateProfile"
case .apiParseMarkdown: return "apiParseMarkdown"
case .createMyAddress: return "createMyAddress"
case .deleteMyAddress: return "deleteMyAddress"
case .showMyAddress: return "showMyAddress"
@@ -178,7 +184,6 @@ public enum ChatResponse: Decodable, Error {
case chatCleared(chatInfo: ChatInfo)
case userProfileNoChange
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
case apiParsedMarkdown(formattedText: [FormattedText]?)
case userContactLink(connReqContact: String)
case userContactLinkCreated(connReqContact: String)
case userContactLinkDeleted
@@ -243,7 +248,6 @@ public enum ChatResponse: Decodable, Error {
case .chatCleared: return "chatCleared"
case .userProfileNoChange: return "userProfileNoChange"
case .userProfileUpdated: return "userProfileUpdated"
case .apiParsedMarkdown: return "apiParsedMarkdown"
case .userContactLink: return "userContactLink"
case .userContactLinkCreated: return "userContactLinkCreated"
case .userContactLinkDeleted: return "userContactLinkDeleted"
@@ -309,7 +313,6 @@ public enum ChatResponse: Decodable, Error {
case let .chatCleared(chatInfo): return String(describing: chatInfo)
case .userProfileNoChange: return noDetails
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
case let .apiParsedMarkdown(formattedText): return String(describing: formattedText)
case let .userContactLink(connReq): return connReq
case let .userContactLinkCreated(connReq): return connReq
case .userContactLinkDeleted: return noDetails
@@ -370,6 +373,16 @@ public enum AgentPhase: String, Codable {
case suspended = "SUSPENDED"
}
public struct ArchiveConfig: Encodable {
var archivePath: String
var disableCompression: Bool?
public init(archivePath: String, disableCompression: Bool? = nil) {
self.archivePath = archivePath
self.disableCompression = disableCompression
}
}
public func decodeJSON<T: Decodable>(_ json: String) -> T? {
if let data = json.data(using: .utf8) {
return try? jsonDecoder.decode(T.self, from: data)
+60 -12
View File
@@ -10,12 +10,12 @@ import Foundation
import SwiftUI
let GROUP_DEFAULT_APP_STATE = "appState"
let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
let APP_GROUP_NAME = "group.chat.simplex.app"
func getGroupDefaults() -> UserDefaults? {
UserDefaults(suiteName: APP_GROUP_NAME)
}
public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)!
public enum AppState: String {
case active
@@ -42,18 +42,66 @@ public enum AppState: String {
}
}
public func setAppState(_ state: AppState) {
if let defaults = getGroupDefaults() {
defaults.set(state.rawValue, forKey: GROUP_DEFAULT_APP_STATE)
public enum DBContainer: String {
case documents
case group
}
public let appStateGroupDefault = EnumDefault<AppState>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_APP_STATE,
withDefault: .active
)
public let dbContainerGroupDefault = EnumDefault<DBContainer>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_DB_CONTAINER,
withDefault: .documents
)
public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START)
public class DateDefault {
var defaults: UserDefaults
var key: String
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String) {
self.defaults = defaults
self.key = forKey
}
public func get() -> Date {
let ts = defaults.double(forKey: key)
return Date(timeIntervalSince1970: ts)
}
public func set(_ ts: Date) {
defaults.set(ts.timeIntervalSince1970, forKey: key)
defaults.synchronize()
}
}
public func getAppState() -> AppState {
if let defaults = getGroupDefaults(),
let rawValue = defaults.string(forKey: GROUP_DEFAULT_APP_STATE),
let state = AppState(rawValue: rawValue) {
return state
public class EnumDefault<T: RawRepresentable> where T.RawValue == String {
var defaults: UserDefaults
var key: String
var defaultValue: T
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) {
self.defaults = defaults
self.key = forKey
self.defaultValue = withDefault
}
public func get() -> T {
if let rawValue = defaults.string(forKey: key),
let value = T(rawValue: rawValue) {
return value
}
return defaultValue
}
public func set(_ value: T) {
defaults.set(value.rawValue, forKey: key)
defaults.synchronize()
}
return .active
}
+48 -17
View File
@@ -17,13 +17,55 @@ public let maxImageSize: Int64 = 236700
public let maxFileSize: Int64 = 8000000
func getDocumentsDirectory() -> URL {
// FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
public func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
func getGroupContainerDirectory() -> URL {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
}
func getAppDirectory() -> URL {
dbContainerGroupDefault.get() == .group
? getGroupContainerDirectory()
: getDocumentsDirectory()
// getDocumentsDirectory()
}
let DB_FILE_PREFIX = "simplex_v1"
func getLegacyDatabasePath() -> URL {
getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false)
}
public func getAppDatabasePath() -> URL {
dbContainerGroupDefault.get() == .group
? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false)
: getLegacyDatabasePath()
// getLegacyDatabasePath()
}
public func hasLegacyDatabase() -> Bool {
let dbPath = getLegacyDatabasePath()
let fm = FileManager.default
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")
try? fm.removeItem(at: appFiles)
return r1 && r2
}
public func getAppFilesDirectory() -> URL {
getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
getAppDirectory().appendingPathComponent("app_files", isDirectory: true)
}
func getAppFilePath(_ fileName: String) -> URL {
@@ -96,8 +138,9 @@ private func saveFile(_ data: Data, _ fileName: String) -> String? {
private func uniqueCombine(_ fileName: String) -> String {
func tryCombine(_ fileName: String, _ n: Int) -> String {
let name = fileName.deletingPathExtension
let ext = fileName.pathExtension
let ns = fileName as NSString
let name = ns.deletingPathExtension
let ext = ns.pathExtension
let suffix = (n == 0) ? "" : "_\(n)"
let f = "\(name)\(suffix).\(ext)"
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
@@ -105,18 +148,6 @@ private func uniqueCombine(_ fileName: String) -> String {
return tryCombine(fileName, 0)
}
private extension String {
var ns: NSString {
return self as NSString
}
var pathExtension: String {
return ns.pathExtension
}
var deletingPathExtension: String {
return ns.deletingPathExtension
}
}
public func removeFile(_ fileName: String) {
do {
try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path)
+2
View File
@@ -18,3 +18,5 @@ typedef void* chat_ctrl;
extern chat_ctrl chat_init(char *path);
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
extern char *chat_recv_msg(chat_ctrl ctl);
extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
extern char *chat_parse_markdown(char *str);