mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
core: fix archive export when some filename is not compatible with zip (#4561)
* core: fix archive export when some filename is not compatible with zip * update * core, ios * update kotlin apis, ios: add alert to migrate from device
This commit is contained in:
parent
8fbba16f53
commit
cb76c8079c
10 changed files with 115 additions and 38 deletions
|
@ -279,8 +279,10 @@ func apiGetAppSettings(settings: AppSettings) throws -> AppSettings {
|
|||
throw r
|
||||
}
|
||||
|
||||
func apiExportArchive(config: ArchiveConfig) async throws {
|
||||
try await sendCommandOkResp(.apiExportArchive(config: config))
|
||||
func apiExportArchive(config: ArchiveConfig) async throws -> [ArchiveError] {
|
||||
let r = await chatSendCmd(.apiExportArchive(config: config))
|
||||
if case let .archiveExported(archiveErrors) = r { return archiveErrors }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] {
|
||||
|
|
|
@ -15,6 +15,7 @@ enum DatabaseAlert: Identifiable {
|
|||
case importArchive
|
||||
case archiveImported
|
||||
case archiveImportedWithErrors(archiveErrors: [ArchiveError])
|
||||
case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError])
|
||||
case deleteChat
|
||||
case chatDeleted
|
||||
case deleteLegacyDatabase
|
||||
|
@ -29,6 +30,7 @@ enum DatabaseAlert: Identifiable {
|
|||
case .importArchive: return "importArchive"
|
||||
case .archiveImported: return "archiveImported"
|
||||
case .archiveImportedWithErrors: return "archiveImportedWithErrors"
|
||||
case .archiveExportedWithErrors: return "archiveExportedWithErrors"
|
||||
case .deleteChat: return "deleteChat"
|
||||
case .chatDeleted: return "chatDeleted"
|
||||
case .deleteLegacyDatabase: return "deleteLegacyDatabase"
|
||||
|
@ -265,10 +267,18 @@ struct DatabaseView: View {
|
|||
title: Text("Chat database imported"),
|
||||
message: Text("Restart the app to use imported chat database")
|
||||
)
|
||||
case .archiveImportedWithErrors:
|
||||
case let .archiveImportedWithErrors(errs):
|
||||
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.")
|
||||
message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
|
||||
)
|
||||
case let .archiveExportedWithErrors(archivePath, errs):
|
||||
return Alert(
|
||||
title: Text("Chat database exported"),
|
||||
message: Text("You may save the exported archive.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
||||
dismissButton: .default(Text("Continue")) {
|
||||
showShareSheet(items: [archivePath])
|
||||
}
|
||||
)
|
||||
case .deleteChat:
|
||||
return Alert(
|
||||
|
@ -349,9 +359,16 @@ struct DatabaseView: View {
|
|||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let archivePath = try await exportChatArchive()
|
||||
showShareSheet(items: [archivePath])
|
||||
await MainActor.run { progressIndicator = false }
|
||||
let (archivePath, archiveErrors) = try await exportChatArchive()
|
||||
if archiveErrors.isEmpty {
|
||||
showShareSheet(items: [archivePath])
|
||||
await MainActor.run { progressIndicator = false }
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
|
||||
progressIndicator = false
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||
|
@ -486,6 +503,17 @@ struct DatabaseView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func archiveErrorsText(_ errs: [ArchiveError]) -> Text {
|
||||
return Text("\n" + errs.map(showArchiveError).joined(separator: "\n"))
|
||||
|
||||
func showArchiveError(_ err: ArchiveError) -> String {
|
||||
switch err {
|
||||
case let .import(importError): importError
|
||||
case let .fileError(file, fileError): "\(file): \(fileError)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopChatAsync() async throws {
|
||||
try await apiStopChat()
|
||||
ChatReceiver.shared.stop()
|
||||
|
|
|
@ -190,7 +190,7 @@ struct MigrateToAppGroupView: View {
|
|||
do {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
try await apiExportArchive(config: config)
|
||||
_ = try await apiExportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.exported) }
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
|
@ -222,7 +222,7 @@ struct MigrateToAppGroupView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL {
|
||||
func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [ArchiveError]) {
|
||||
let archiveTime = Date.now
|
||||
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
|
||||
let archiveName = "simplex-chat.\(ts).zip"
|
||||
|
@ -233,13 +233,13 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL {
|
|||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
try await apiExportArchive(config: config)
|
||||
let errs = try await apiExportArchive(config: config)
|
||||
if storagePath == nil {
|
||||
deleteOldArchive()
|
||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||
chatArchiveTimeDefault.set(archiveTime)
|
||||
}
|
||||
return archivePath
|
||||
return (archivePath, errs)
|
||||
}
|
||||
|
||||
func deleteOldArchive() {
|
||||
|
|
|
@ -32,6 +32,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
|
|||
case keychainError(_ title: LocalizedStringKey = "Keychain error")
|
||||
case databaseError(_ title: LocalizedStringKey = "Database error", message: String)
|
||||
case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String)
|
||||
case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError])
|
||||
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
|
@ -45,6 +46,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
|
|||
case .keychainError: return "keychainError"
|
||||
case let .databaseError(title, message): return "\(title) \(message)"
|
||||
case let .unknownError(title, message): return "\(title) \(message)"
|
||||
case let .archiveExportedWithErrors(path, _): return "archiveExportedWithErrors \(path)"
|
||||
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
|
@ -166,6 +168,14 @@ struct MigrateFromDevice: View {
|
|||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .unknownError(title, message):
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .archiveExportedWithErrors(archivePath, errs):
|
||||
return Alert(
|
||||
title: Text("Chat database exported"),
|
||||
message: Text("You may migrate the exported database.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
||||
dismissButton: .default(Text("Continue")) {
|
||||
Task { await uploadArchive(path: archivePath) }
|
||||
}
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
|
@ -449,15 +459,12 @@ struct MigrateFromDevice: View {
|
|||
Task {
|
||||
do {
|
||||
try? FileManager.default.createDirectory(at: getMigrationTempFilesDirectory(), withIntermediateDirectories: true)
|
||||
let archivePath = try await exportChatArchive(getMigrationTempFilesDirectory())
|
||||
if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path),
|
||||
let totalBytes = attrs[.size] as? Int64 {
|
||||
await MainActor.run {
|
||||
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||
}
|
||||
let (archivePath, errs) = try await exportChatArchive(getMigrationTempFilesDirectory())
|
||||
if errs.isEmpty {
|
||||
await uploadArchive(path: archivePath)
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Exported file doesn't exist")
|
||||
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: errs)
|
||||
migrationState = .uploadConfirmation
|
||||
}
|
||||
}
|
||||
|
@ -469,6 +476,20 @@ struct MigrateFromDevice: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadArchive(path archivePath: URL) async {
|
||||
if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path),
|
||||
let totalBytes = attrs[.size] as? Int64 {
|
||||
await MainActor.run {
|
||||
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||
}
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Exported file doesn't exist")
|
||||
migrationState = .uploadConfirmation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func initTemporaryDatabase() -> (chat_ctrl, User)? {
|
||||
let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl)
|
||||
|
|
|
@ -689,6 +689,7 @@ public enum ChatResponse: Decodable, Error {
|
|||
case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs)
|
||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||
case chatError(user_: UserRef?, chatError: ChatError)
|
||||
case archiveExported(archiveErrors: [ArchiveError])
|
||||
case archiveImported(archiveErrors: [ArchiveError])
|
||||
case appSettings(appSettings: AppSettings)
|
||||
|
||||
|
@ -851,6 +852,7 @@ public enum ChatResponse: Decodable, Error {
|
|||
case .agentSubsSummary: return "agentSubsSummary"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
case .chatError: return "chatError"
|
||||
case .archiveExported: return "archiveExported"
|
||||
case .archiveImported: return "archiveImported"
|
||||
case .appSettings: return "appSettings"
|
||||
}
|
||||
|
@ -1021,6 +1023,7 @@ public enum ChatResponse: Decodable, Error {
|
|||
case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary))
|
||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
case let .archiveExported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .appSettings(appSettings): return String(describing: appSettings)
|
||||
}
|
||||
|
@ -2036,8 +2039,8 @@ public enum SMPAgentError: Decodable, Hashable {
|
|||
}
|
||||
|
||||
public enum ArchiveError: Decodable, Hashable {
|
||||
case `import`(chatError: ChatError)
|
||||
case importFile(file: String, chatError: ChatError)
|
||||
case `import`(importError: String)
|
||||
case fileError(file: String, fileError: String)
|
||||
}
|
||||
|
||||
public enum RemoteCtrlError: Decodable, Hashable {
|
||||
|
|
|
@ -782,9 +782,9 @@ object ChatController {
|
|||
throw Exception("failed to get app settings: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiExportArchive(config: ArchiveConfig) {
|
||||
suspend fun apiExportArchive(config: ArchiveConfig): List<ArchiveError> {
|
||||
val r = sendCmd(null, CC.ApiExportArchive(config))
|
||||
if (r is CR.CmdOk) return
|
||||
if (r is CR.ArchiveExported) return r.archiveErrors
|
||||
throw Exception("failed to export archive: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
|
@ -4882,6 +4882,7 @@ sealed class CR {
|
|||
@Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR()
|
||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("archiveExported") class ArchiveExported(val archiveErrors: List<ArchiveError>): CR()
|
||||
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
|
||||
@Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR()
|
||||
@Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR()
|
||||
|
@ -5051,6 +5052,7 @@ sealed class CR {
|
|||
is CmdOk -> "cmdOk"
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
is ArchiveExported -> "archiveExported"
|
||||
is ArchiveImported -> "archiveImported"
|
||||
is AppSettingsR -> "appSettings"
|
||||
is Response -> "* $type"
|
||||
|
@ -5235,6 +5237,7 @@ sealed class CR {
|
|||
is CmdOk -> withUser(user, noDetails())
|
||||
is ChatCmdError -> withUser(user_, chatError.string)
|
||||
is ChatRespError -> withUser(user_, chatError.string)
|
||||
is ArchiveExported -> "${archiveErrors.map { it.string } }"
|
||||
is ArchiveImported -> "${archiveErrors.map { it.string } }"
|
||||
is AppSettingsR -> json.encodeToString(appSettings)
|
||||
is Response -> json
|
||||
|
@ -6040,11 +6043,11 @@ sealed class RCErrorType {
|
|||
@Serializable
|
||||
sealed class ArchiveError {
|
||||
val string: String get() = when (this) {
|
||||
is ArchiveErrorImport -> "import ${chatError.string}"
|
||||
is ArchiveErrorImportFile -> "importFile $file ${chatError.string}"
|
||||
is ArchiveErrorImport -> "import ${importError}"
|
||||
is ArchiveErrorFile -> "importFile $file ${fileError}"
|
||||
}
|
||||
@Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError()
|
||||
@Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError()
|
||||
@Serializable @SerialName("import") class ArchiveErrorImport(val importError: String): ArchiveError()
|
||||
@Serializable @SerialName("fileError") class ArchiveErrorFile(val file: String, val fileError: String): ArchiveError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -124,6 +124,7 @@ import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (
|
|||
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
||||
import System.Exit (ExitCode, exitSuccess)
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
import qualified System.FilePath as FP
|
||||
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush)
|
||||
import System.Random (randomRIO)
|
||||
import Text.Read (readMaybe)
|
||||
|
@ -677,7 +678,7 @@ processChatCommand' vr = \case
|
|||
chatWriteVar sel $ Just f
|
||||
APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
|
||||
SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_
|
||||
APIExportArchive cfg -> checkChatStopped $ lift (exportArchive cfg) >> ok_
|
||||
APIExportArchive cfg -> checkChatStopped $ CRArchiveExported <$> lift (exportArchive cfg)
|
||||
ExportArchive -> do
|
||||
ts <- liftIO getCurrentTime
|
||||
let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip"
|
||||
|
@ -5207,8 +5208,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||
_ -> pure ()
|
||||
|
||||
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv))
|
||||
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do
|
||||
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv' -> do
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv'
|
||||
inline <- receiveInlineMode fInv (Just mc) fileChunkSize
|
||||
ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
|
@ -5224,6 +5226,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||
fileSource = (`CryptoFile` cryptoArgs) <$> filePath
|
||||
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
|
||||
|
||||
mkValidFileInvitation :: FileInvitation -> FileInvitation
|
||||
mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = FP.makeValid $ FP.takeFileName fileName}
|
||||
|
||||
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> CM ()
|
||||
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
||||
updateRcvChatItem `catchCINotFound` \_ -> do
|
||||
|
@ -5463,8 +5468,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||
|
||||
-- TODO remove once XFile is discontinued
|
||||
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> CM ()
|
||||
processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
processFileInvitation' ct fInv' msg@RcvMessage {sharedMsgId_} msgMeta = do
|
||||
ChatConfig {fileChunkSize} <- asks config
|
||||
let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv'
|
||||
inline <- receiveInlineMode fInv Nothing fileChunkSize
|
||||
RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
|
||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||
|
|
|
@ -52,18 +52,22 @@ archiveAssetsFolder = "simplex_v1_assets"
|
|||
wallpapersFolder :: String
|
||||
wallpapersFolder = "wallpapers"
|
||||
|
||||
exportArchive :: ArchiveConfig -> CM' ()
|
||||
exportArchive :: ArchiveConfig -> CM' [ArchiveError]
|
||||
exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
|
||||
withTempDir cfg "simplex-chat." $ \dir -> do
|
||||
StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles
|
||||
copyFile (dbFilePath chatStore) $ dir </> archiveChatDbFile
|
||||
copyFile (dbFilePath agentStore) $ dir </> archiveAgentDbFile
|
||||
forM_ filesPath $ \fp ->
|
||||
copyDirectoryFiles fp $ dir </> archiveFilesFolder
|
||||
errs <-
|
||||
forM filesPath $ \fp ->
|
||||
copyValidDirectoryFiles entrySelectorError fp $ dir </> archiveFilesFolder
|
||||
forM_ assetsPath $ \fp ->
|
||||
copyDirectoryFiles (fp </> wallpapersFolder) $ dir </> archiveAssetsFolder </> wallpapersFolder
|
||||
let method = if disableCompression == Just True then Z.Store else Z.Deflate
|
||||
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
|
||||
pure $ fromMaybe [] errs
|
||||
where
|
||||
entrySelectorError f = (Z.mkEntrySelector f $> Nothing) `E.catchAny` (pure . Just . show)
|
||||
|
||||
importArchive :: ArchiveConfig -> CM' [ArchiveError]
|
||||
importArchive cfg@ArchiveConfig {archivePath} =
|
||||
|
@ -85,7 +89,7 @@ importArchive cfg@ArchiveConfig {archivePath} =
|
|||
(doesDirectoryExist fromDir)
|
||||
(copyDirectoryFiles fromDir fp)
|
||||
(pure [])
|
||||
`E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e]
|
||||
`E.catch` \(e :: E.SomeException) -> pure [AEImport $ show e]
|
||||
_ -> pure []
|
||||
|
||||
withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a)
|
||||
|
@ -94,14 +98,22 @@ withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
|
|||
_ -> withSystemTempDirectory
|
||||
|
||||
copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError]
|
||||
copyDirectoryFiles fromDir toDir = do
|
||||
copyDirectoryFiles fromDir toDir = copyValidDirectoryFiles (\_ -> pure Nothing) fromDir toDir
|
||||
|
||||
copyValidDirectoryFiles :: (FilePath -> IO (Maybe String)) -> FilePath -> FilePath -> CM' [ArchiveError]
|
||||
copyValidDirectoryFiles isFileError fromDir toDir = do
|
||||
createDirectoryIfMissing True toDir
|
||||
fs <- listDirectory fromDir
|
||||
foldM copyFileCatchError [] fs
|
||||
where
|
||||
copyFileCatchError fileErrs f =
|
||||
(copyDirectoryFile f $> fileErrs)
|
||||
`E.catch` \(e :: E.SomeException) -> pure (AEImportFile f (ChatError . CEException $ show e) : fileErrs)
|
||||
liftIO (isFileError f) >>= \case
|
||||
Nothing ->
|
||||
(copyDirectoryFile f $> fileErrs)
|
||||
`E.catch` \(e :: E.SomeException) -> addErr $ show e
|
||||
Just e -> addErr e
|
||||
where
|
||||
addErr e = pure $ AEFileError f e : fileErrs
|
||||
copyDirectoryFile f = do
|
||||
let fn = takeFileName f
|
||||
f' = fromDir </> fn
|
||||
|
|
|
@ -775,6 +775,7 @@ data ChatResponse
|
|||
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
|
||||
| CRChatError {user_ :: Maybe User, chatError :: ChatError}
|
||||
| CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]}
|
||||
| CRArchiveExported {archiveErrors :: [ArchiveError]}
|
||||
| CRArchiveImported {archiveErrors :: [ArchiveError]}
|
||||
| CRAppSettings {appSettings :: AppSettings}
|
||||
| CRTimedAction {action :: String, durationMilliseconds :: Int64}
|
||||
|
@ -1250,8 +1251,8 @@ data RemoteCtrlStopReason
|
|||
deriving (Show, Exception)
|
||||
|
||||
data ArchiveError
|
||||
= AEImport {chatError :: ChatError}
|
||||
| AEImportFile {file :: String, chatError :: ChatError}
|
||||
= AEImport {importError :: String}
|
||||
| AEFileError {file :: String, fileError :: String}
|
||||
deriving (Show, Exception)
|
||||
|
||||
-- | Host (mobile) side of transport to process remote commands and forward notifications
|
||||
|
|
|
@ -408,6 +408,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError True logLevel testView e
|
||||
CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e
|
||||
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs
|
||||
CRArchiveExported archiveErrs -> if null archiveErrs then ["ok"] else ["archive export errors: " <> plain (show archiveErrs)]
|
||||
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
|
||||
CRAppSettings as -> ["app settings: " <> viewJSON as]
|
||||
CRTimedAction _ _ -> []
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue