{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ExtendedDefaultRules #-} {-# OPTIONS_GHC -fno-warn-type-defaults #-} module Main where import Control.Monad.IO.Class (MonadIO(..)) import Data.IORef import Data.List (sort, sortBy, isPrefixOf, stripPrefix) import Data.Maybe (fromJust) import Data.Monoid ( (<>) ) import Data.Ord (comparing, Down(..)) import Data.Text (Text(..)) import qualified Data.Text as T import System.Directory (copyFileWithMetadata) import System.FilePath (splitPath, joinPath) import System.FilePath.Find (find, always, fileType, (==?) , FileType(..), FindClause(..) , (&&?), (<=?) ) import qualified System.FilePath.Find as Fd (fileSize) import System.IO (hSetBuffering, stdout, BufferMode(..)) import System.Posix.Files (fileSize, modificationTime , getSymbolicLinkStatus , setFileTimesHiRes, accessTimeHiRes , modificationTimeHiRes, getFileStatus) import System.Posix.Types (COff(..)) import System.IO.Unsafe import qualified Shelly.Lifted as S import Shelly.Lifted hiding (find, FilePath) import Trunc (truncate) import CmdArgs (CmdArgs(..), withCmdArgs) default (Text) -- | naughty. shift these into a reader monad or something. {-# NOINLINE debugRef #-} debugRef = unsafePerformIO $ newIORef False {-# NOINLINE debug #-} debug = unsafePerformIO $ readIORef debugRef -- | number of levels in a path depth path = length $ splitPath path -- | basically, makes srcFile relative to srcDir, -- then substitutes in dstDir instead. -- -- will throw exception on empty strings. replaceDir :: String -> String -> String -> String replaceDir srcDir dstDir srcFile= let srcDir' = if last srcDir /= '/' then srcDir <> "/" else srcDir dstDir' = if last dstDir /= '/' then dstDir <> "/" else dstDir in replaceDir_ srcDir' dstDir' srcFile where replaceDir_ srcDir dstDir srcFile = if srcDir `isPrefixOf` srcFile then let fileBit = fromJust $ stripPrefix srcDir srcFile in joinPath [dstDir, fileBit] else error $ "replaceDir: srcDir '" <> srcDir <> "' is not prefix of srcFile '" <> srcFile <> "'" -- | replaceDir' - version for Text replaceDir' srcDir dstDir srcFile = T.pack $ replaceDir (T.unpack srcDir) (T.unpack dstDir) (T.unpack srcFile) -- NB: -- "cp -a" -- is same as System.Directory.copyFileWithMetadata -- | eitherFiles pred f g src dst -- -- where src and dst are directory names, -- will process all the files under src according -- to pred. If they pass, it processes them with "f" -- (takes the src file path and the dest file path), -- otherwise with "g". eitherFiles :: FindClause Bool -> (Text -> Text -> Sh a) -> (Text -> Text -> Sh a) -> Text -> Text -> Sh ([a], [a]) eitherFiles = filtSplitFiles (fileType ==? RegularFile) -- | same as eitherFiles, but return Sh (). eitherFiles_ :: FindClause Bool -> (Text -> Text -> Sh ()) -> (Text -> Text -> Sh ()) -> Text -> Text -> Sh () eitherFiles_ pred f g src dst = do _ <- eitherFiles pred f g src dst return () -- same as eitherFiles, but for directories. eitherDirs = filtSplitFiles (fileType ==? Directory) -- | filtSplitFiles filterPred splitPred f g src dst: -- -- iterate over the files in directory src. -- filter for ones matching filterPred, discarding all others. -- then split them into 2 - those matching splitPred, and those not. -- On those that do, execute f, on the others, execute g. -- Return the results filtSplitFiles :: Control.Monad.IO.Class.MonadIO m => FindClause Bool -> FindClause Bool -> (Text -> Text -> m a) -> (Text -> Text -> m b) -> Text -> Text -> m ([a], [b]) filtSplitFiles filterPred splitPred f g src dst = do predDirs <- liftIO $ find always (filterPred &&? splitPred) (T.unpack src) otherfiles <- liftIO $ find always (filterPred &&? (not <$> splitPred)) (T.unpack src) let getNewName = replaceDir' src dst doF p = f p (getNewName p) doG p = g p (getNewName p) good <- mapM (doF . T.pack) predDirs ungood <- mapM (doG . T.pack) otherfiles return (good, ungood) -- | bogoCopy pred srcDir dstDir: make a 'clone' of srcDir at dstDir; -- but if a file passes "pred", then make a real copy, -- but if not, just make a zero-size sparse file with the same -- name and attributes. --bogoCopy -- :: System.FilePath.Find.FindClause Bool bogoCopy :: FindClause Bool -> Text -> Text -> Sh () bogoCopy pred srcDir dstDir = do let isDir = fileType ==? Directory when debug $ echo_err "copying directory structure" tree_cp srcDir dstDir when debug $ echo_err "copying files" eitherFiles_ pred real_cp zero_cp srcDir dstDir let -- return the src, and an action to clone the time depthAndSetTime :: Text -> Text -> Sh (String, Sh ()) depthAndSetTime src dst = return (T.unpack src, time_cp src dst) when debug $ echo_err "cloning dir times" -- set the times, running actions in reverse order of depth -- (i.e. deepest first) (xs, _) <- eitherDirs isDir depthAndSetTime (\_ _ -> return ()) srcDir dstDir sequence_ $ snd $ unzip $ sortBy (comparing (Down . fst)) xs fromPath = T.unpack . toTextIgnore -- | FilePath version of fileSize fileSize' path = fileSize <$> (getFileStatus . fromPath) path hasSize pred path = do sz <- fileSize' path return $ pred sz real_cp :: Text -> Text -> Sh () real_cp src dst = do -- cmd "cp" "-d" "--preserve=all" src dst when debug $ echo_err $ "real_cp " <> src <> " " <> dst liftIO $ copyFileWithMetadata (T.unpack src) (T.unpack dst) zero_cp :: Text -> Text -> Sh () zero_cp src dst = do when debug $ echo_err $ "zero_cp " <> src <> " " <> dst cmd "cp" "--attributes-only" "--preserve=all" src dst stat <- liftIO $ getSymbolicLinkStatus (T.unpack src) let aTime = accessTimeHiRes stat mTime = modificationTimeHiRes stat sz = fileSize stat liftIO $ do Trunc.truncate (T.unpack dst) 0 Trunc.truncate (T.unpack dst) (fromIntegral sz) setFileTimesHiRes (T.unpack dst) aTime mTime time_cp :: Text -> Text -> Sh () time_cp src dst = do when debug $ echo_err $ "time_cp " <> src <> " " <> dst -- cmd "cp" "--attributes-only" "--preserve=all" src dst stat <- liftIO $ getSymbolicLinkStatus (T.unpack src) let aTime = accessTimeHiRes stat mTime = modificationTimeHiRes stat sz = fileSize stat liftIO $ setFileTimesHiRes (T.unpack dst) aTime mTime -- | make copy of directory tree. should preserve modification times, ownership/permissions tree_cp :: Text -> Text -> Sh () tree_cp src dst = do when debug $ echo_err $ "tree_cp " <> src <> " " <> dst run_ "rsync" ["-avAt", "--include", "*/", "--exclude", "*", src, dst] -- | src and dst are source and dest dirs. -- maxSize is size less than which, in MB, we should make real copies mainSh :: String -> String -> COff -> IO () mainSh src dst maxSizeBytes = do hSetBuffering stdout LineBuffering let verbosify = if debug then verbosely else silently shelly $ verbosify $ bogoCopy (Fd.fileSize <=? maxSizeBytes) (T.pack (src <> "/")) (T.pack dst) main :: IO () main = withCmdArgs $ \(CmdArgs.CmdArgs verbose bytesSize srcDir dstDir) -> do writeIORef debugRef verbose mainSh srcDir dstDir (fromIntegral bytesSize)