{-|
Module      : Network.FTP.Client
Description : Transfer files over FTP and FTPS
License     : Public Domain
Stability   : experimental
Portability : POSIX
-}
module Network.FTP.Client (
    -- * Main Entrypoints
    withFTP,
    withFTPS,
    -- * Control Commands
    login,
    pasv,
    rename,
    dele,
    cwd,
    size,
    mkd,
    rmd,
    pwd,
    quit,
    -- * Data Commands
    nlst,
    retr,
    list,
    stor,
    mlsd,
    mlst,
    -- * Types
    FTPCommand(..),
    FTPResponse(..),
    ResponseStatus(..),
    MlsxResponse(..),
    RTypeCode(..),
    PortActivity(..),
    ProtType(..),
    Security(..),
    Handle(..),
    -- * Exceptions
    FTPException(..),
    -- * Handle Implementations
    sIOHandleImpl,
    tlsHandleImpl,
    -- * Lower Level Functions
    sendCommand,
    sendCommandS,
    sendAll,
    sendAllS,
    getLineResp,
    getResponse,
    getResponseS,
    sendCommandLine,
    createSendDataCommand,
    createTLSSendDataCommand,
    parseMlsxLine
) where

import qualified Data.ByteString.Char8 as C
import qualified Data.ByteString as B
import Data.ByteString (ByteString)
import Data.List
import Data.Attoparsec.ByteString.Char8
import qualified Network.Socket as S
import qualified System.IO as SIO
import Data.Monoid ((<>), mconcat)
import Control.Exception
import Control.Monad.Catch (MonadCatch, MonadMask)
import qualified Control.Monad.Catch as M
import Control.Monad
import Control.Monad.IO.Class
import Data.Bits
import Network.Connection
import System.IO.Error
import Data.ByteString.Lazy.Internal (defaultChunkSize)
import Data.Functor ((<$>))
import Control.Applicative ((<*>))
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Control.Arrow
import Data.Typeable

import Debug.Trace

debugging :: Bool
debugging = True

debugPrint :: (Show a, MonadIO m) => a -> m ()
debugPrint s = debugPrint' s debugging
    where
        debugPrint' _ False = return ()
        debugPrint' s True = liftIO $ print s

debugResponse :: (Show a, MonadIO m) => a -> m ()
debugResponse s = debugPrint $ "Recieved: " <> (show s)

data Security = Clear | TLS

-- | Can send and recieve a 'Data.ByteString.ByteString'.
data Handle = Handle
    { send :: ByteString -> IO ()
    , sendLine :: ByteString -> IO ()
    , recv :: Int -> IO ByteString
    , recvLine :: IO ByteString
    , security :: Security
    }

data FTPMessage = SingleLine ByteString | MultiLine [ByteString]

instance Show FTPMessage where
    show (SingleLine message) = C.unpack message
    show (MultiLine messages) = intercalate "\n" $ C.unpack <$> messages

-- | Response from an FTP command. ex "200 Welcome!"
data FTPResponse = FTPResponse {
    frStatus :: ResponseStatus, -- ^ Interpretation of the first digit of an FTP response code
    frCode :: Int, -- ^ The three digit response code
    frMessage :: FTPMessage -- ^ Text of the response
}

instance Show FTPResponse where
    show fr = (show $ frCode fr) <> " " <> (show $ frMessage fr)

-- | First digit of an FTP response
data ResponseStatus
    = Wait -- ^ 1
    | Success -- ^ 2
    | Continue -- ^ 3
    | FailureRetry -- ^ 4
    | Failure -- ^ 5
    deriving (Show, Eq)

data FTPException
    = FailureRetryException FTPResponse
    | FailureException FTPResponse
    | UnsuccessfulException FTPResponse
    | BogusResponseFormatException FTPResponse
    | BadProtocolResponseException ByteString
    deriving (Show, Typeable)

instance Exception FTPException

responseStatus :: ByteString -> ResponseStatus
responseStatus cbs =
    case C.uncons cbs of
        Just ('1', _) -> Wait
        Just ('2', _) -> Success
        Just ('3', _) -> Continue
        Just ('4', _) -> FailureRetry
        Just ('5', _) -> Failure
        _ -> throw $ BadProtocolResponseException cbs

data RTypeCode = TA | TI

serialzeRTypeCode :: RTypeCode -> String
serialzeRTypeCode TA = "A"
serialzeRTypeCode TI = "I"

data PortActivity = Active | Passive

data ProtType = P | C

-- | Commands according to the FTP specification
data FTPCommand
    = User String
    | Pass String
    | Acct String
    | RType RTypeCode
    | Retr String
    | Nlst [String]
    | Port S.HostAddress S.PortNumber
    | Stor String
    | List [String]
    | Rnfr String
    | Rnto String
    | Dele String
    | Size String
    | Mkd String
    | Rmd String
    | Pbsz Int
    | Prot ProtType
    | Mlsd String
    | Mlst String
    | Cwd String
    | Cdup
    | Ccc
    | Auth
    | Pwd
    | Abor
    | Pasv
    | Quit

instance Show FTPCommand where
    show = serializeCommand

formatPort :: S.HostAddress -> S.PortNumber -> String
formatPort ha pn =
    let (w1, w2, w3, w4) = S.hostAddressToTuple ha
        hn = show <$> [w1, w2, w3, w4]
        portParts = show <$> [pn `quot` 256, pn `mod` 256]
    in intercalate "," (hn <> portParts)

serializeCommand :: FTPCommand -> String
serializeCommand (User user)  = "USER " <> user
serializeCommand (Pass pass)  = "PASS " <> pass
serializeCommand (Acct acct)  = "ACCT " <> acct
serializeCommand (RType rt)   = "TYPE " <> serialzeRTypeCode rt
serializeCommand (Retr file)  = "RETR " <> file
serializeCommand (Nlst [])    = "NLST"
serializeCommand (Nlst args)  = "NLST " <> intercalate " " args
serializeCommand (Port ha pn) = "PORT " <> formatPort ha pn
serializeCommand (Stor loc)   = "STOR " <> loc
serializeCommand (List [])    = "LIST"
serializeCommand (List args)  = "LIST " <> intercalate " " args
serializeCommand (Rnfr from)  = "RNFR " <> from
serializeCommand (Rnto to)    = "RNTO " <> to
serializeCommand (Dele file)  = "DELE " <> file
serializeCommand (Size file)  = "SIZE " <> file
serializeCommand (Mkd dir)    = "MKD " <> dir
serializeCommand (Rmd dir)    = "RMD " <> dir
serializeCommand (Pbsz buf)   = "PBSZ " <> show buf
serializeCommand (Prot P)     = "PROT P"
serializeCommand (Prot C)     = "PROT C"
serializeCommand (Mlsd path)  = "MLSD " <> path
serializeCommand (Mlst path)  = "MLST " <> path
serializeCommand (Cwd dir)    = "CWD " <> dir
serializeCommand Cdup         = "CDUP"
serializeCommand Ccc          = "CCC"
serializeCommand Auth         = "AUTH TLS"
serializeCommand Pwd          = "PWD"
serializeCommand Abor         = "ABOR"
serializeCommand Pasv         = "PASV"
serializeCommand Quit         = "QUIT"

stripCLRF :: ByteString -> ByteString
stripCLRF = C.takeWhile $ (&&) <$> (/= '\r') <*> (/= '\n')

-- | Get a line from the server
getLineResp :: Handle -> IO ByteString
getLineResp h = stripCLRF <$> recvLine h

-- | Get a full response from the server
-- Used in 'sendCommand'
getResponse :: MonadIO m => Handle -> m FTPResponse
getResponse h = do
    line <- liftIO $ getLineResp h
    let (code, rest) = C.splitAt 3 line
    message <- if C.head rest == '-'
        then MultiLine <$> loopMultiLine h code [line]
        else return $ SingleLine line
    let codeDroppedMessage = case message of
            SingleLine message -> SingleLine $ C.drop 4 message
            MultiLine [] -> MultiLine []
            MultiLine (message:messages) ->
                MultiLine ((C.drop 4 message):messages)
    let response = FTPResponse
            (responseStatus code)
            (read $ C.unpack code)
            codeDroppedMessage
    case frStatus response of
        FailureRetry -> liftIO $ throwIO $ FailureRetryException response
        Failure -> liftIO $ throwIO $ FailureException response
        _ -> return response

loopMultiLine
    :: MonadIO m
    => Handle
    -> ByteString
    -> [ByteString]
    -> m [ByteString]
loopMultiLine h code lines = do
    nextLine <- liftIO $ getLineResp h
    let newLines = lines <> [C.dropWhile (== ' ') nextLine]
        nextCode = C.take 3 nextLine
    if nextCode == code
        then return newLines
        else loopMultiLine h code newLines

