{-# 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)
{-# NOINLINE debugRef #-}
debugRef = unsafePerformIO $ newIORef False
{-# NOINLINE debug #-}
debug = unsafePerformIO $ readIORef debugRef
depth path = length $ splitPath path
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' srcDir dstDir srcFile =
T.pack $ replaceDir (T.unpack srcDir) (T.unpack dstDir) (T.unpack srcFile)
eitherFiles
:: FindClause Bool
-> (Text -> Text -> Sh a)
-> (Text -> Text -> Sh a)
-> Text
-> Text
-> Sh ([a], [a])
eitherFiles =
filtSplitFiles (fileType ==? RegularFile)
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 ()
eitherDirs =
filtSplitFiles (fileType ==? Directory)
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 :: 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
depthAndSetTime :: Text -> Text -> Sh (String, Sh ())
depthAndSetTime src dst =
return (T.unpack src, time_cp src dst)
when debug $
echo_err "cloning dir times"
(xs, _) <- eitherDirs isDir
depthAndSetTime
(\_ _ -> return ())
srcDir
dstDir
sequence_ $ snd $ unzip $ sortBy (comparing (Down . fst)) xs
fromPath = T.unpack . toTextIgnore
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
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
stat <- liftIO $ getSymbolicLinkStatus (T.unpack src)
let aTime = accessTimeHiRes stat
mTime = modificationTimeHiRes stat
sz = fileSize stat
liftIO $
setFileTimesHiRes (T.unpack dst) aTime mTime
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]
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)