FTPLine-1.4.0.0: Main.hs
{-# LANGUAGE FlexibleInstances #-}
module Main (main) where
import Data.Maybe
import qualified Data.Strict.Maybe as Strict
import Control.Monad.State.Strict
import System.Console.Haskeline hiding (getInputLine)
import qualified System.Console.Haskeline as Haskeline
(getInputLine)
import Network.Socket hiding (connect)
import Network.FTP.Client
import Network.FTP.Client.Parser
import System.Directory
import Control.Applicative ((<$>))
import Data.List (intercalate)
import System.Console.ANSI
import System.IO
import System.Environment (getArgs)
import qualified Data.ByteString.Char8 as B
import Paths_FTPLine (version)
import Data.Version (showVersion)
ftperror :: String -> FTPLine a
ftperror = liftIO . fail
getInputLine :: String -> FTPLine String
getInputLine str
= do withColor Yellow $ outputStr str
ms <- Haskeline.getInputLine "\n"
if ms == Nothing then
do withColor Green $ outputStrLn "Switching local/remote..."
return "switch"
else return $ fromJust ms
getInputLine_ :: String -> String -> FTPLine ()
getInputLine_ str com
= do withColor Yellow $ outputStr str
outputStrLn com
data File = File FilePath !B.ByteString
type SMaybe = Strict.Maybe
data FTPState = FTPState{conn :: Maybe FTPConnection,
file :: !(SMaybe File), host :: Maybe String, mode :: Bool,
interactive :: Bool}
boolToMode :: Bool -> String
boolToMode b = if b then "Remote" else "Local"
initialFTPState :: FTPState
initialFTPState
= FTPState{conn = Nothing, file = Strict.Nothing, host = Nothing,
mode = False, interactive = False}
type FTPLine a = InputT (StateT FTPState IO) a
getting ::
(MonadState st m, MonadTrans t, Functor (t m)) =>
(st -> x) -> t m x
getting f = f <$> lift get
modif :: (MonadTrans t, MonadState s m) => (s -> s) -> t m ()
modif = lift . modify
getConnection :: FTPLine (Maybe FTPConnection)
getConnection = getting conn
newConnection :: String -> FTPConnection -> FTPLine ()
newConnection hn c
= modif $
\ (FTPState _ mf _ b i) -> FTPState (Just c) mf (Just hn) b i
removeConnection :: FTPLine ()
removeConnection
= modif $
\ (FTPState _ mf _ b i) -> FTPState Nothing mf Nothing b i
getFile :: FTPLine (SMaybe File)
getFile = getting file
newFile :: File -> FTPLine ()
newFile f
= modif $!
\ (FTPState mc _ md b i) -> FTPState mc (Strict.Just f) md b i
cleanFile :: FTPLine ()
cleanFile
= modif $!
\ (FTPState mc _ md b i) -> FTPState mc Strict.Nothing md b i
getHost :: FTPLine String
getHost
= return "Local" <-> const (getting $ maybe "Local" id . host)
getMode :: FTPLine Bool
getMode = getting mode
turnMode :: FTPLine ()
turnMode = modif $ \ s -> s{mode = not $ mode s}
isInteractive :: FTPLine Bool
isInteractive = getting interactive
setInteractive :: Bool -> FTPLine ()
setInteractive b = modif $ \ s -> s{interactive = b}
connect :: HostName -> PortNumber -> FTPLine FTPResult
connect hn pn
= do mc <- getConnection
if isNothing mc then return () else
(liftIO $ quit $ fromJust mc) >> return ()
(c, r) <- liftIO $ connectFTP hn pn
newConnection hn c
saveConnection hn pn
return r
saveConnection :: HostName -> PortNumber -> FTPLine ()
saveConnection hn pn
= do appDir <- liftIO ftpLineDir
liftIO $ writeFile (appDir ++ "/LastConnection") $ show (hn, pn)
withConnection :: (FTPConnection -> FTPLine a) -> FTPLine a
withConnection f
= do mc <- getConnection
if isNothing mc then ftperror "Connection not established." else
f $ fromJust mc
saveLogin :: String -> String -> FTPLine ()
saveLogin name pass
= do appDir <- liftIO ftpLineDir
liftIO $ writeFile (appDir ++ "/LastLogin") $ show (name, pass)
ftplogin :: Maybe (String, String) -> FTPLine FTPResult
ftplogin Nothing = withConnection $ liftIO . loginAnon
ftplogin (Just (name, pass))
= do r <- withConnection $
\ c -> liftIO $ login c name (Just pass) Nothing
saveLogin name pass
return r
lastConn :: FTPLine ()
lastConn
= hand
(const $
withColor Red $ outputStrLn "Last connection doesn't exist.")
$
do appDir <- liftIO ftpLineDir
lc <- liftIO $ readFile $ appDir ++ "/LastConnection"
_ <- (liftIO $ readIO lc) >>= uncurry connect
outputStrLn "Last connection reestablished."
hand
(const $ withColor Red $ outputStrLn "Last login doesn't exist.")
$
do ll <- liftIO $ readFile $ appDir ++ "/LastLogin"
_ <- (liftIO $ readIO ll) >>= ftplogin . Just
outputStrLn "Last login reestablished."
disconnect :: FTPLine FTPResult
disconnect
= withConnection $
\ c ->
do removeConnection
liftIO $ quit c
ftpdebug :: FTPLine ()
ftpdebug = liftIO enableFTPDebugging
success :: FTPResult
success = (1, ["Success"])
failure :: FTPResult
failure = (0, ["Failure"])
showFTPResult :: FTPResult -> String
showFTPResult (n, xs) = unwords $ [show n, ":"] ++ xs
outputFTPResult :: FTPResult -> FTPLine ()
outputFTPResult = withColor Magenta . outputStrLn . showFTPResult
discardConnection :: FTPLine Bool
discardConnection
= do mc <- getConnection
b <- getMode
return $ not b || isNothing mc
(<->) :: FTPLine a -> (FTPConnection -> FTPLine a) -> FTPLine a
la <-> ra
= do b <- discardConnection
if b then la else
do mc <- getConnection
ra $ fromJust mc
localdir :: Maybe String -> FTPLine ()
localdir fp
= if isNothing fp then
liftIO (getDirectoryContents ".") >>= mapM_ outputStrLn . reverse
else
liftIO (getDirectoryContents $ fromJust fp) >>= mapM_ outputStrLn
pdir :: Maybe String -> FTPLine ()
pdir fp
= localdir fp <-> \ c -> liftIO (dir c fp) >>= mapM_ outputStrLn
getfile :: String -> FTPLine FTPResult
getfile fp
= (do x <- liftIO $ B.readFile fp
newFile $ File fp x
return success)
<->
\ c ->
do (x, r) <- liftIO $ getbinary c fp
newFile $ File fp $ B.pack x
return r
download :: String -> FTPLine FTPResult
download fp = withConnection $ \ c -> liftIO $ downloadbinary c fp
putfile :: String -> FTPLine FTPResult
putfile fp
= (do x <- getFile
if Strict.isNothing x then ftperror "File memory empty." else
let (File _ cnt) = Strict.fromJust x in
do liftIO $ B.writeFile fp cnt
return success)
<->
\ c ->
do x <- getFile
if Strict.isNothing x then ftperror "File memory empty." else
let (File _ cnt) = Strict.fromJust x in
liftIO $ putbinary c fp $ B.unpack cnt
upload :: String -> FTPLine FTPResult
upload fp = withConnection $ \ c -> liftIO $ uploadbinary c fp
renamefile :: String -> String -> FTPLine FTPResult
renamefile fp1 fp2
= (do liftIO $ renameFile fp1 fp2
return success)
<-> \ c -> liftIO $ rename c fp1 fp2
deletefile :: String -> FTPLine FTPResult
deletefile fp
= (do liftIO $ removeFile fp
return success)
<-> \ c -> liftIO $ delete c fp
sizefile :: String -> FTPLine FTPResult
sizefile fp
= do n <- (liftIO $ B.length <$> B.readFile fp) <->
\ c -> liftIO $ size c fp
outputStrLn $ show n
return success
ccd :: String -> FTPLine FTPResult
ccd fp
= (do liftIO $ setCurrentDirectory fp
return success)
<-> \ c -> liftIO $ cwd c fp
newdir :: String -> FTPLine FTPResult
newdir fp
= (do liftIO $ createDirectory fp
return success)
<-> \ c -> liftIO $ snd <$> mkdir c fp
remdir :: String -> FTPLine FTPResult
remdir fp
= (do liftIO $ removeDirectory fp
return success)
<-> \ c -> liftIO $ rmdir c fp
curdir :: FTPLine (Maybe String, FTPResult)
curdir
= (do x <- liftIO getCurrentDirectory
return (Just x, success))
<-> (liftIO . pwd)
class (Read a) => Arg a where
parse :: String -> FTPLine a
parse = liftIO . readIO
output :: (Show a) => a -> FTPLine ()
output = outputStrLn . show
command :: String -> FTPLine ()
command str
= if null xs then outputStrLn "No command introduced." else
withColor Green $ runCom (head xs) (tail xs)
where xs = separgs str
printHelp :: String -> FTPLine ()
printHelp str
= withColor Cyan $
mapM_
(\ ln ->
if head ln == '*' then outputStrLn ln else
italized $ outputStrLn ln)
$ commandDesc str
hand :: (MonadException m) => (IOException -> m a) -> m a -> m a
hand = handle
handres :: FTPLine FTPResult -> FTPLine ()
handres c
= hand ((>> return failure) . withColor Red . output) c >>=
outputFTPResult
hand_ :: FTPLine () -> FTPLine ()
hand_ = hand (withColor Red . output)
inEchoOff, inEchoOn :: FTPLine ()
inEchoOff = liftIO $ hSetEcho stdin False
inEchoOn = liftIO $ hSetEcho stdin True
runCom :: String -> [String] -> FTPLine ()
runCom str ["help"] = printHelp str
runCom "help" _
= do withColor Cyan $ outputStrLn helpHead
mapM_ printHelp commands
runCom "connect" args = handres $ mkCom args $ uncurry connect
runCom "login" args
= do isInt <- isInteractive
if isInt then
do let isAnon
= do withColor Yellow $ outputStr "Login as anonymous? (y/n) "
c <- getInputChar ""
case c of
Just 'y' -> return True
Just 'n' -> return False
_ -> do liftIO $
do cursorUpLine 1
clearFromCursorToScreenEnd
isAnon
b <- isAnon
if b then handres $ withColor Green $ ftplogin Nothing else
do withColor Yellow $ outputStr "User name: "
name <- Haskeline.getInputLine ""
withColor Yellow $ outputStr "Password : "
pass <- getPassword (Just '*') ""
if isNothing name || isNothing pass then
outputStrLn "Incorrect login." else
handres $
withColor Green $ ftplogin $ Just (fromJust name, fromJust pass)
else
if null args then handres $ ftplogin Nothing else
handres $ mkCom args $ ftplogin . Just
runCom "disconnect" _ = handres disconnect
runCom "ftpdebug" _ = ftpdebug
runCom "dir" args
= if null args then hand_ $ pdir Nothing else
hand_ $ mkCom args $ pdir . Just
runCom "getfile" args = handres $ mkCom args $ getfile
runCom "download" args = handres $ mkCom args $ download
runCom "putfile" args = handres $ mkCom args $ putfile
runCom "upload" args = handres $ mkCom args $ upload
runCom "rename" args = handres $ mkCom args $ uncurry renamefile
runCom "delete" args = handres $ mkCom args $ deletefile
runCom "size" args = handres $ mkCom args $ sizefile
runCom "cd" args = handres $ mkCom args $ ccd
runCom "md" args = handres $ mkCom args $ newdir
runCom "rd" args = handres $ mkCom args $ remdir
runCom "clear" _
= do hand_ cleanFile
outputStrLn "Memory Cleared."
runCom "pause" _
= do outputStr "PAUSE. Press ENTER to continue..."
inEchoOff
_ <- liftIO $ getLine
inEchoOn
outputStrLn ""
runCom "switch" _
= do turnMode
b <- getMode
outputStrLn $ "Current mode: " ++ boolToMode b
runCom "last" _ = lastConn
runCom x _
= withColor Cyan $ outputStrLn $ "Command doesn't exist: " ++ x
commands :: [String]
commands
= ["connect", "disconnect", "login", "ftpdebug", "last", "dir",
"cd", "md", "rd", "getfile", "putfile", "clear", "download",
"upload", "rename", "delete", "size", "switch", "pause", "exit"]
helpHead :: String
helpHead
= unlines
["", "Below is a list of available commands.",
"Arguments in brackets are optional.",
"Arguments in braces are only used in batch mode."]
commandDesc :: String -> [String]
commandDesc "connect"
= ["connect HostName Port", "* Connect to remote FTP server."]
commandDesc "login"
= ["login {[UserName Password]}", "* Login to the FTP server."]
commandDesc "disconnect"
= ["disconnect", "* Close the current conection."]
commandDesc "ftpdebug"
= ["ftpdebug", "* Enable logging of FTP messages."]
commandDesc "dir"
= ["dir [FilePath]", "* Show a directory content."]
commandDesc "getfile"
= ["getfile FilePath",
"* Load specified file and save its content in a temporal memory."]
commandDesc "download"
= ["download FilePath", "* Download a remote file to your system."]
commandDesc "putfile"
= ["putfile FilePath",
"* Write a file with the temporal memory's content."]
commandDesc "upload"
= ["upload FilePath", "* Upload a file from disk."]
commandDesc "rename"
= ["rename FilePath1 FilePath2", "* Rename a file."]
commandDesc "delete" = ["delete FilePath", "* Delete a file."]
commandDesc "size"
= ["size FilePath", "* Return the size of a file."]
commandDesc "cd"
= ["cd FilePath", "* Change the current directory."]
commandDesc "md" = ["md FilePath", "* Create a new directory."]
commandDesc "rd" = ["rd FilePath", "* Remove a directory"]
commandDesc "exit" = ["exit", "* Close the client."]
commandDesc "clear" = ["clear", "* Clear the temporal memory."]
commandDesc "pause"
= ["pause", "* Stop the program until pressing ENTER.",
"* Useful for FTP batch programs."]
commandDesc "switch"
= ["switch", "* Switch between local and remote mode.",
"* CTRL+D invokes this action."]
commandDesc "last"
= ["last",
"* Try to connect and login as it was done the last time."]
commandDesc _ = ["* Invalid command."]
mkCom :: (Arg a) => [String] -> (a -> FTPLine b) -> FTPLine b
mkCom [] _ = ftperror "Need more arguments."
mkCom args f
= if length args > 1 then
parse (concat ["(", intercalate "," args, ")"]) >>= f else
parse (head args) >>= f
instance Arg [Char] where
parse = return
instance Read PortNumber where
readsPrec n str = (\(x,r) -> ((fromIntegral :: Int -> PortNumber) x , r)) <$> readsPrec n str
safeTail :: [a] -> [a]
safeTail [] = []
safeTail (_:xs) = xs
instance (Arg a, Arg b) => Arg (a, b) where
parse str
= let str' = init . tail $ str
xs = takeWhile (/= ',') str'
ys = safeTail $ dropWhile (/= ',') str'
in
do x <- parse xs
y <- parse ys
return (x, y)
instance Arg PortNumber where
parse str = liftIO $ readIO str
addChar :: Char -> [String] -> [String]
addChar c [] = [[c]]
addChar c (x : xs) = (c : x) : xs
args' :: String -> [String] -> Bool -> [String]
args' [] xs _ = xs
args' (x : xs) ys b
| x == ' ' =
if b then args' xs (addChar x ys) b else args' xs ([] : ys) b
| x == '"' = args' xs ys $ not b
| otherwise = args' xs (addChar x ys) b
separgs :: String -> [String]
separgs str = reverse $ reverse <$> args' str [] False
withSGR :: (MonadIO m) => [SGR] -> [SGR] -> m a -> m a
withSGR xs ys comp
= do liftIO $ setSGR xs
x <- comp
liftIO $ setSGR ys
return x
withColor :: (MonadIO m) => Color -> m a -> m a
withColor col
= withSGR [SetColor Foreground Vivid col]
[SetColor Foreground Vivid White]
italized :: (MonadIO m) => m a -> m a
italized
= withSGR [SetSwapForegroundBackground True]
[SetSwapForegroundBackground False]
ftpLineDir :: IO FilePath
ftpLineDir = getAppUserDataDirectory "FTPLine"
main :: IO ()
main
= do setSGR [SetColor Foreground Vivid White]
args <- getArgs
appDir <- ftpLineDir
createDirectoryIfMissing True appDir
putStrLn $ "*** Welcome to FTPLine " ++ showVersion version ++ " ***"
if null args then
withColor Cyan $
do putStr "Type "
italized $ putStr "help"
putStrLn " for a list of commands."
let sets
= defaultSettings{historyFile = Just $ appDir ++ "/History"}
evalStateT (runInputT sets mainInteractive) initialFTPState
else
do let arg = head args
exst <- doesFileExist arg
if exst then
do txt <- readFile arg
evalStateT
(runInputT defaultSettings $
mapM_
(\ str ->
do (mcd, hn) <- withColor Green $
do mcd <- hand
((>> (return $ Just "?")) .
withColor Red . output)
$ fst <$> curdir
hn <- getHost
return (mcd, hn)
let cd = if isNothing mcd then "? " else fromJust mcd
getInputLine_ (unwords [cd, "@", hn ++ ">>\n"]) str
hand_ $ command str)
$ lines txt ++ ["disconnect"])
initialFTPState
putStrLn "Closing FTPLine."
else withColor Red $ putStrLn $ "File " ++ arg ++ " doesn't exist."
setSGR [Reset]
mainInteractive :: FTPLine ()
mainInteractive
= do setInteractive True
runCicle
runCicle :: FTPLine ()
runCicle
= hand
(\ e ->
do withColor Red $ output e
ln <- getInputLine "? >>"
if ln == "exit" then exit else
do hand_ $ command ln
runCicle)
mainCicle
mainCicle :: FTPLine ()
mainCicle
= do (mcd, hn) <- withColor Green $
do mcd <- hand
(\ e ->
do withColor Red $ output e
return Nothing)
$ fst <$> curdir
hn <- getHost
return (mcd, hn)
let cd = if isNothing mcd then "? " else fromJust mcd
ln <- getInputLine $ unwords [cd, "@", hn ++ ">>"]
if ln == "exit" then exit else
do hand_ $ command ln
mainCicle
exit :: FTPLine ()
exit
= do _ <- hand (\ _ -> return failure) $ withColor Green disconnect
outputStrLn "Closing FTPLine."