sync-mht-0.2.0.0: src/main/hs/Main.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import System.Environment
import System.Process
import System.IO
import System.IO.Error
import System.Console.GetOpt
import Paths_sync_mht (version)
import Data.List
import Data.Version (showVersion)
import Sync.MerkleTree.CommTypes
import Sync.MerkleTree.Sync
data SyncOptions
= SyncOptions
{ so_source :: Maybe FilePath
, so_destination :: Maybe FilePath
, so_remote :: Maybe String
, so_ignore :: [FilePath]
, so_add :: Bool
, so_update :: Bool
, so_delete :: Bool
, so_help :: Bool
, so_nonOptions :: [String]
}
defaultSyncOptions :: SyncOptions
defaultSyncOptions =
SyncOptions
{ so_source = Nothing
, so_destination = Nothing
, so_remote = Nothing
, so_ignore = []
, so_add = False
, so_update = False
, so_delete = False
, so_help = False
, so_nonOptions = []
}
toClientServerOptions :: SyncOptions -> ClientServerOptions
toClientServerOptions so =
ClientServerOptions
{ cs_add = so_add so
, cs_update = so_update so
, cs_delete = so_delete so
, cs_ignore = so_ignore so
}
optDescriptions :: [OptDescr (SyncOptions -> SyncOptions)]
optDescriptions =
[ Option ['s'] ["source"] (ReqArg (\fp so -> so { so_source = Just fp }) "DIR")
"directory to copy files from (source) (this option is required)"
, Option ['d'] ["destination"] (ReqArg (\fp so -> so { so_destination = Just fp }) "DIR")
"directory to copy files to (destination) (this option is required)"
, Option ['r'] ["remote-shell"] (ReqArg (\s so -> so { so_remote = Just s }) "CMD")
"synchroize with a remote-site using a remote command execution tool (like ssh or docker)"
, Option ['i'] ["ignore"] (ReqArg (\fp so -> so { so_ignore = fp:(so_ignore so) }) "PATH") ( concat
[ "files or directories (relative to the source and destination directories) that are to "
, "be ignored during synchroization (this option can be given multiple times)"
])
, Option ['a'] ["add"] (NoArg (\so -> so { so_add = True })) ( concat
[ "copy files from the source directory if there is corresponding file inside "
, "the destination directory"
])
, Option ['u'] ["update"] (NoArg (\so -> so { so_update = True })) ( concat
[ "overwrite existing files inside the destination directory if their content does not "
, "match the respective files inside the source directory"
])
, Option [] ["delete"] (NoArg (\so -> so { so_delete = True })) ( concat
[ "delete files inside the destination directory if there is no corresponding file inside "
, "the source directory"
])
, Option ['h'] ["help"] (NoArg (\so -> so { so_help = True })) "shows usage information"
]
parseNonOption :: String -> (SyncOptions -> SyncOptions)
parseNonOption s so = so { so_nonOptions = s:(so_nonOptions so) }
toSyncOptions :: [(SyncOptions -> SyncOptions)] -> SyncOptions
toSyncOptions = foldl (flip id) defaultSyncOptions
putError :: String -> IO ()
putError = hPutStrLn stderr
printUsageInfo :: [String] -> IO ()
printUsageInfo prefix = mapM_ putError (prefix ++ [usageInfo header optDescriptions] ++ [details])
where
header = unlines
[ "sync-mht version " ++ showVersion version
, ""
, "Usage: sync-mht [OPTIONS..]"
]
details = concat
[ "Note: If the --remote-shell option has been provided, exactly one of the directories "
, "must be prepended with 'remote:' - indicating a folder on the site, accessible with "
, "the provided remote shell command."
]
_HIDDENT_CLIENT_MODE_OPTION_ :: String
_HIDDENT_CLIENT_MODE_OPTION_ = "--hidden-client-mode-option"
main :: IO ()
main = flip catchIOError (putError . show) $
do args <- getArgs
let parsedOpts = getOpt (ReturnInOrder parseNonOption) optDescriptions args
case () of
() | [_HIDDENT_CLIENT_MODE_OPTION_] == args -> child
| (options,[],[]) <- parsedOpts -> run $ toSyncOptions options
| (_,_,errs) <- parsedOpts -> printUsageInfo errs
data Side
= Remote FilePath
| Local FilePath
parseFilePath :: FilePath -> Side
parseFilePath fp
| Just rest <- stripPrefix "remote:" fp = Remote rest
| otherwise = Local fp
run :: SyncOptions -> IO ()
run so
| so_help so =
printUsageInfo []
| not (null (so_nonOptions so)) =
printUsageInfo ["Could not understand the following options: " ++ show (so_nonOptions so)]
| Just source <- so_source so, Just destination <- so_destination so =
case (parseFilePath source, parseFilePath destination) of
(Remote _, Remote _) -> printUsageInfo [doubleRemote]
(Local source', Local destination')
| Just _ <- so_remote so -> printUsageInfo [missingRemote]
| otherwise -> local cs source' destination'
(Remote source', Local destination')
| Just remoteCmd <- so_remote so -> runParent cs remoteCmd source' destination' FromRemote
| otherwise -> printUsageInfo [missingRemoteCmd]
(Local source', Remote destination')
| Just remoteCmd <- so_remote so -> runParent cs remoteCmd source' destination' ToRemote
| otherwise -> printUsageInfo [missingRemoteCmd]
| otherwise =
do let missingOpts =
intercalate ", " $ map snd $ filter ((== Nothing) . ($ so) . fst)
[(so_source, "--source"),(so_destination, "--destination")]
printUsageInfo ["The follwing required options: " ++ missingOpts ++ " were missing."]
where
cs = toClientServerOptions so
doubleRemote = "Either the directory given in --source or --destination must be local."
missingRemote = concat
[ "The --remote-shell options requires that either the directory given at "
, "--source or --destination is at remote site. (Indicated by the prefix: 'remote:')"
]
missingRemoteCmd = "The --remote-shell is required when the prefix 'remote:' is used."
runParent :: ClientServerOptions -> String -> FilePath -> FilePath -> Direction -> IO ()
runParent clientServerOpts remoteCmd source destination dir =
do let remoteCmd' = remoteCmd ++ " " ++ _HIDDENT_CLIENT_MODE_OPTION_
handles <- createProcess ((shell remoteCmd') { std_in = CreatePipe, std_out = CreatePipe })
case handles of
(Just hIn, Just hOut, Nothing, _ph) ->
do streams <- openStreams hOut hIn
parent streams source destination dir clientServerOpts
_ -> fail "createProcess did return the correct set of handles."