mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-04 06:56:12 +00:00
* core: switch connection * chat items for SWITCH * additional events for connection switch * update simplexmq * test * comment test output * update messages for connection switch Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
153 lines
5.7 KiB
Haskell
153 lines
5.7 KiB
Haskell
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
module Simplex.Chat.Archive
|
|
( exportArchive,
|
|
importArchive,
|
|
deleteStorage,
|
|
sqlCipherExport,
|
|
)
|
|
where
|
|
|
|
import qualified Codec.Archive.Zip as Z
|
|
import Control.Monad.Except
|
|
import Control.Monad.Reader
|
|
import Data.Functor (($>))
|
|
import qualified Data.Text as T
|
|
import qualified Database.SQLite3 as SQL
|
|
import Simplex.Chat.Controller
|
|
import Simplex.Messaging.Agent.Client (agentClientStore)
|
|
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString)
|
|
import Simplex.Messaging.Util
|
|
import System.FilePath
|
|
import UnliftIO.Directory
|
|
import UnliftIO.Exception (SomeException, bracket, catch)
|
|
import UnliftIO.STM
|
|
import UnliftIO.Temporary
|
|
|
|
archiveAgentDbFile :: String
|
|
archiveAgentDbFile = "simplex_v1_agent.db"
|
|
|
|
archiveChatDbFile :: String
|
|
archiveChatDbFile = "simplex_v1_chat.db"
|
|
|
|
archiveFilesFolder :: String
|
|
archiveFilesFolder = "simplex_v1_files"
|
|
|
|
exportArchive :: ChatMonad m => ArchiveConfig -> m ()
|
|
exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
|
|
withTempDir cfg "simplex-chat." $ \dir -> do
|
|
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
|
copyFile chatDb $ dir </> archiveChatDbFile
|
|
copyFile agentDb $ dir </> archiveAgentDbFile
|
|
forM_ filesPath $ \fp ->
|
|
copyDirectoryFiles fp $ dir </> archiveFilesFolder
|
|
let method = if disableCompression == Just True then Z.Store else Z.Deflate
|
|
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
|
|
|
|
importArchive :: ChatMonad m => ArchiveConfig -> m ()
|
|
importArchive cfg@ArchiveConfig {archivePath} =
|
|
withTempDir cfg "simplex-chat." $ \dir -> do
|
|
Z.withArchive archivePath $ Z.unpackInto dir
|
|
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
|
backup chatDb
|
|
backup agentDb
|
|
copyFile (dir </> archiveChatDbFile) chatDb
|
|
copyFile (dir </> archiveAgentDbFile) agentDb
|
|
let filesDir = dir </> archiveFilesFolder
|
|
forM_ filesPath $ \fp ->
|
|
whenM (doesDirectoryExist filesDir) $
|
|
copyDirectoryFiles filesDir fp
|
|
where
|
|
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
|
|
|
|
withTempDir :: ChatMonad m => ArchiveConfig -> (String -> (FilePath -> m ()) -> m ())
|
|
withTempDir cfg = case parentTempDirectory cfg of
|
|
Just tmpDir -> withTempDirectory tmpDir
|
|
_ -> withSystemTempDirectory
|
|
|
|
copyDirectoryFiles :: MonadIO m => FilePath -> FilePath -> m ()
|
|
copyDirectoryFiles fromDir toDir = do
|
|
createDirectoryIfMissing False toDir
|
|
fs <- listDirectory fromDir
|
|
forM_ fs $ \f -> do
|
|
let fn = takeFileName f
|
|
f' = fromDir </> fn
|
|
whenM (doesFileExist f') $ copyFile f' $ toDir </> fn
|
|
|
|
deleteStorage :: ChatMonad m => m ()
|
|
deleteStorage = do
|
|
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
|
removeFile chatDb
|
|
removeFile agentDb
|
|
mapM_ removePathForcibly filesPath
|
|
|
|
data StorageFiles = StorageFiles
|
|
{ chatDb :: FilePath,
|
|
chatEncrypted :: TVar Bool,
|
|
agentDb :: FilePath,
|
|
agentEncrypted :: TVar Bool,
|
|
filesPath :: Maybe FilePath
|
|
}
|
|
|
|
storageFiles :: ChatMonad m => m StorageFiles
|
|
storageFiles = do
|
|
ChatController {chatStore, filesFolder, smpAgent} <- ask
|
|
let SQLiteStore {dbFilePath = chatDb, dbEncrypted = chatEncrypted} = chatStore
|
|
SQLiteStore {dbFilePath = agentDb, dbEncrypted = agentEncrypted} = agentClientStore smpAgent
|
|
filesPath <- readTVarIO filesFolder
|
|
pure StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted, filesPath}
|
|
|
|
sqlCipherExport :: forall m. ChatMonad m => DBEncryptionConfig -> m ()
|
|
sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key'} =
|
|
when (key /= key') $ do
|
|
fs@StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted} <- storageFiles
|
|
checkFile `with` fs
|
|
backup `with` fs
|
|
(export chatDb chatEncrypted >> export agentDb agentEncrypted)
|
|
`catchError` \e -> (restore `with` fs) >> throwError e
|
|
where
|
|
action `with` StorageFiles {chatDb, agentDb} = action chatDb >> action agentDb
|
|
backup f = copyFile f (f <> ".bak")
|
|
restore f = copyFile (f <> ".bak") f
|
|
checkFile f = unlessM (doesFileExist f) $ throwDBError $ DBErrorNoFile f
|
|
export f dbEnc = do
|
|
enc <- readTVarIO dbEnc
|
|
when (enc && null key) $ throwDBError DBErrorEncrypted
|
|
when (not enc && not (null key)) $ throwDBError DBErrorPlaintext
|
|
withDB (`SQL.exec` exportSQL) DBErrorExport
|
|
renameFile (f <> ".exported") f
|
|
withDB (`SQL.exec` testSQL) DBErrorOpen
|
|
atomically $ writeTVar dbEnc $ not (null key')
|
|
where
|
|
withDB a err =
|
|
liftIO (bracket (SQL.open $ T.pack f) SQL.close a $> Nothing)
|
|
`catch` checkSQLError
|
|
`catch` (\(e :: SomeException) -> sqliteError' e)
|
|
>>= mapM_ (throwDBError . err)
|
|
where
|
|
checkSQLError e = case SQL.sqlError e of
|
|
SQL.ErrorNotADatabase -> pure $ Just SQLiteErrorNotADatabase
|
|
_ -> sqliteError' e
|
|
sqliteError' :: Show e => e -> m (Maybe SQLiteError)
|
|
sqliteError' = pure . Just . SQLiteError . show
|
|
exportSQL =
|
|
T.unlines $
|
|
keySQL key
|
|
<> [ "ATTACH DATABASE " <> sqlString (f <> ".exported") <> " AS exported KEY " <> sqlString key' <> ";",
|
|
"SELECT sqlcipher_export('exported');",
|
|
"DETACH DATABASE exported;"
|
|
]
|
|
testSQL =
|
|
T.unlines $
|
|
keySQL key'
|
|
<> [ "PRAGMA foreign_keys = ON;",
|
|
"PRAGMA secure_delete = ON;",
|
|
"PRAGMA auto_vacuum = FULL;",
|
|
"SELECT count(*) FROM sqlite_master;"
|
|
]
|
|
keySQL k = ["PRAGMA key = " <> sqlString k <> ";" | not (null k)]
|