ensureSuccess :: MonadIO m => FTPResponse -> m FTPResponse
ensureSuccess resp =
    case frStatus resp of
        Success -> return resp
        _ -> liftIO $ throwIO $ UnsuccessfulException resp

getResponseS :: MonadIO m => Handle -> m FTPResponse
getResponseS = ensureSuccess <=< getResponse

sendCommandLine :: MonadIO m => Handle -> ByteString -> m ()
sendCommandLine h = liftIO . send h . (<> "\r\n")

-- | Send a command to the server and get a response back.
-- Some commands use a data 'Handle', and their data is not returned here.
sendCommand :: MonadIO m => Handle -> FTPCommand -> m FTPResponse
sendCommand h fc = do
    let command = serializeCommand fc
    debugPrint $ "Sending: " <> command
    sendCommandLine h $ C.pack command
    resp <- getResponse h
    debugResponse resp
    return resp

sendCommandS :: MonadIO m => Handle -> FTPCommand -> m FTPResponse
sendCommandS h fc = sendCommand h fc >>= ensureSuccess

-- | Equvalent to
--
-- > mapM . sendCommand
sendAll :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse]
sendAll = mapM . sendCommand

-- | Equvalent to
--
-- > mapM . sendCommandS
sendAllS :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse]
sendAllS = mapM . sendCommandS

-- Control connection

createSocket
    :: MonadIO m
    => Maybe String
    -> Int
    -> S.AddrInfo
    -> m (S.Socket, S.AddrInfo)
createSocket host portNum hints = do
    addr:_ <- liftIO $ S.getAddrInfo (Just hints) host (Just $ show portNum)
    debugPrint $ "Addr: " <> show addr
    sock <- liftIO $ S.socket
        (S.addrFamily addr)
        (S.addrSocketType addr)
        (S.addrProtocol addr)
    return (sock, addr)

withSocketPassive
    :: (MonadIO m, MonadMask m)
    => String
    -> Int
    -> (S.Socket -> m a)
    -> m a
withSocketPassive host portNum f = do
    let hints = S.defaultHints {
        S.addrSocketType = S.Stream
    }
    M.bracketOnError
        (createSocket (Just host) portNum hints)
        (liftIO . S.close . fst)
        (\(sock, addr) -> do
            debugPrint $ "Connecting"
            liftIO $ S.connect sock (S.addrAddress addr)
            debugPrint "Connected"
            f sock
        )

withSocketActive :: (MonadIO m, MonadMask m) => (S.Socket -> m a) -> m a
withSocketActive f = do
    let hints = S.defaultHints {
        S.addrSocketType = S.Stream,
        S.addrFlags = [S.AI_PASSIVE]
    }
    M.bracketOnError
        (createSocket Nothing 0 hints)
        (liftIO . S.close . fst)
        (\(sock, addr) -> do
            debugPrint "Binding"
            liftIO $ S.bind sock (S.addrAddress addr)
            liftIO $ S.listen sock 1
            debugPrint "Listening"
            f sock
        )

createSIOHandle :: (MonadIO m, MonadMask m) => String -> Int -> m SIO.Handle
createSIOHandle host portNum = withSocketPassive host portNum
    $ liftIO . flip S.socketToHandle SIO.ReadWriteMode

sIOHandleImpl :: SIO.Handle -> Handle
sIOHandleImpl h = Handle
    { send = C.hPut h
    , sendLine = C.hPutStrLn h
    , recv = C.hGetSome h
    , recvLine = C.hGetLine h
    , security = Clear
    }

withSIOHandle
    :: (MonadIO m, MonadMask m)
    => String
    -> Int
    -> (Handle -> m a)
    -> m a
withSIOHandle host portNum f = M.bracket
    (liftIO $ createSIOHandle host portNum)
    (liftIO . SIO.hClose)
    (f . sIOHandleImpl)

-- | Takes a host name and port. A handle for interacting with the server
-- will be returned in a callback.
--
-- @
-- withFTP "ftp.server.com" 21 $ \h welcome -> do
--     print welcome
--     login h "username" "password"
--     print =<< nlst h []
-- @
withFTP
    :: (MonadIO m, MonadMask m)
    => String
    -> Int
    -> (Handle -> FTPResponse -> m a)
    -> m a
withFTP host portNum f = withSIOHandle host portNum $ \h -> do
    resp <- getResponse h
    f h resp

-- Data connection

withDataSocketPasv
    :: (MonadIO m, MonadMask m)
    => Handle
    -> (S.Socket -> m a)
    -> m a
