mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-08-06 06:29:30 +00:00
* fix: ignore pending XFTP files in storage accounting * style --------- Co-authored-by: Paul Bottinelli <paul.bottinelli@trailofbits.com>
199 lines
7.1 KiB
Haskell
199 lines
7.1 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE KindSignatures #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
module Simplex.FileTransfer.Server.Store
|
|
( FSType (..),
|
|
SFSType (..),
|
|
FileStoreClass (..),
|
|
FileRec (..),
|
|
FileRecipient (..),
|
|
STMFileStore (..),
|
|
RoundedFileTime,
|
|
fileTimePrecision,
|
|
)
|
|
where
|
|
|
|
import Data.Kind (Type)
|
|
|
|
import Control.Concurrent.STM
|
|
import Control.Monad
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
import Data.Int (Int64)
|
|
import qualified Data.Map.Strict as M
|
|
import Data.Maybe (catMaybes, isJust)
|
|
import Data.Set (Set)
|
|
import qualified Data.Set as S
|
|
import Data.Word (Word32)
|
|
import Simplex.FileTransfer.Protocol (FileInfo (..), SFileParty (..), XFTPFileId)
|
|
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Protocol (BlockingInfo, RcvPublicAuthKey, RecipientId, SenderId)
|
|
import Simplex.Messaging.Server.QueueStore (ServerEntityStatus (..))
|
|
import Simplex.Messaging.Server.StoreLog (StoreLog, closeStoreLog)
|
|
import System.IO (IOMode (..))
|
|
import Simplex.Messaging.SystemTime
|
|
import Simplex.Messaging.TMap (TMap)
|
|
import qualified Simplex.Messaging.TMap as TM
|
|
import Simplex.Messaging.Util (ifM)
|
|
|
|
data FSType = FSMemory | FSPostgres
|
|
|
|
data SFSType :: FSType -> Type where
|
|
SFSMemory :: SFSType 'FSMemory
|
|
SFSPostgres :: SFSType 'FSPostgres
|
|
|
|
data FileRec = FileRec
|
|
{ senderId :: SenderId,
|
|
fileInfo :: FileInfo,
|
|
filePath :: TVar (Maybe FilePath),
|
|
recipientIds :: TVar (Set RecipientId),
|
|
createdAt :: RoundedFileTime,
|
|
fileStatus :: TVar ServerEntityStatus
|
|
}
|
|
|
|
type RoundedFileTime = RoundedSystemTime 3600
|
|
|
|
fileTimePrecision :: Int64
|
|
fileTimePrecision = 3600
|
|
|
|
data FileRecipient = FileRecipient RecipientId C.APublicAuthKey
|
|
deriving (Show)
|
|
|
|
instance StrEncoding FileRecipient where
|
|
strEncode (FileRecipient rId rKey) = strEncode rId <> ":" <> strEncode rKey
|
|
strP = FileRecipient <$> strP <* A.char ':' <*> strP
|
|
|
|
class FileStoreClass s where
|
|
type FileStoreConfig s
|
|
newFileStore :: FileStoreConfig s -> IO s
|
|
closeFileStore :: s -> IO ()
|
|
addFile :: s -> SenderId -> FileInfo -> RoundedFileTime -> ServerEntityStatus -> IO (Either XFTPErrorType ())
|
|
setFilePath :: s -> SenderId -> FilePath -> IO (Either XFTPErrorType ())
|
|
addRecipient :: s -> SenderId -> FileRecipient -> IO (Either XFTPErrorType ())
|
|
deleteFile :: s -> SenderId -> IO (Either XFTPErrorType ())
|
|
deleteFiles :: s -> [SenderId] -> IO ()
|
|
deleteFiles s = mapM_ (void . deleteFile s)
|
|
blockFile :: s -> SenderId -> BlockingInfo -> Bool -> IO (Either XFTPErrorType ())
|
|
deleteRecipient :: s -> RecipientId -> FileRec -> IO ()
|
|
getFile :: s -> SFileParty p -> XFTPFileId -> IO (Either XFTPErrorType (FileRec, C.APublicAuthKey))
|
|
ackFile :: s -> RecipientId -> IO (Either XFTPErrorType ())
|
|
expiredFiles :: s -> Int64 -> Int -> IO [(SenderId, Maybe FilePath, Word32)]
|
|
getUsedStorage :: s -> IO Int64
|
|
getFileCount :: s -> IO Int
|
|
|
|
-- STM in-memory store
|
|
|
|
data STMFileStore = STMFileStore
|
|
{ files :: TMap SenderId FileRec,
|
|
recipients :: TMap RecipientId (SenderId, RcvPublicAuthKey),
|
|
stmStoreLog :: TVar (Maybe (StoreLog 'WriteMode))
|
|
}
|
|
|
|
instance FileStoreClass STMFileStore where
|
|
type FileStoreConfig STMFileStore = ()
|
|
|
|
newFileStore () = do
|
|
files <- TM.emptyIO
|
|
recipients <- TM.emptyIO
|
|
stmStoreLog <- newTVarIO Nothing
|
|
pure STMFileStore {files, recipients, stmStoreLog}
|
|
|
|
closeFileStore STMFileStore {stmStoreLog} = readTVarIO stmStoreLog >>= mapM_ closeStoreLog
|
|
|
|
addFile STMFileStore {files} sId fileInfo createdAt status = atomically $
|
|
ifM (TM.member sId files) (pure $ Left DUPLICATE_) $ do
|
|
f <- newFileRec sId fileInfo createdAt status
|
|
TM.insert sId f files
|
|
pure $ Right ()
|
|
|
|
setFilePath st sId fPath = atomically $
|
|
withFile st sId $ \FileRec {filePath, fileStatus} -> do
|
|
readTVar filePath >>= \case
|
|
Just _ -> pure $ Left AUTH
|
|
Nothing ->
|
|
readTVar fileStatus >>= \case
|
|
EntityActive -> do
|
|
writeTVar filePath (Just fPath)
|
|
pure $ Right ()
|
|
_ -> pure $ Left AUTH
|
|
|
|
addRecipient st@STMFileStore {recipients} senderId (FileRecipient rId rKey) = atomically $
|
|
withFile st senderId $ \FileRec {recipientIds} -> do
|
|
rIds <- readTVar recipientIds
|
|
mem <- TM.member rId recipients
|
|
if rId `S.member` rIds || mem
|
|
then pure $ Left DUPLICATE_
|
|
else do
|
|
writeTVar recipientIds $! S.insert rId rIds
|
|
TM.insert rId (senderId, rKey) recipients
|
|
pure $ Right ()
|
|
|
|
deleteFile STMFileStore {files, recipients} senderId = atomically $ do
|
|
TM.lookupDelete senderId files >>= \case
|
|
Just FileRec {recipientIds} -> do
|
|
readTVar recipientIds >>= mapM_ (`TM.delete` recipients)
|
|
pure $ Right ()
|
|
_ -> pure $ Left AUTH
|
|
|
|
blockFile st senderId info _deleted = atomically $
|
|
withFile st senderId $ \FileRec {fileStatus} -> do
|
|
writeTVar fileStatus $! EntityBlocked info
|
|
pure $ Right ()
|
|
|
|
deleteRecipient STMFileStore {recipients} rId FileRec {recipientIds} = atomically $ do
|
|
TM.delete rId recipients
|
|
modifyTVar' recipientIds $ S.delete rId
|
|
|
|
getFile st party fId = atomically $ case party of
|
|
SFSender -> withFile st fId $ pure . Right . (\f -> (f, sndKey $ fileInfo f))
|
|
SFRecipient ->
|
|
TM.lookup fId (recipients st) >>= \case
|
|
Just (sId, rKey) -> withFile st sId $ pure . Right . (,rKey)
|
|
_ -> pure $ Left AUTH
|
|
|
|
ackFile st@STMFileStore {recipients} recipientId = atomically $ do
|
|
TM.lookupDelete recipientId recipients >>= \case
|
|
Just (sId, _) ->
|
|
withFile st sId $ \FileRec {recipientIds} -> do
|
|
modifyTVar' recipientIds $ S.delete recipientId
|
|
pure $ Right ()
|
|
_ -> pure $ Left AUTH
|
|
|
|
expiredFiles STMFileStore {files} old _limit = do
|
|
fs <- readTVarIO files
|
|
fmap catMaybes . forM (M.toList fs) $ \(sId, FileRec {fileInfo = FileInfo {size}, filePath, createdAt = RoundedSystemTime createdAt}) ->
|
|
if createdAt + fileTimePrecision < old
|
|
then do
|
|
path <- readTVarIO filePath
|
|
pure $ Just (sId, path, size)
|
|
else pure Nothing
|
|
|
|
getUsedStorage STMFileStore {files} = foldM addSize 0 =<< readTVarIO files
|
|
where
|
|
addSize acc FileRec {fileInfo = FileInfo {size}, filePath} =
|
|
ifM (isJust <$> readTVarIO filePath) (pure $! acc + fromIntegral size) (pure acc)
|
|
|
|
getFileCount STMFileStore {files} = M.size <$> readTVarIO files
|
|
|
|
-- Internal STM helpers
|
|
|
|
newFileRec :: SenderId -> FileInfo -> RoundedFileTime -> ServerEntityStatus -> STM FileRec
|
|
newFileRec senderId fileInfo createdAt status = do
|
|
recipientIds <- newTVar S.empty
|
|
filePath <- newTVar Nothing
|
|
fileStatus <- newTVar status
|
|
pure FileRec {senderId, fileInfo, filePath, recipientIds, createdAt, fileStatus}
|
|
|
|
withFile :: STMFileStore -> SenderId -> (FileRec -> STM (Either XFTPErrorType a)) -> STM (Either XFTPErrorType a)
|
|
withFile STMFileStore {files} sId a =
|
|
TM.lookup sId files >>= \case
|
|
Just f -> a f
|
|
_ -> pure $ Left AUTH
|