{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Messaging.Server.StoreLog
( StoreLog,
openWriteStoreLog,
openReadStoreLog,
closeStoreLog,
logCreateQueue,
logSecureQueue,
logDeleteQueue,
readWriteStoreLog,
)
where
import Control.Applicative (optional, (<|>))
import Control.Monad (unless)
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first, second)
import Data.ByteString.Base64 (encode)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Either (partitionEithers)
import Data.Functor (($>))
import Data.List (foldl')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (base64P, parseAll)
import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.QueueStore (QueueRec (..), QueueStatus (..))
import Simplex.Messaging.Transport (trimCR)
import System.Directory (doesFileExist)
import System.IO
data StoreLog (a :: IOMode) where
ReadStoreLog :: FilePath -> Handle -> StoreLog 'ReadMode
WriteStoreLog :: FilePath -> Handle -> StoreLog 'WriteMode
data StoreLogRecord
= CreateQueue QueueRec
| SecureQueue QueueId SenderPublicKey
| DeleteQueue QueueId
storeLogRecordP :: Parser StoreLogRecord
storeLogRecordP :: Parser StoreLogRecord
storeLogRecordP =
"CREATE " Parser ByteString ByteString
-> Parser StoreLogRecord -> Parser StoreLogRecord
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f b
*> Parser StoreLogRecord
createQueueP
Parser StoreLogRecord
-> Parser StoreLogRecord -> Parser StoreLogRecord
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> "SECURE " Parser ByteString ByteString
-> Parser StoreLogRecord -> Parser StoreLogRecord
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f b
*> Parser StoreLogRecord
secureQueueP
Parser StoreLogRecord
-> Parser StoreLogRecord -> Parser StoreLogRecord
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> "DELETE " Parser ByteString ByteString
-> Parser StoreLogRecord -> Parser StoreLogRecord
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f b
*> (ByteString -> StoreLogRecord
DeleteQueue (ByteString -> StoreLogRecord)
-> Parser ByteString ByteString -> Parser StoreLogRecord
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Parser ByteString ByteString
base64P)
where
createQueueP :: Parser StoreLogRecord
createQueueP = QueueRec -> StoreLogRecord
CreateQueue (QueueRec -> StoreLogRecord)
-> Parser ByteString QueueRec -> Parser StoreLogRecord
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Parser ByteString QueueRec
queueRecP
secureQueueP :: Parser StoreLogRecord
secureQueueP = ByteString -> SenderPublicKey -> StoreLogRecord
SecureQueue (ByteString -> SenderPublicKey -> StoreLogRecord)
-> Parser ByteString ByteString
-> Parser ByteString (SenderPublicKey -> StoreLogRecord)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Parser ByteString ByteString
base64P Parser ByteString (SenderPublicKey -> StoreLogRecord)
-> Parser ByteString Char
-> Parser ByteString (SenderPublicKey -> StoreLogRecord)
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* Parser ByteString Char
A.space Parser ByteString (SenderPublicKey -> StoreLogRecord)
-> Parser ByteString SenderPublicKey -> Parser StoreLogRecord
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Parser ByteString SenderPublicKey
C.pubKeyP
queueRecP :: Parser ByteString QueueRec
queueRecP = do
ByteString
recipientId <- "rid=" Parser ByteString ByteString
-> Parser ByteString ByteString -> Parser ByteString ByteString
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f b
*> Parser ByteString ByteString
base64P Parser ByteString ByteString
-> Parser ByteString Char -> Parser ByteString ByteString
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* Parser ByteString Char
A.space
ByteString
senderId <- "sid=" Parser ByteString ByteString
-> Parser ByteString ByteString -> Parser ByteString ByteString
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f b
*> Parser ByteString ByteString
base64P Parser ByteString ByteString
-> Parser ByteString Char -> Parser ByteString ByteString
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* Parser ByteString Char
A.space
SenderPublicKey
recipientKey <- "rk=" Parser ByteString ByteString
-> Parser ByteString SenderPublicKey
-> Parser ByteString SenderPublicKey
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f b
*> Parser ByteString SenderPublicKey
C.pubKeyP Parser ByteString SenderPublicKey
-> Parser ByteString Char -> Parser ByteString SenderPublicKey
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f a
<* Parser ByteString Char
A.space
Maybe SenderPublicKey
senderKey <- "sk=" Parser ByteString ByteString
-> Parser ByteString (Maybe SenderPublicKey)
-> Parser ByteString (Maybe SenderPublicKey)
forall (f :: * -> *) a b. Applicative f => f a -> f b -> f b
*> Parser ByteString SenderPublicKey
-> Parser ByteString (Maybe SenderPublicKey)
forall (f :: * -> *) a. Alternative f => f a -> f (Maybe a)
optional Parser ByteString SenderPublicKey
C.pubKeyP
QueueRec -> Parser ByteString QueueRec
forall (f :: * -> *) a. Applicative f => a -> f a
pure QueueRec :: ByteString
-> ByteString
-> SenderPublicKey
-> Maybe SenderPublicKey
-> QueueStatus
-> QueueRec
QueueRec {ByteString
recipientId :: ByteString
recipientId :: ByteString
recipientId, ByteString
senderId :: ByteString
senderId :: ByteString
senderId, SenderPublicKey
recipientKey :: SenderPublicKey
recipientKey :: SenderPublicKey
recipientKey, Maybe SenderPublicKey
senderKey :: Maybe SenderPublicKey
senderKey :: Maybe SenderPublicKey
senderKey, status :: QueueStatus
status = QueueStatus
QueueActive}
serializeStoreLogRecord :: StoreLogRecord -> ByteString
serializeStoreLogRecord :: StoreLogRecord -> ByteString
serializeStoreLogRecord = \case
CreateQueue q :: QueueRec
q -> "CREATE " ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> QueueRec -> ByteString
serializeQueue QueueRec
q
SecureQueue rId :: ByteString
rId sKey :: SenderPublicKey
sKey -> "SECURE " ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString -> ByteString
encode ByteString
rId ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> " " ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> SenderPublicKey -> ByteString
C.serializePubKey SenderPublicKey
sKey
DeleteQueue rId :: ByteString
rId -> "DELETE " ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString -> ByteString
encode ByteString
rId
where
serializeQueue :: QueueRec -> ByteString
serializeQueue QueueRec {ByteString
recipientId :: ByteString
recipientId :: QueueRec -> ByteString
recipientId, ByteString
senderId :: ByteString
senderId :: QueueRec -> ByteString
senderId, SenderPublicKey
recipientKey :: SenderPublicKey
recipientKey :: QueueRec -> SenderPublicKey
recipientKey, Maybe SenderPublicKey
senderKey :: Maybe SenderPublicKey
senderKey :: QueueRec -> Maybe SenderPublicKey
senderKey} =
[ByteString] -> ByteString
B.unwords
[ "rid=" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString -> ByteString
encode ByteString
recipientId,
"sid=" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString -> ByteString
encode ByteString
senderId,
"rk=" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> SenderPublicKey -> ByteString
C.serializePubKey SenderPublicKey
recipientKey,
"sk=" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
-> (SenderPublicKey -> ByteString)
-> Maybe SenderPublicKey
-> ByteString
forall b a. b -> (a -> b) -> Maybe a -> b
maybe "" SenderPublicKey -> ByteString
C.serializePubKey Maybe SenderPublicKey
senderKey
]
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
openWriteStoreLog f :: FilePath
f = FilePath -> Handle -> StoreLog 'WriteMode
WriteStoreLog FilePath
f (Handle -> StoreLog 'WriteMode)
-> IO Handle -> IO (StoreLog 'WriteMode)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> IOMode -> IO Handle
openFile FilePath
f IOMode
WriteMode
openReadStoreLog :: FilePath -> IO (StoreLog 'ReadMode)
openReadStoreLog :: FilePath -> IO (StoreLog 'ReadMode)
openReadStoreLog f :: FilePath
f = do
FilePath -> IO Bool
doesFileExist FilePath
f IO Bool -> (Bool -> IO ()) -> IO ()
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= (Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
`unless` FilePath -> FilePath -> IO ()
writeFile FilePath
f "")
FilePath -> Handle -> StoreLog 'ReadMode
ReadStoreLog FilePath
f (Handle -> StoreLog 'ReadMode)
-> IO Handle -> IO (StoreLog 'ReadMode)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> IOMode -> IO Handle
openFile FilePath
f IOMode
ReadMode
closeStoreLog :: StoreLog a -> IO ()
closeStoreLog :: StoreLog a -> IO ()
closeStoreLog = \case
WriteStoreLog _ h :: Handle
h -> Handle -> IO ()
hClose Handle
h
ReadStoreLog _ h :: Handle
h -> Handle -> IO ()
hClose Handle
h
writeStoreLogRecord :: StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord :: StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord (WriteStoreLog _ h :: Handle
h) r :: StoreLogRecord
r = do
Handle -> ByteString -> IO ()
B.hPutStrLn Handle
h (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ StoreLogRecord -> ByteString
serializeStoreLogRecord StoreLogRecord
r
Handle -> IO ()
hFlush Handle
h
logCreateQueue :: StoreLog 'WriteMode -> QueueRec -> IO ()
logCreateQueue :: StoreLog 'WriteMode -> QueueRec -> IO ()
logCreateQueue s :: StoreLog 'WriteMode
s = StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord StoreLog 'WriteMode
s (StoreLogRecord -> IO ())
-> (QueueRec -> StoreLogRecord) -> QueueRec -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. QueueRec -> StoreLogRecord
CreateQueue
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SenderPublicKey -> IO ()
logSecureQueue :: StoreLog 'WriteMode -> ByteString -> SenderPublicKey -> IO ()
logSecureQueue s :: StoreLog 'WriteMode
s qId :: ByteString
qId sKey :: SenderPublicKey
sKey = StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord StoreLog 'WriteMode
s (StoreLogRecord -> IO ()) -> StoreLogRecord -> IO ()
forall a b. (a -> b) -> a -> b
$ ByteString -> SenderPublicKey -> StoreLogRecord
SecureQueue ByteString
qId SenderPublicKey
sKey
logDeleteQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
logDeleteQueue :: StoreLog 'WriteMode -> ByteString -> IO ()
logDeleteQueue s :: StoreLog 'WriteMode
s = StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord StoreLog 'WriteMode
s (StoreLogRecord -> IO ())
-> (ByteString -> StoreLogRecord) -> ByteString -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> StoreLogRecord
DeleteQueue
readWriteStoreLog :: StoreLog 'ReadMode -> IO (Map RecipientId QueueRec, StoreLog 'WriteMode)
readWriteStoreLog :: StoreLog 'ReadMode
-> IO (Map ByteString QueueRec, StoreLog 'WriteMode)
readWriteStoreLog s :: StoreLog 'ReadMode
s@(ReadStoreLog f :: FilePath
f _) = do
Map ByteString QueueRec
qs <- StoreLog 'ReadMode -> IO (Map ByteString QueueRec)
readQueues StoreLog 'ReadMode
s
StoreLog 'ReadMode -> IO ()
forall (a :: IOMode). StoreLog a -> IO ()
closeStoreLog StoreLog 'ReadMode
s
StoreLog 'WriteMode
s' <- FilePath -> IO (StoreLog 'WriteMode)
openWriteStoreLog FilePath
f
StoreLog 'WriteMode -> Map ByteString QueueRec -> IO ()
writeQueues StoreLog 'WriteMode
s' Map ByteString QueueRec
qs
(Map ByteString QueueRec, StoreLog 'WriteMode)
-> IO (Map ByteString QueueRec, StoreLog 'WriteMode)
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Map ByteString QueueRec
qs, StoreLog 'WriteMode
s')
writeQueues :: StoreLog 'WriteMode -> Map RecipientId QueueRec -> IO ()
writeQueues :: StoreLog 'WriteMode -> Map ByteString QueueRec -> IO ()
writeQueues s :: StoreLog 'WriteMode
s = (QueueRec -> IO ()) -> Map ByteString QueueRec -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord StoreLog 'WriteMode
s (StoreLogRecord -> IO ())
-> (QueueRec -> StoreLogRecord) -> QueueRec -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. QueueRec -> StoreLogRecord
CreateQueue) (Map ByteString QueueRec -> IO ())
-> (Map ByteString QueueRec -> Map ByteString QueueRec)
-> Map ByteString QueueRec
-> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (QueueRec -> Bool)
-> Map ByteString QueueRec -> Map ByteString QueueRec
forall a k. (a -> Bool) -> Map k a -> Map k a
M.filter QueueRec -> Bool
active
where
active :: QueueRec -> Bool
active QueueRec {QueueStatus
status :: QueueStatus
status :: QueueRec -> QueueStatus
status} = QueueStatus
status QueueStatus -> QueueStatus -> Bool
forall a. Eq a => a -> a -> Bool
== QueueStatus
QueueActive
type LogParsingError = (String, ByteString)
readQueues :: StoreLog 'ReadMode -> IO (Map RecipientId QueueRec)
readQueues :: StoreLog 'ReadMode -> IO (Map ByteString QueueRec)
readQueues (ReadStoreLog _ h :: Handle
h) = Handle -> IO ByteString
LB.hGetContents Handle
h IO ByteString
-> (ByteString -> IO (Map ByteString QueueRec))
-> IO (Map ByteString QueueRec)
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= ([LogParsingError], Map ByteString QueueRec)
-> IO (Map ByteString QueueRec)
returnResult (([LogParsingError], Map ByteString QueueRec)
-> IO (Map ByteString QueueRec))
-> (ByteString -> ([LogParsingError], Map ByteString QueueRec))
-> ByteString
-> IO (Map ByteString QueueRec)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ([LogParsingError], Map ByteString QueueRec)
procStoreLog
where
procStoreLog :: LB.ByteString -> ([LogParsingError], Map RecipientId QueueRec)
procStoreLog :: ByteString -> ([LogParsingError], Map ByteString QueueRec)
procStoreLog = ([StoreLogRecord] -> Map ByteString QueueRec)
-> ([LogParsingError], [StoreLogRecord])
-> ([LogParsingError], Map ByteString QueueRec)
forall (p :: * -> * -> *) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second ((Map ByteString QueueRec
-> StoreLogRecord -> Map ByteString QueueRec)
-> Map ByteString QueueRec
-> [StoreLogRecord]
-> Map ByteString QueueRec
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' Map ByteString QueueRec
-> StoreLogRecord -> Map ByteString QueueRec
procLogRecord Map ByteString QueueRec
forall k a. Map k a
M.empty) (([LogParsingError], [StoreLogRecord])
-> ([LogParsingError], Map ByteString QueueRec))
-> (ByteString -> ([LogParsingError], [StoreLogRecord]))
-> ByteString
-> ([LogParsingError], Map ByteString QueueRec)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Either LogParsingError StoreLogRecord]
-> ([LogParsingError], [StoreLogRecord])
forall a b. [Either a b] -> ([a], [b])
partitionEithers ([Either LogParsingError StoreLogRecord]
-> ([LogParsingError], [StoreLogRecord]))
-> (ByteString -> [Either LogParsingError StoreLogRecord])
-> ByteString
-> ([LogParsingError], [StoreLogRecord])
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (ByteString -> Either LogParsingError StoreLogRecord)
-> [ByteString] -> [Either LogParsingError StoreLogRecord]
forall a b. (a -> b) -> [a] -> [b]
map ByteString -> Either LogParsingError StoreLogRecord
parseLogRecord ([ByteString] -> [Either LogParsingError StoreLogRecord])
-> (ByteString -> [ByteString])
-> ByteString
-> [Either LogParsingError StoreLogRecord]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> [ByteString]
LB.lines
returnResult :: ([LogParsingError], Map RecipientId QueueRec) -> IO (Map RecipientId QueueRec)
returnResult :: ([LogParsingError], Map ByteString QueueRec)
-> IO (Map ByteString QueueRec)
returnResult (errs :: [LogParsingError]
errs, res :: Map ByteString QueueRec
res) = (LogParsingError -> IO ()) -> [LogParsingError] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ LogParsingError -> IO ()
printError [LogParsingError]
errs IO () -> Map ByteString QueueRec -> IO (Map ByteString QueueRec)
forall (f :: * -> *) a b. Functor f => f a -> b -> f b
$> Map ByteString QueueRec
res
parseLogRecord :: LB.ByteString -> Either LogParsingError StoreLogRecord
parseLogRecord :: ByteString -> Either LogParsingError StoreLogRecord
parseLogRecord = (\s :: ByteString
s -> (FilePath -> LogParsingError)
-> Either FilePath StoreLogRecord
-> Either LogParsingError StoreLogRecord
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (,ByteString
s) (Either FilePath StoreLogRecord
-> Either LogParsingError StoreLogRecord)
-> Either FilePath StoreLogRecord
-> Either LogParsingError StoreLogRecord
forall a b. (a -> b) -> a -> b
$ Parser StoreLogRecord
-> ByteString -> Either FilePath StoreLogRecord
forall a. Parser a -> ByteString -> Either FilePath a
parseAll Parser StoreLogRecord
storeLogRecordP ByteString
s) (ByteString -> Either LogParsingError StoreLogRecord)
-> (ByteString -> ByteString)
-> ByteString
-> Either LogParsingError StoreLogRecord
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
trimCR (ByteString -> ByteString)
-> (ByteString -> ByteString) -> ByteString -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
LB.toStrict
procLogRecord :: Map RecipientId QueueRec -> StoreLogRecord -> Map RecipientId QueueRec
procLogRecord :: Map ByteString QueueRec
-> StoreLogRecord -> Map ByteString QueueRec
procLogRecord m :: Map ByteString QueueRec
m = \case
CreateQueue q :: QueueRec
q -> ByteString
-> QueueRec -> Map ByteString QueueRec -> Map ByteString QueueRec
forall k a. Ord k => k -> a -> Map k a -> Map k a
M.insert (QueueRec -> ByteString
recipientId QueueRec
q) QueueRec
q Map ByteString QueueRec
m
SecureQueue qId :: ByteString
qId sKey :: SenderPublicKey
sKey -> (QueueRec -> QueueRec)
-> ByteString -> Map ByteString QueueRec -> Map ByteString QueueRec
forall k a. Ord k => (a -> a) -> k -> Map k a -> Map k a
M.adjust (\q :: QueueRec
q -> QueueRec
q {senderKey :: Maybe SenderPublicKey
senderKey = SenderPublicKey -> Maybe SenderPublicKey
forall a. a -> Maybe a
Just SenderPublicKey
sKey}) ByteString
qId Map ByteString QueueRec
m
DeleteQueue qId :: ByteString
qId -> ByteString -> Map ByteString QueueRec -> Map ByteString QueueRec
forall k a. Ord k => k -> Map k a -> Map k a
M.delete ByteString
qId Map ByteString QueueRec
m
printError :: LogParsingError -> IO ()
printError :: LogParsingError -> IO ()
printError (e :: FilePath
e, s :: ByteString
s) = ByteString -> IO ()
B.putStrLn (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ "Error parsing log: " ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> FilePath -> ByteString
B.pack FilePath
e ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> " - " ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
s