withDataSocketPasv h f = do
    (host, portNum) <- pasv h
    debugPrint $ "Host: " <> host
    debugPrint $ "Port: " <> show portNum
    withSocketPassive host portNum f

withDataSocketActive
    :: (MonadIO m, MonadMask m)
    => Handle
    -> (S.Socket -> m a)
    -> m a
withDataSocketActive h f = withSocketActive $ \socket -> do
    (S.SockAddrInet sPort sHost) <- liftIO $ S.getSocketName socket
    port h sHost sPort
    f socket

-- | Open a socket that can be used for data transfers
withDataSocket
    :: (MonadIO m, MonadMask m)
    => PortActivity
    -> Handle
    -> (S.Socket -> m a)
    -> m a
withDataSocket Active  = withDataSocketActive
withDataSocket Passive = withDataSocketPasv

acceptData :: MonadIO m => PortActivity -> S.Socket -> m S.Socket
acceptData Passive = return
acceptData Active = return . fst <=< liftIO . S.accept

-- Response to data commands should be 150 but apparently
-- some servers will respond with 200 before 150 so just ignore it
ensureSucessfulData :: MonadIO m => Handle -> FTPResponse -> m ()
ensureSucessfulData h resp = do
    resp' <- case frStatus resp of
        Success -> do
            newResp <- getResponse h
            debugResponse newResp
            return newResp
        _ -> return resp
    liftIO $ when (frStatus resp' /= Wait)
        $ throwIO $ UnsuccessfulException resp

-- | Send setup commands to the server and
-- create a data 'System.IO.Handle'
createSendDataCommand
    :: (MonadIO m, MonadMask m)
    => Handle
    -> PortActivity
    -> FTPCommand
    -> m (SIO.Handle)
createSendDataCommand h pa cmd = withDataSocket pa h $ \socket -> do
    resp <- sendCommand h cmd
    ensureSucessfulData h resp
    acceptedSock <- acceptData pa socket
    liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode

-- | Provides a data 'Handle' in a callback for a command
withDataCommand
    :: (MonadIO m, MonadMask m)
    => Handle
    -> PortActivity
    -> RTypeCode
    -> FTPCommand
    -> (Handle -> m a)
    -> m a
withDataCommand ch pa code cmd f = do
    sendCommandS ch $ RType code
    x <- M.bracket
        (createSendDataCommand ch pa cmd)
        (liftIO . SIO.hClose)
        (f . sIOHandleImpl)
    resp <- getResponse ch
    debugResponse resp
    return x

-- | Recieve data and interpret it linewise
getAllLineResp :: (MonadIO m, MonadCatch m) => Handle -> m ByteString
getAllLineResp h = getAllLineResp' h []
    where
        getAllLineResp' h ret = ( do
            line <- liftIO $ getLineResp h
            getAllLineResp' h (ret <> [line]))
                `M.catchIOError` (\_ -> return $ C.intercalate "\n" ret)

-- | Recieve all data and return it as a 'Data.ByteString.ByteString'
recvAll :: (MonadIO m, MonadCatch m) => Handle -> m ByteString
recvAll h = recvAll' ""
    where
        recvAll' bs = ( do
            chunk <- liftIO $ recv h defaultChunkSize
            recvAll' $ bs <> chunk)
                `M.catchIOError` (\_ -> return bs)

-- TLS connection

connectTLS :: MonadIO m => SIO.Handle -> String -> Int -> m Connection
connectTLS h host portNum = do
    context <- liftIO initConnectionContext
    let tlsSettings = TLSSettingsSimple
            { settingDisableCertificateValidation = True
            , settingDisableSession = False
            , settingUseServerName = False
            }
        connectionParams = ConnectionParams
            { connectionHostname = host
            , connectionPort = toEnum . fromEnum $ portNum
            , connectionUseSecure = Just tlsSettings
            , connectionUseSocks = Nothing
            }
    liftIO $ connectFromHandle context h connectionParams

createTLSConnection
    :: (MonadIO m, MonadMask m)
    => String
    -> Int
    -> m (FTPResponse, Connection)
createTLSConnection host portNum = do
    h <- createSIOHandle host portNum
    let insecureH = sIOHandleImpl h
    resp <- getResponse insecureH
    sendCommand insecureH Auth
    conn <- connectTLS h host portNum
    return (resp, conn)

tlsHandleImpl :: Connection -> Handle
tlsHandleImpl c = Handle
    { send = connectionPut c
    , sendLine = connectionPut c . (<> "\n")
    , recv = connectionGet c
    , recvLine = connectionGetLine maxBound c
    , security = TLS
    }

withTLSHandle
    :: (MonadMask m, MonadIO m)
    => String
    -> Int
    -> (Handle -> FTPResponse -> m a)
    -> m a
withTLSHandle host portNum f = M.bracket
    (createTLSConnection host portNum)
    (liftIO . connectionClose . snd)
    (\(resp, conn) -> f (tlsHandleImpl conn) resp)

-- | Takes a host name and port. A handle for interacting with the server
-- will be returned in a callback. The commands will be protected with TLS.
--
-- @
-- withFTPS "ftps.server.com" 21 $ \h welcome -> do
--     print welcome
--     login h "username" "password"
--     print =<< nlst h []
-- @
withFTPS
    :: (MonadMask m, MonadIO m)
    => String
    -> Int
    -> (Handle -> FTPResponse -> m a)
    -> m a
withFTPS host portNum = withTLSHandle host portNum

-- TLS data connection

-- | Send setup commands to the server and
-- create a data TLS connection
createTLSSendDataCommand
    :: (MonadIO m, MonadMask m)
    => Handle
    -> PortActivity
    -> FTPCommand
    -> m Connection
createTLSSendDataCommand ch pa cmd = do
    sendAllS ch [Pbsz 0, Prot P]
    withDataSocket pa ch $ \socket -> do
        resp <- sendCommand ch cmd
        ensureSucessfulData ch resp
        acceptedSock <- acceptData pa socket
        (S.SockAddrInet sPort sHost) <- liftIO $ S.getSocketName acceptedSock
        let (h1, h2, h3, h4) = S.hostAddressToTuple sHost
            hostName = intercalate "." $ (show . fromEnum) <$> [h1, h2, h3, h4]
        h <- liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode
        liftIO $ connectTLS h hostName (fromEnum sPort)

withTLSDataCommand
    :: (MonadIO m, MonadMask m)
    => Handle
    -> PortActivity
    -> RTypeCode
    -> FTPCommand
    -> (Handle -> m a)
    -> m a
withTLSDataCommand ch pa code cmd f = do
    sendCommandS ch $ RType code
    x <- M.bracket
        (createTLSSendDataCommand ch pa cmd)
        (liftIO . connectionClose)
        (f . tlsHandleImpl)
    resp <- getResponse ch
    debugPrint $ "Recieved: " <> (show resp)
    return x

parseResponse :: MonadIO m => FTPResponse -> Parser a -> m a
parseResponse resp p =
    let parsableMessage = case frMessage resp of
            SingleLine message -> message
            MultiLine messages -> C.intercalate "\n" messages
    in case parseOnly p parsableMessage of
        Right x -> return x
        Left _ -> liftIO $ throwIO
            $ BadProtocolResponseException parsableMessage

ensureCode :: MonadIO m => FTPResponse -> Int -> m ()
ensureCode resp code =
    liftIO $ when (frCode resp /= code)
        $ liftIO $ throwIO $ UnsuccessfulException resp

parse227 :: Parser (String, Int)
parse227 = do
    skipWhile (/= '(') *> char '('
    [h1,h2,h3,h4,p1,p2] <- many1 digit `sepBy` char ','
    let host = intercalate "." [h1,h2,h3,h4]
        highBits = read p1
        lowBits = read p2
        portNum = (highBits `shift` 8) + lowBits
    return (host, portNum)

parse257 :: Parser String
parse257 = do
    char '"'
    C.unpack <$> takeTill (== '"')

-- Control commands

login :: MonadIO m => Handle -> String -> String -> m FTPResponse
login h user pass = do
    resp <- last <$> sendAll h [User user, Pass pass]
    ensureSuccess resp

pasv :: MonadIO m => Handle -> m (String, Int)
pasv h = do
    resp <- sendCommandS h Pasv
    ensureCode resp 227
    parseResponse resp parse227

port :: MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse
port h ha pn = sendCommandS h (Port ha pn)

acct :: MonadIO m => Handle -> String -> m FTPResponse
acct h pass = sendCommandS h (Acct pass)

rename :: MonadIO m => Handle -> String -> String -> m FTPResponse
rename h from to = do
    res <- sendCommand h (Rnfr from)
    case frStatus res of
        Continue -> sendCommandS h (Rnto to)
        _ -> return res

dele :: MonadIO m => Handle -> String -> m FTPResponse
dele h file = sendCommandS h (Dele file)

cwd :: MonadIO m => Handle -> String -> m FTPResponse
cwd h dir =
    sendCommandS h $ if dir == ".."
        then Cdup
        else Cwd dir

size :: MonadIO m => Handle -> String -> m Int
size h file = do
    resp <- sendCommandS h (Size file)
    ensureCode resp 213
    return $ case frMessage resp of
        SingleLine message -> read $ C.unpack $ message
        MultiLine _ -> 0

mkd :: MonadIO m => Handle -> String -> m String
mkd h dir = do
    resp <- sendCommandS h (Mkd dir)
    ensureCode resp 257
    parseResponse resp parse257

rmd :: MonadIO m => Handle -> String -> m FTPResponse
rmd h dir = sendCommandS h (Rmd dir)

pwd :: MonadIO m => Handle -> m String
pwd h = do
    resp <- sendCommandS h Pwd
    ensureCode resp 257
    parseResponse resp parse257

quit :: MonadIO m => Handle -> m FTPResponse
quit h = sendCommandS h Quit

mlst :: (MonadIO m, MonadMask m) => Handle -> String -> m MlsxResponse
mlst h path = do
    resp <- sendCommandS h (Mlst path)
    case frMessage resp of
        SingleLine message -> return $ parseMlsxLine message
        MultiLine messages -> if length messages >= 2
            then return $ parseMlsxLine $ messages !! 1
            else liftIO $ throwIO $ BogusResponseFormatException resp

-- TLS commands

pbsz :: MonadIO m => Handle -> Int -> m FTPResponse
pbsz h = sendCommandS h . Pbsz

prot :: MonadIO m => Handle -> ProtType -> m FTPResponse
prot h = sendCommandS h . Prot

ccc :: MonadIO m => Handle -> m FTPResponse
ccc h = sendCommandS h Ccc

auth :: MonadIO m => Handle -> m FTPResponse
auth h = sendCommandS h Auth

-- Data commands

sendType :: MonadIO m => RTypeCode -> ByteString -> Handle -> m ()
sendType TA dat h = void $ mapM (sendCommandLine h) $ C.split '\n' dat
sendType TI dat h = liftIO $ send h dat

withDataCommandSecurity
    :: (MonadIO m, MonadMask m)
    => Handle
    -> PortActivity
    -> RTypeCode
    -> FTPCommand
    -> (Handle -> m a)
    -> m a
withDataCommandSecurity h =
    case security h of
        Clear -> withDataCommand h
        TLS -> withTLSDataCommand h

nlst :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString
nlst h args = withDataCommandSecurity h Passive TA (Nlst args) getAllLineResp

retr :: (MonadIO m, MonadMask m) => Handle -> String -> m ByteString
retr h path = withDataCommandSecurity h Passive TI (Retr path) recvAll

list :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString
list h args = withDataCommandSecurity h Passive TA (List args) getAllLineResp

stor
    :: (MonadIO m, MonadMask m)
    => Handle
    -> String
    -> B.ByteString
    -> RTypeCode
    -> m ()
stor h loc dat rtype =
    withDataCommandSecurity h Passive rtype (Stor loc) $ sendType rtype dat

data MlsxResponse = MlsxResponse {
    mrFilename :: String,
    mrFacts :: Map String String
} deriving (Show)

splitApart :: Char -> ByteString -> (ByteString, ByteString)
splitApart on s =
    let (x0, x1) = C.break (== on) s
    in (x0, C.drop 1 x1)

parseMlsxLine :: ByteString -> MlsxResponse
parseMlsxLine line =
    let (factLine, filename) = splitApart ' ' line
        bFacts = splitApart '=' <$> C.split ';' factLine
        facts
            = Map.fromList
            $ filter (not . null . fst)
            $ join (***) C.unpack <$> bFacts
    in MlsxResponse (C.unpack filename) facts

getMlsxResponse :: (MonadIO m, MonadCatch m) => Handle -> m [MlsxResponse]
getMlsxResponse h = getMlsxResponse' h []
    where
        getMlsxResponse' h ret = ( do
            line <- liftIO $ getLineResp h
            getMlsxResponse' h $
                if C.null line
                    then ret
                    else (parseMlsxLine line):ret
            ) `M.catchIOError` (\_ -> return ret)

mlsd :: (MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse]
mlsd h path = withDataCommandSecurity h Passive TA (Mlsd path) getMlsxResponse