diff --git a/docs/rfcs/files.mmd b/docs/rfcs/files.mmd new file mode 100644 index 0000000000..4431726b77 --- /dev/null +++ b/docs/rfcs/files.mmd @@ -0,0 +1,129 @@ +sequenceDiagram + participant AU as Alice's
chat UI + participant AC as Alice's
chat core + participant AA as Alice's
agent + participant BA as Bob's
agent + participant BC as Bob's
chat + participant BU as Bob's
chat UI + + note over AU, AA: Alice's app + note over BA, BU: Bob's app + + note over AU, BU: Direct file transfer handshake + + note over AU: Send message with file + AU ->> AC: APISendMessage with filePath + AC ->> AA: createConnection + AA ->> AC: connId, fileConnReq + note left of AC: createSndFileTransfer
files.ci_file_status = CIFSSndStored
snd_files.file_status = FSNew
connections.conn_type = ConnSndFile
connections.conn_status = ConnNew + AC ->> BC: XMsgNew with FileInvitation, fileConnReq = fileConnReq + note right of BC: createRcvFileTransfer
files.ci_file_status = CIFSRcvInvitation
rcv_files.file_status = FSNew
rcv_files.file_queue_info = fileConnReq + BC ->> BU: CRNewChatItem + note over BU: Accept file + BU ->> BC: ReceiveFile + BC ->> BA: joinConnection with ConnInfo = XFileAcpt + BA ->> BC: connId + note right of BC: acceptRcvFileTransfer
files.ci_file_status = CIFSRcvAccepted
rcv_files.file_status = FSAccepted
connections.conn_type = ConnRcvFile
connections.conn_status = ConnJoined + BA ->> AC: CONF with XFileAcpt + note left of AC: snd_files.file_status = FSAccepted + AC ->> AA: allowConnection + note left of AC: connections.conn_status = ConnAccepted + note over AA, BA: ...
Connection handshake
... + par Alice connected + AA ->> AC: CON + note left of AC: connections.conn_status = ConnReady
snd_files.file_status = FSConnected
files.ci_file_status = CIFSSndTransfer + AC ->> AU: CRSndFileStart + note over AC: sendFileChunk + and Bob Connected + BA ->> BC: CON + note right of BC: connections.conn_status = ConnReady
rcv_files.file_status = FSConnected
files.ci_file_status = CIFSRcvTransfer + BC ->> BU: CRRcvFileStart + end + + note over AU, BU: Group file transfer handshake + + note over AU: Send message with file + AU ->> AC: APISendMessage with filePath + note left of AC: createSndGroupFileTransfer
files.ci_file_status = CIFSSndStored + AC ->> BC: XMsgNew with FileInvitation, fileConnReq = Nothing + note right of BC: createRcvGroupFileTransfer
files.ci_file_status = CIFSRcvInvitation
rcv_files.file_status = FSNew
rcv_files.file_queue_info = NULL + BC ->> BU: CRNewChatItem + note over BU: Accept file + BU ->> BC: ReceiveFile + BC ->> BA: createConnection + BA ->> BC: connId, fileInvConnReq + note right of BC: acceptRcvFileTransfer
files.ci_file_status = CIFSRcvAccepted
rcv_files.file_status = FSAccepted
connections.conn_type = ConnRcvFile
connections.conn_status = ConnNew + BC ->> AC: XFileAcptInv sharedMsgId fileInvConnReq + AC ->> AA: joinConnection with ConnInfo = XOk + AA ->> AC: connId + note left of AC: createSndGroupFileTransferConnection
connections.conn_type = ConnSndFile
connections.conn_status = ConnNew
snd_files.file_status = FSAccepted + AA ->> BC: CONF with XOk + BC ->> BA: allowConnection + note right of BC: connections.conn_status = ConnAccepted + note over AA, BA: ...
Connection handshake
... + par Alice connected + AA ->> AC: CON + note left of AC: connections.conn_status = ConnReady
snd_files.file_status = FSConnected
files.ci_file_status = CIFSSndTransfer + AC ->> AU: CRSndFileStart + note over AC: sendFileChunk + and Bob Connected + BA ->> BC: CON + note right of BC: connections.conn_status = ConnReady
rcv_files.file_status = FSConnected
files.ci_file_status = CIFSRcvTransfer + BC ->> BU: CRRcvFileStart + end + + note over AU, BU: File transfer + + loop while createSndFileChunk returns chunkNo + AC ->> BC: FileChunk + AA ->> AC: SENT + note over BC: appendFileChunk + end + opt receiver cancelled file transfer + note over BU: Cancel file + BU ->> BC: CancelFile + note right of BC: files.cancelled = true
files.ci_file_status = CIFSRcvCancelled
rcv_files.file_status = FSCancelled
deleteRcvFileChunks + BC ->> BA: deleteConnection + BC ->> BU: CRRcvFileSndCancelled + note over AA, BA: connection is deleted + AC ->> BA: FileChunk + BA ->> AA: MERR AUTH + AA ->> AC: MERR + note over AC: cancelSndFileTransfer + alt AUTH + AC ->> AU: CRSndFileRcvCancelled + else other error, possibly not due to cancel + AC ->> AU: CEFileSend + end + end + AC ->> BC: Final FileChunk + note left of AC: snd_files.file_status = FSComplete
deleteSndFileChunks
files.ci_file_status = CIFSSndComplete + AC ->> AU: CRSndFileComplete + AC ->> AA: deleteConnection + note over BC: appendFileChunk + note right of BC: rcv_files.file_status = FSComplete
files.ci_file_status = CIFSRcvComplete
deleteRcvFileChunks + BC ->> BU: CRRcvFileComplete + BC ->> BA: deleteConnection + + note over AU, BU: Sender cancels file transfer + + note over AU: Cancel file + AU ->> AC: CancelFile + note left of AC: files.cancelled = true
files.ci_file_status = CIFSSndCancelled
snd_files.file_status = FSCancelled
deleteSndFileChunks + AC ->> BA: FileChunkCancel (over file connection) + note left of AC: deleteConnection + AC ->> BA: XFileCancel (over direct/group connection) + AC ->> AU: CRSndGroupFileCancelled + par FileChunkCancel + BA ->> BC: FileChunkCancel + note over BC: Cancel file (if it wasn't already cancelled) + note right of BC: files.cancelled = true
files.ci_file_status = CIFSRcvCancelled
rcv_files.file_status = FSCancelled
deleteRcvFileChunks + BC ->> BA: deleteConnection + BC ->> BU: CRRcvFileSndCancelled + and XFileCancel + BA ->> BC: XFileCancel + note over BC: Cancel file (if it wasn't already cancelled) + note right of BC: files.cancelled = true
files.ci_file_status = CIFSRcvCancelled
rcv_files.file_status = FSCancelled
deleteRcvFileChunks + BC ->> BA: deleteConnection + BC ->> BU: CRRcvFileSndCancelled + end diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2bd6bdfd00..bbc8000c28 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -842,7 +842,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F tryError (withAgent $ \a -> joinConnection a connReq . directMessage $ XFileAcpt fName) >>= \case Right agentConnId -> do filePath <- getRcvFilePath filePath_ fName - withStore $ \st -> acceptRcvFileTransfer st user fileId agentConnId filePath + withStore $ \st -> acceptRcvFileTransfer st user fileId agentConnId ConnJoined filePath Left e -> throwError e -- group file protocol Nothing -> @@ -855,7 +855,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F sharedMsgId <- withStore $ \st -> getSharedMsgIdByFileId st userId fileId (agentConnId, fileInvConnReq) <- withAgent (`createConnection` SCMInvitation) filePath <- getRcvFilePath filePath_ fName - ci <- withStore $ \st -> acceptRcvFileTransfer st user fileId agentConnId filePath + ci <- withStore $ \st -> acceptRcvFileTransfer st user fileId agentConnId ConnNew filePath void $ sendDirectMessage conn (XFileAcptInv sharedMsgId fileInvConnReq fName) (GroupId groupId) pure ci _ -> throwChatError $ CEFileInternal "member connection not active" @@ -1836,16 +1836,18 @@ parseFileChunk msg = appendFileChunk :: ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> m () appendFileChunk ft@RcvFileTransfer {fileId, fileStatus} chunkNo chunk = case fileStatus of - RFSConnected RcvFileInfo {filePath} -> do - fsFilePath <- toFSFilePath filePath - append_ filePath fsFilePath + RFSConnected RcvFileInfo {filePath} -> append_ filePath + -- sometimes update of file transfer status to FSConnected + -- doesn't complete in time before MSG with first file chunk + RFSAccepted RcvFileInfo {filePath} -> append_ filePath RFSCancelled _ -> pure () _ -> throwChatError $ CEFileInternal "receiving file transfer not in progress" where - append_ fPath fPathUsed = do - h <- getFileHandle fileId fPathUsed rcvFiles AppendMode + append_ filePath = do + fsFilePath <- toFSFilePath filePath + h <- getFileHandle fileId fsFilePath rcvFiles AppendMode E.try (liftIO $ B.hPut h chunk >> hFlush h) >>= \case - Left (e :: E.SomeException) -> throwChatError . CEFileWrite fPath $ show e + Left (e :: E.SomeException) -> throwChatError . CEFileWrite fsFilePath $ show e Right () -> withStore $ \st -> updatedRcvFileChunkStored st ft chunkNo getFileHandle :: ChatMonad m => Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> m Handle diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 30e8f8a4d2..c08406646a 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -2025,8 +2025,8 @@ createRcvGroupFileTransfer st userId GroupMember {groupId, groupMemberId, localD currentTs <- getCurrentTime DB.execute db - "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (userId, groupId, fileName, fileSize, chunkSize, currentTs, currentTs) + "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (userId, groupId, fileName, fileSize, chunkSize, CIFSRcvInvitation, currentTs, currentTs) fileId <- insertedRowId db DB.execute db @@ -2082,8 +2082,8 @@ getRcvFileTransfer_ db userId fileId = cancelled = fromMaybe False cancelled_ rcvFileTransfer _ = Left $ SERcvFileNotFound fileId -acceptRcvFileTransfer :: StoreMonad m => SQLiteStore -> User -> Int64 -> ConnId -> FilePath -> m AChatItem -acceptRcvFileTransfer st user@User {userId} fileId agentConnId filePath = +acceptRcvFileTransfer :: StoreMonad m => SQLiteStore -> User -> Int64 -> ConnId -> ConnStatus -> FilePath -> m AChatItem +acceptRcvFileTransfer st user@User {userId} fileId agentConnId connStatus filePath = liftIOEither . withTransaction st $ \db -> do currentTs <- getCurrentTime DB.execute @@ -2097,7 +2097,7 @@ acceptRcvFileTransfer st user@User {userId} fileId agentConnId filePath = DB.execute db "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (agentConnId, ConnJoined, ConnRcvFile, fileId, userId, currentTs, currentTs) + (agentConnId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs) getChatItemByFileId_ db user fileId updateRcvFileStatus :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> FileStatus -> m ()