mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
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:
committed by
GitHub
parent
4d9e446489
commit
6a2f2a512f
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user