mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 14:15:55 +00:00
core: add missing status transitions for group file transfer; fix group file delivery race condition (#640)
This commit is contained in:
129
docs/rfcs/files.mmd
Normal file
129
docs/rfcs/files.mmd
Normal file
@@ -0,0 +1,129 @@
|
||||
sequenceDiagram
|
||||
participant AU as Alice's<br>chat UI
|
||||
participant AC as Alice's<br>chat core
|
||||
participant AA as Alice's<br>agent
|
||||
participant BA as Bob's<br>agent
|
||||
participant BC as Bob's<br>chat
|
||||
participant BU as Bob's<br>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<br>files.ci_file_status = CIFSSndStored<br>snd_files.file_status = FSNew<br>connections.conn_type = ConnSndFile<br>connections.conn_status = ConnNew
|
||||
AC ->> BC: XMsgNew with FileInvitation, fileConnReq = fileConnReq
|
||||
note right of BC: createRcvFileTransfer<br>files.ci_file_status = CIFSRcvInvitation<br>rcv_files.file_status = FSNew<br>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<br>files.ci_file_status = CIFSRcvAccepted<br>rcv_files.file_status = FSAccepted<br>connections.conn_type = ConnRcvFile<br>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: ...<br>Connection handshake<br>...
|
||||
par Alice connected
|
||||
AA ->> AC: CON
|
||||
note left of AC: connections.conn_status = ConnReady<br>snd_files.file_status = FSConnected<br>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<br>rcv_files.file_status = FSConnected<br>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<br>files.ci_file_status = CIFSSndStored
|
||||
AC ->> BC: XMsgNew with FileInvitation, fileConnReq = Nothing
|
||||
note right of BC: createRcvGroupFileTransfer<br>files.ci_file_status = CIFSRcvInvitation<br>rcv_files.file_status = FSNew<br>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<br>files.ci_file_status = CIFSRcvAccepted<br>rcv_files.file_status = FSAccepted<br>connections.conn_type = ConnRcvFile<br>connections.conn_status = ConnNew
|
||||
BC ->> AC: XFileAcptInv sharedMsgId fileInvConnReq
|
||||
AC ->> AA: joinConnection with ConnInfo = XOk
|
||||
AA ->> AC: connId
|
||||
note left of AC: createSndGroupFileTransferConnection<br>connections.conn_type = ConnSndFile<br>connections.conn_status = ConnNew<br>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: ...<br>Connection handshake<br>...
|
||||
par Alice connected
|
||||
AA ->> AC: CON
|
||||
note left of AC: connections.conn_status = ConnReady<br>snd_files.file_status = FSConnected<br>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<br>rcv_files.file_status = FSConnected<br>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<br>files.ci_file_status = CIFSRcvCancelled<br>rcv_files.file_status = FSCancelled<br>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<br>deleteSndFileChunks<br>files.ci_file_status = CIFSSndComplete
|
||||
AC ->> AU: CRSndFileComplete
|
||||
AC ->> AA: deleteConnection
|
||||
note over BC: appendFileChunk
|
||||
note right of BC: rcv_files.file_status = FSComplete<br>files.ci_file_status = CIFSRcvComplete<br>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<br>files.ci_file_status = CIFSSndCancelled<br>snd_files.file_status = FSCancelled<br>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<br>files.ci_file_status = CIFSRcvCancelled<br>rcv_files.file_status = FSCancelled<br>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<br>files.ci_file_status = CIFSRcvCancelled<br>rcv_files.file_status = FSCancelled<br>deleteRcvFileChunks
|
||||
BC ->> BA: deleteConnection
|
||||
BC ->> BU: CRRcvFileSndCancelled
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
Reference in New Issue
Block a user