packages feed

haskell-ftp-0.1.0.0: Network/FTP/Monad.hs

{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, MultiParamTypeClasses, FlexibleContexts, TypeFamilies #-}
module Network.FTP.Monad where

import qualified Prelude as P
import BasicPrelude
import Filesystem.Path.CurrentOS (parent)

import qualified Data.ByteString.Char8 as S
import qualified Data.CaseInsensitive as CI
import Data.Conduit

import Control.Monad.Trans.State (StateT, runStateT, gets, modify)
import Control.Monad.Base (MonadBase(..))
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Trans.Control (MonadBaseControl(..))

import Network.Socket (Socket, SockAddr)
import Network.FTP.Backend (UserId)

data DataChannel = NoChannel
                 | PasvChannel Socket
                 | PortChannel SockAddr
    deriving (Show)

data DataType = ASCII | Binary
    deriving (Show)

data FTPState m = FTPState
  { ftpSource   :: ResumableSource m ByteString -- ^ source for reading request
  , ftpSink     :: Sink ByteString m ()         -- ^ sink for sending respose
  , ftpRemote   :: SockAddr                     -- ^ client address
  , ftpLocal    :: SockAddr                     -- ^ local address
  , ftpChannel  :: DataChannel                  -- ^ ftp data channel
  , ftpDataType :: DataType                     -- ^ ftp data type
  , ftpRename   :: Maybe FilePath               -- ^ store from name during renaming.
  , ftpDir      :: FilePath                     -- ^ the ftp virtual directory.
  , ftpUser     :: Maybe (UserId m)             -- ^ current authenticated user.
  }

defaultFTPState :: ResumableSource m ByteString
                -> Sink ByteString m ()
                -> SockAddr
                -> SockAddr
                -> FTPState m
defaultFTPState src snk remote local =
    FTPState src
             snk
             remote
             local
             NoChannel
             ASCII
             Nothing
             "/"
             Nothing

{-|
 - FTP is a monad transformer that only handles only ftp protocol, and leaves authentication and filesystem details to underlying monad.
 -}
newtype FTP m a = FTP { unFTP :: StateT (FTPState m) m a }
    deriving (Functor, Applicative, Monad, MonadIO)

instance MonadTrans FTP where
    lift m = FTP (lift m)

instance MonadBase IO m => MonadBase IO (FTP m) where
    liftBase = FTP . liftBase

instance MonadBaseControl IO m => MonadBaseControl IO (FTP m) where
    newtype StM (FTP m) a = FTPStM { unFTPStM :: StM (StateT (FTPState m) m) a }
    liftBaseWith f = FTP . liftBaseWith $ \runInBase -> f $ liftM FTPStM . runInBase . unFTP
    restoreM = FTP . restoreM . unFTPStM

runFTP :: FTPState m -> FTP m a -> m (a, FTPState m)
runFTP s m = runStateT (unFTP m) s

{-|
 - Consume request source, store the modified ResumableSource into state monad.
 -}
consumeSource :: Monad m
              => (ResumableSource m ByteString -> FTP m (ResumableSource m ByteString, a))
              -> FTP m a
consumeSource f = do
    src <- FTP (gets ftpSource)
    (src', r) <- f src
    FTP $ modify $ \st -> st{ ftpSource = src' }
    return r

{-|
 - Run a `Sink` to parse request.
 - e.g. `wait getCommand :: FTP m (ByteString, ByteString)`
 -}
wait :: Monad m => Sink ByteString m b -> FTP m b
wait snk = consumeSource $ \src -> lift (src $$++ snk)

{-|
 - Get a line, fail when connection closed. The stream is assumed to be individual lines produced by upstream conduit.
 -}
headOrFail :: Monad m => GSink a m a
headOrFail = await >>= maybe (fail "connection closed") return

{-|
 - Parse a request command.
 -}
getCommand :: Monad m => GSink ByteString m (CI.CI ByteString, ByteString)
getCommand = do
    s' <- headOrFail
    let (cmd', arg) = S.span (\c -> c/=' ' && c/='\r') s'
        arg' = if S.length arg < 2
                 then S.empty
                 else S.tail (S.init arg) -- strip heading ' ' and trailing '\r'
    return (CI.mk cmd', arg')

{-|
 - Parse a command, if it's not the expected one, then fail.
 -}
expect :: (Monad m) => CI.CI ByteString -> GSink ByteString m ByteString
expect s = do
    (cmd, arg) <- getCommand
    if s==cmd
      then return arg
      else fail $ concat ["expect ", P.show s, " but got ", P.show cmd, " ."]

{-|
 - Send raw message to client
 -}
send :: Monad m => ByteString -> FTP m ()
send s = do
    snk <- FTP $ gets ftpSink
    lift (yield s $$ snk)

{-|
 - Send reply code and message to client, support multiline message.
 -}
reply :: Monad m => ByteString -> ByteString -> FTP m ()
reply code msg =
    send . S.concat $ build code (S.lines msg) ++ ["\r\n"]
  where
    build c []     = [c, "  "]
    build c [s]    = [c, " ", s]
    build c (s:ss) = [c, "-", s] ++ build c ss

{-|
 - Change working directory
 -}
cwd :: Monad m => FilePath -> FTP m ()
cwd ".." = FTP (modify $ \st -> st{ftpDir = parent (ftpDir st)})
cwd d    = FTP (modify $ \st -> st{ftpDir = ftpDir st </> d})

{-|
 - Print working directory
 -}
pwd :: Monad m => FTP m FilePath
pwd = FTP (gets ftpDir)

{-|
 - Make absolute path.
 -}
ftpAbsolute :: Monad m => FilePath -> FTP m FilePath
ftpAbsolute path = do
    dir <- FTP (gets ftpDir)
    return (dir </> path)