sync-mht 0.2.1.1 → 0.3.0.0
raw patch · 12 files changed
+521/−209 lines, 12 filesdep +Cabaldep +randomdep +regex-compatdep ~directory
Dependencies added: Cabal, random, regex-compat, temporary, time
Dependency ranges changed: directory
Files
- README.md +3/−0
- dist/build/mainStub/mainStub-tmp/mainStub.hs +5/−0
- src/main/hs/Main.hs +2/−151
- src/main/hs/Sync/MerkleTree/Analyse.hs +15/−7
- src/main/hs/Sync/MerkleTree/Client.hs +17/−9
- src/main/hs/Sync/MerkleTree/CommTypes.hs +18/−8
- src/main/hs/Sync/MerkleTree/Run.hs +175/−0
- src/main/hs/Sync/MerkleTree/Sync.hs +41/−20
- src/main/hs/Sync/MerkleTree/Test.hs +183/−0
- src/main/hs/Sync/MerkleTree/Types.hs +13/−6
- src/main/hs/Sync/MerkleTree/Util/RequestMonad.hs +1/−4
- sync-mht.cabal +48/−4
README.md view
@@ -32,3 +32,6 @@ to synchronize the folder /bar (of the container named bar) with the local folder foo/. +Note that: The options -a -u --delete respectively, allow copying of files to the target directory,+updating files that are already in the target directory - not matching the contents in the source+directory and delete files that are in the destination directory but not in the source directory.
+ dist/build/mainStub/mainStub-tmp/mainStub.hs view
@@ -0,0 +1,5 @@+module Main ( main ) where+import Distribution.Simple.Test ( stubMain )+import Sync.MerkleTree.Test ( tests )+main :: IO ()+main = stubMain tests
src/main/hs/Main.hs view
@@ -1,165 +1,16 @@--- main module {-# 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"+import Sync.MerkleTree.Run 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+ () | [_HIDDENT_CLIENT_MODE_OPTION_] == args -> runChild | (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."-
src/main/hs/Sync/MerkleTree/Analyse.hs view
@@ -5,17 +5,19 @@ {-# LANGUAGE OverloadedStrings #-} module Sync.MerkleTree.Analyse ( analyseDirectory+ , isRealFile ) where import Control.Monad+import Data.Maybe import System.FilePath import Foreign.C.Types import Prelude hiding (lookup) import Sync.MerkleTree.Types import System.Directory- import System.Posix.Types import System.Posix.Files+import Text.Regex (matchRegex, mkRegex, Regex) import qualified Data.Text as T isRealFile :: String -> Bool@@ -23,16 +25,22 @@ | x `elem` [".", ".."] = False | otherwise = True -analyseDirectory :: FilePath -> [FilePath] -> Path -> IO [Entry]-analyseDirectory fp ignore path- | fp `elem` ignore = return []+shouldIgnore :: Path -> [Regex] -> Bool+shouldIgnore p regexes = any (isJust . (`matchRegex` (toFilePath "" p))) regexes++analyseDirectory :: FilePath -> [String] -> Path -> IO [Entry]+analyseDirectory fp ignore path = analyseDir fp (map mkRegex ignore) path++analyseDir :: FilePath -> [Regex] -> Path -> IO [Entry]+analyseDir fp ignore path+ | shouldIgnore path ignore = return [] | otherwise = do files <- getDirectoryContents fp liftM concat $ mapM (analyse fp ignore path) $ filter isRealFile files -analyse :: FilePath -> [FilePath] -> Path -> String -> IO [Entry]+analyse :: FilePath -> [Regex] -> Path -> String -> IO [Entry] analyse fp ignore path name- | (fp </> name) `elem` ignore = return []+ | shouldIgnore path' ignore = return [] | otherwise = do status <- getFileStatus fp' analyse' status@@ -50,6 +58,6 @@ , f_modtime = FileModTime modtime } ] | isDirectory status =- liftM ((DirectoryEntry path'):) $ analyseDirectory fp' ignore path'+ liftM ((DirectoryEntry path'):) $ analyseDir fp' ignore path' | otherwise = return [] -- No support for devices, sockets yet.
src/main/hs/Sync/MerkleTree/Client.hs view
@@ -6,6 +6,7 @@ import Control.Monad import Control.Monad.IO.Class import Data.Foldable(Foldable)+import Data.Function import Data.Monoid(Monoid, mappend, mempty, Sum(..)) import Data.Set(Set) import Data.List@@ -49,6 +50,11 @@ do True <- logReq $ SerText t return () +data SimpleEntry+ = FileSimpleEntry Path+ | DirectorySimpleEntry Path+ deriving (Eq, Ord)+ analyseEntries :: Diff Entry -> ([Entry],[Entry],[Entry]) analyseEntries (Diff obsoleteEntries newEntries) = (M.elems deleteMap, M.elems changeMap, M.elems newMap)@@ -59,8 +65,8 @@ obsMap = M.fromList $ S.toList $ S.map keyValue obsoleteEntries updMap = M.fromList $ S.toList $ S.map keyValue newEntries keyValue x = (name x, x)- name (FileEntry f) = f_name f- name (DirectoryEntry f) = f+ name (FileEntry f) = FileSimpleEntry $ f_name f+ name (DirectoryEntry f) = DirectorySimpleEntry f abstractClient :: (ClientMonad m) => ClientServerOptions -> FilePath -> Trie Entry -> m () abstractClient cs fp trie =@@ -78,20 +84,25 @@ case e of FileEntry f -> liftIO $ removeFile $ toFilePath fp $ f_name f DirectoryEntry p -> liftIO $ removeDirectory $ toFilePath fp p- mapM_ (split . map (synchronizeNewOrChangedEntry fp))- $ splitEvery _CONCURRENT_FILETRANSFER_SIZE_ $ sort- $ [ e | cs_add cs, e <- newEntries ] ++ [ e | cs_update cs, e <- changedEntries ]+ mapM_ (synchronizeNewOrChangedEntries fp)+ $ groupBy ((==) `on` levelOf)+ $ sort $ [ e | cs_add cs, e <- newEntries ] ++ [ e | cs_update cs, e <- changedEntries ] True <- terminateReq return () _CONCURRENT_FILETRANSFER_SIZE_ :: Int-_CONCURRENT_FILETRANSFER_SIZE_ = 96+_CONCURRENT_FILETRANSFER_SIZE_ = 48 splitEvery :: Int -> [a] -> [[a]] splitEvery n l | null l = [] | (h,t) <- splitAt n l = h:(splitEvery n t) +synchronizeNewOrChangedEntries :: (ClientMonad m) => FilePath -> [Entry] -> m ()+synchronizeNewOrChangedEntries fp entries =+ forM_ (splitEvery _CONCURRENT_FILETRANSFER_SIZE_ entries) $ \entryGroup ->+ split $ map (synchronizeNewOrChangedEntry fp) entryGroup+ synchronizeNewOrChangedEntry :: (ClientMonad m) => FilePath -> Entry -> m () synchronizeNewOrChangedEntry fp entry = case entry of@@ -121,6 +132,3 @@ do s' <- querySetReq loc let s = getAll trie return $ Diff (s `S.difference` s') (s' `S.difference` s)---
src/main/hs/Sync/MerkleTree/CommTypes.hs view
@@ -33,9 +33,7 @@ , cs_delete :: Bool , cs_ignore :: [FilePath] }- deriving (Generic, Show, Typeable)--instance Serialize ClientServerOptions+ deriving (Read, Show) data Request = QuerySet TrieLocation@@ -55,9 +53,21 @@ instance Serialize QueryFileResponse -data ChildSide- = Service FilePath ClientServerOptions- | Client FilePath ClientServerOptions- deriving (Show, Generic, Typeable)+data ProtocolVersion+ = Version1+ deriving (Read, Show, Eq) -instance Serialize ChildSide+thisProtocolVersion :: ProtocolVersion+thisProtocolVersion = Version1++data LaunchMessage+ = LaunchMessage+ { lm_protocolVersion :: ProtocolVersion+ , lm_dir :: FilePath+ , lm_side :: Side+ , lm_clientServerOptions :: ClientServerOptions+ }+ deriving (Read, Show)++data Side = Server | Client+ deriving (Read, Show)
+ src/main/hs/Sync/MerkleTree/Run.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+module Sync.MerkleTree.Run where++import Control.Concurrent+import System.Process+import System.IO+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 RemoteCmd+ = RemoteCmd String+ | Simulate++data SyncOptions+ = SyncOptions+ { so_source :: Maybe FilePath+ , so_destination :: Maybe FilePath+ , so_remote :: Maybe RemoteCmd+ , so_ignore :: [String]+ , 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 $ RemoteCmd 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+++_HIDDENT_CLIENT_MODE_OPTION_ :: String+_HIDDENT_CLIENT_MODE_OPTION_ = "--hidden-client-mode-option"+++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."+ ]+++data Location+ = Remote FilePath+ | Local FilePath++parseFilePath :: FilePath -> Location+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."++runChild :: IO ()+runChild =+ do streams <- openStreams stdin stdout+ child streams++runParent :: ClientServerOptions -> RemoteCmd -> FilePath -> FilePath -> Direction -> IO ()+runParent clientServerOpts mRemoteCmd source destination dir =+ case mRemoteCmd of+ RemoteCmd remoteCmd ->+ 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 not return the correct set of handles."+ Simulate ->+ do (parentInStream, childOutStream) <- mkChanStreams+ (childInStream, parentOutStream) <- mkChanStreams+ _ <- forkIO $ child $ StreamPair { sp_in = childInStream, sp_out = childOutStream }+ let parentStreams = StreamPair { sp_in = parentInStream, sp_out = parentOutStream }+ parent parentStreams source destination dir clientServerOpts
src/main/hs/Sync/MerkleTree/Sync.hs view
@@ -8,9 +8,12 @@ , local , parent , openStreams+ , mkChanStreams+ , StreamPair(..) , Direction(..) ) where +import Control.Concurrent(newChan) import Control.Monad import Control.Monad.State import Data.Monoid@@ -24,6 +27,7 @@ import qualified Data.Serialize as SE import qualified Data.ByteString as BS import qualified System.IO.Streams as ST+import qualified System.IO.Streams.Concurrent as ST import Sync.MerkleTree.Analyse import Sync.MerkleTree.CommTypes@@ -43,6 +47,11 @@ outStream <- ST.handleToOutputStream hOut return $ StreamPair { sp_in = inStream, sp_out = outStream } +mkChanStreams :: IO (InputStream ByteString, OutputStream ByteString)+mkChanStreams =+ do chan <- newChan+ liftM2 (,) (ST.chanToInput chan) (ST.chanToOutput chan)+ instance Protocol RequestMonad where queryHashReq = request . QueryHash querySetReq = request . QuerySet@@ -61,23 +70,28 @@ = FromRemote | ToRemote -child :: IO ()-child =- do streams <- openStreams stdin stdout- side <- getFromInputStream (sp_in streams)- case side of- Service dir clientServerOpts -> server dir clientServerOpts streams- Client dir clientServerOpts -> client dir clientServerOpts streams+child :: StreamPair -> IO ()+child streams =+ do launchMessage <- getFromInputStream (sp_in streams)+ serverOrClient (read launchMessage) streams parent :: StreamPair -> FilePath -> FilePath -> Direction -> ClientServerOptions -> IO () parent streams source destination direction clientServerOpts = case direction of FromRemote ->- do respond (sp_out streams) $ Service source clientServerOpts- client destination clientServerOpts streams+ do respond (sp_out streams) $ show $ mkLaunchMessage Server source+ serverOrClient (mkLaunchMessage Client destination) streams ToRemote ->- do respond (sp_out streams) $ Client destination clientServerOpts- server source clientServerOpts streams+ do respond (sp_out streams) $ show $ mkLaunchMessage Client destination+ serverOrClient (mkLaunchMessage Server source) streams+ where+ mkLaunchMessage side dir =+ LaunchMessage+ { lm_dir = dir+ , lm_clientServerOptions = clientServerOpts+ , lm_protocolVersion = thisProtocolVersion+ , lm_side = side+ } respond :: (Show a, SE.Serialize a) => OutputStream ByteString -> a -> IO () respond os = mapM_ (flip ST.write os . Just) . (:[BS.empty]) . SE.encode@@ -88,10 +102,19 @@ destinationDir <- liftM (mkTrie 0) $ analyseDirectory destination (cs_ignore cs) Root evalStateT (abstractClient cs destination destinationDir) (startServerState source sourceDir) -server :: FilePath -> ClientServerOptions -> StreamPair -> IO ()-server fp cs streams =- do dir <- analyseDirectory fp (cs_ignore cs) Root- evalStateT loop (startServerState fp $ mkTrie 0 dir)+serverOrClient :: LaunchMessage -> StreamPair -> IO ()+serverOrClient lm streams+ | lm_protocolVersion lm == thisProtocolVersion =+ let side =+ case lm_side lm of+ Server -> server+ Client -> client (lm_clientServerOptions lm)+ in do entries <- analyseDirectory (lm_dir lm) (cs_ignore $ lm_clientServerOptions lm) Root+ side entries (lm_dir lm) streams+ | otherwise = fail "Incompatible sync-mht versions."++server :: [Entry] -> FilePath -> StreamPair -> IO ()+server entries fp streams = evalStateT loop (startServerState fp $ mkTrie 0 entries) where serverRespond = liftIO . respond (sp_out streams) loop =@@ -104,8 +127,6 @@ Log t -> logReq t >>= serverRespond >> loop Terminate -> terminateReq >>= serverRespond >> return () -client :: FilePath -> ClientServerOptions -> StreamPair -> IO ()-client fp cs streams =- do dir <- analyseDirectory fp (cs_ignore cs) Root- runRequestMonad (sp_in streams) (sp_out streams) $ abstractClient cs fp $ mkTrie 0 dir-+client :: ClientServerOptions -> [Entry] -> FilePath -> StreamPair -> IO ()+client cs entries fp streams =+ runRequestMonad (sp_in streams) (sp_out streams) $ abstractClient cs fp $ mkTrie 0 entries
+ src/main/hs/Sync/MerkleTree/Test.hs view
@@ -0,0 +1,183 @@+module Sync.MerkleTree.Test where++import Control.Concurrent+import Control.Monad+import Data.Ix+import Data.List+import Data.Time.Calendar+import Data.Time.Clock+import Sync.MerkleTree.Analyse+import Sync.MerkleTree.Run+import System.Directory+import System.FilePath+import System.IO+import System.IO.Error+import System.IO.Temp+import System.Posix.Files+import System.Random+import qualified Distribution.TestSuite as TS++mkTestInstance :: String -> IO TS.Result -> TS.TestInstance+mkTestInstance name run = ti+ where+ ti =+ TS.TestInstance+ { TS.run = liftM TS.Finished (catchIOError run $ return . TS.Fail . show)+ , TS.name = name+ , TS.tags = []+ , TS.options = []+ , TS.setOption = setOpt+ }+ setOpt _ _ = Right ti++tests :: IO [TS.Test]+tests = return $+ [ TS.testGroup "all" $+ [ TS.Test testOptions+ ] ++ (map TS.Test testSync)+ ]++testOptions :: TS.TestInstance+testOptions = mkTestInstance "testOptions" $+ let prepare add update delete go =+ withSystemTempDirectory "sync-mht" $ \testDir ->+ do let srcDir = testDir </> "src"+ destDir = testDir </> "dest"+ createDirectory srcDir+ createDirectory destDir+ writeFile (srcDir </> "same.txt") "test"+ copyFile (srcDir </> "same.txt") (destDir </> "same.txt")+ writeFile (srcDir </> "changed.txt") "test"+ writeFile (destDir </> "changed.txt") "testB"+ writeFile (srcDir </> "added.txt") "test"+ writeFile (destDir </> "deleted.txt") "testB"+ run $+ SyncOptions+ { so_source = Just $ srcDir+ , so_destination = Just $ destDir+ , so_remote = Nothing+ , so_ignore = []+ , so_add = add+ , so_update = update+ , so_delete = delete+ , so_help = False+ , so_nonOptions = []+ }+ True <- liftM (=="test") $ readFile (srcDir </> "same.txt")+ True <- liftM (=="test") $ readFile (srcDir </> "changed.txt")+ True <- liftM (=="test") $ readFile (srcDir </> "added.txt")+ go srcDir destDir+ in do prepare True False False $ \srcDir destDir ->+ do True <- liftM (=="test") $ readFile (destDir </> "same.txt")+ True <- liftM (=="testB") $ readFile (destDir </> "changed.txt")+ True <- liftM (=="test") $ readFile (destDir </> "added.txt")+ True <- liftM (=="testB") $ readFile (destDir </> "deleted.txt")+ return ()+ prepare False True False $ \srcDir destDir ->+ do True <- liftM (=="test") $ readFile (destDir </> "same.txt")+ True <- liftM (=="test") $ readFile (destDir </> "changed.txt")+ True <- liftM (=="testB") $ readFile (destDir </> "deleted.txt")+ return ()+ prepare False False True $ \srcDir destDir ->+ do True <- liftM (=="test") $ readFile (destDir </> "same.txt")+ True <- liftM (=="testB") $ readFile (destDir </> "changed.txt")+ False <- doesFileExist (destDir </> "deleted.txt")+ return ()+ return TS.Pass++testSync :: [TS.TestInstance]+testSync = + flip map [0,1,2] $ \simulate -> mkTestInstance ("testSync"++ show simulate) $+ do forM [1..50] $ \_ ->+ withSystemTempDirectory "sync-mht" $ \testDir ->+ do mkRandomDir 2 [testDir </> "src", testDir </> "src-backup"]+ mkRandomDir 2 [testDir </> "target"]+ let sourcePrefix+ | simulate == 1 = "remote:"+ | otherwise = ""+ targetPrefix+ | simulate == 2 = "remote:"+ | otherwise = ""+ run $+ SyncOptions+ { so_source = Just $ (sourcePrefix ++) testDir </> "src"+ , so_destination = Just $ (targetPrefix ++) testDir </> "target"+ , so_remote =+ case simulate of+ 0 -> Nothing+ _ -> Just Simulate+ , so_ignore = []+ , so_add = True+ , so_update = True+ , so_delete = True+ , so_help = False+ , so_nonOptions = []+ }+ areDirsEqual (testDir </> "src") (testDir </> "target")+ areDirsEqual (testDir </> "src") (testDir </> "src-backup")+ return TS.Pass++data FileOrDir+ = File+ | Dir+ deriving (Eq, Ix, Enum, Bounded, Ord, Show)++instance Random FileOrDir where+ random = randomR (minBound, maxBound)+ randomR (l, u) g =+ let (i, g) = randomR (fromEnum l, fromEnum u) g+ in (toEnum i, g)++utcTimeFrom :: Integer -> UTCTime+utcTimeFrom x = UTCTime (fromGregorian 2015 07 29) (fromInteger x)++mkRandomDir :: Integer -> [FilePath] -> IO ()+mkRandomDir md fps =+ do forM fps createDirectory+ names <- distinctNames 6+ forM_ names $ \n ->+ do m <- randomRIO (0,1)+ case toEnum m of+ Dir | md > 0 -> mkRandomDir (md - 1) $ map (</> n) fps+ | otherwise -> return ()+ File ->+ do d <- randomRIO (1,4)+ forM_ fps $ \fp ->+ do writeFile (fp </> n) (show d)+ setModificationTime (fp </> n) (utcTimeFrom d)+++areDirsEqual :: FilePath -> FilePath -> IO ()+areDirsEqual fp1 fp2 =+ do files1 <- liftM (sort . filter isRealFile) $ getDirectoryContents fp1+ files2 <- liftM (sort . filter isRealFile) $ getDirectoryContents fp2+ case () of+ () | files1 == files2 -> forM_ files1 $ \f -> areEntriesEqual (fp1 </> f) (fp2 </> f)+ | otherwise -> fail $ "Unequal: " ++ show (fp1, fp2, files1, files2)++areEntriesEqual :: FilePath -> FilePath -> IO ()+areEntriesEqual f1 f2 =+ do s1 <- getFileStatus f1+ s2 <- getFileStatus f2+ case () of+ () | isDirectory s1, isDirectory s2 -> areDirsEqual f1 f2+ | isRegularFile s1, isRegularFile s2 ->+ do c1 <- readFile f1+ c2 <- readFile f2+ unless (c1 == c2) $fail $ "Unequal files: " ++ show (f1, f2, c1, c2)+ | otherwise ->+ fail $ show (f1, f2, isDirectory s1, isDirectory s2, isRegularFile s1, isRegularFile s2)+++-- | @distinctNames k@ creates k distinct file names+distinctNames :: Integer -> IO [String]+distinctNames k = retry+ where+ isDistinct names = null $ filter hasDuplicate $ group $ sort names+ hasDuplicate = (/= 1) . length+ mkName _ = liftM (:[]) $ randomRIO ('a','m')+ retry =+ do result <- forM [1..k] mkName+ if isDistinct result+ then return result+ else retry
src/main/hs/Sync/MerkleTree/Types.hs view
@@ -60,13 +60,21 @@ instance Ord Entry where compare = comparing withLevel where- level Root = 0 :: Int- level (Path _ p) = 1 + level p- withLevel entry =+ withLevel entry = (levelOf entry, toEither entry)+ toEither entry = case entry of- DirectoryEntry p -> (level p, Right p)- FileEntry f -> (level $ f_name f, Left f)+ DirectoryEntry p -> Right p+ FileEntry f -> Left f +levelOf :: Entry -> Int+levelOf e =+ case e of+ DirectoryEntry p -> level p+ FileEntry f -> level $ f_name f+ where+ level Root = 0+ level (Path _ p) = 1 + level p+ instance HasDigest Entry where digest = hash . SE.encode @@ -76,4 +84,3 @@ instance SE.Serialize SerText where get = SE.get >>= either (fail . show) (return . SerText) . TE.decodeUtf8' put = SE.put . TE.encodeUtf8 . unSerText-
src/main/hs/Sync/MerkleTree/Util/RequestMonad.hs view
@@ -39,7 +39,6 @@ import Data.IORef(IORef,newIORef,modifyIORef,readIORef) import Data.Monoid(Monoid, mempty, mappend) import Data.Serialize(Serialize)-import System.IO(hPutStrLn,stderr) import System.IO.Streams(InputStream, OutputStream) import qualified Data.ByteString as BS import qualified Data.Serialize as SE@@ -162,9 +161,7 @@ do x <- getFromInputStream input modifyIORef recvIdx (+1) expected <- readIORef recvIdx- unless (expected == i) $- do hPutStrLn stderr $ "Expected " ++ (show i) ++ " but got " ++ show expected- fail $ "Expected " ++ show i ++ " but got " ++ show expected+ unless (expected == i) $ fail ("Expected " ++ (show i) ++ " but got " ++ show expected) queueRequests sq $ cont x Split (SplitState xs z cont) -> loop cont z xs [] Return x -> return $ Return x
sync-mht.cabal view
@@ -6,9 +6,9 @@ maintainer: Emin Karayel <me@eminkarayel.de> category: Utility extra-doc-files: README.md-cabal-version: >=1.10+cabal-version: >= 1.18 build-type: Simple-version: 0.2.1.1+version: 0.3.0.0 homepage: https://github.com/ekarayel/sync-mht bug-reports: https://github.com/ekarayel/sync-mht/issues package-url: https://github.com/ekarayel/sync-mht@@ -29,8 +29,46 @@ location: https://github.com/ekarayel/sync-mht --recursive source-repository this type: git- tag: 0.2.1.1+ tag: 0.3.0.0 location: https://github.com/ekarayel/sync-mht --recursive+test-suite main+ type: detailed-0.9+ test-module: Sync.MerkleTree.Test+ other-modules:+ Sync.MerkleTree.Analyse+ Sync.MerkleTree.Client+ Sync.MerkleTree.CommTypes+ Sync.MerkleTree.Server+ Sync.MerkleTree.Sync+ Sync.MerkleTree.Run+ Sync.MerkleTree.Trie+ Sync.MerkleTree.Types+ Sync.MerkleTree.Util.RequestMonad+ Paths_sync_mht+ hs-source-dirs: src/main/hs+ default-language: Haskell2010+ build-depends: + base >=4.7 && <4.8+ , unix >=2.7 && <2.8+ , directory >=1.2.3 && <1.3+ , filepath >=1.3 && <1.4+ , process >=1.2 && <1.3+ , cryptohash >=0.11 && <0.12+ , byteable >=0.1 && <0.2+ , array >=0.5 && <0.6+ , containers >=0.5 && <0.6+ , text >=1.2 && <1.3+ , bytestring >=0.10 && <0.11+ , base16-bytestring >=0.1 && <0.2+ , cereal >= 0.4 && < 0.5+ , io-streams >= 1.2 && <1.3+ , transformers >= 0.4 && < 0.5+ , regex-compat >= 0.95 && < 0.96+ , mtl >= 2.2 && < 2.3+ , Cabal >= 1.18 && <= 1.19+ , time >= 1.4 && < 1.5+ , random >= 1.0 && < 1.1+ , temporary >= 1.2 executable sync-mht main-is: Main.hs other-modules:@@ -39,6 +77,7 @@ Sync.MerkleTree.CommTypes Sync.MerkleTree.Server Sync.MerkleTree.Sync+ Sync.MerkleTree.Run Sync.MerkleTree.Trie Sync.MerkleTree.Types Sync.MerkleTree.Util.RequestMonad@@ -46,7 +85,7 @@ build-depends: base >=4.7 && <4.8 , unix >=2.7 && <2.8- , directory >=1.2 && <1.3+ , directory >=1.2.3 && <1.3 , filepath >=1.3 && <1.4 , process >=1.2 && <1.3 , cryptohash >=0.11 && <0.12@@ -59,6 +98,11 @@ , cereal >= 0.4 && < 0.5 , io-streams >= 1.2 && <1.3 , transformers >= 0.4 && < 0.5+ , regex-compat >= 0.95 && < 0.96 , mtl >= 2.2 && < 2.3+ , Cabal >= 1.18 && <= 1.19+ , time >= 1.4 && < 1.5+ , random >= 1.0 && < 1.1+ , temporary >= 1.2 hs-source-dirs: src/main/hs default-language: Haskell2010