@@ -41,6 +41,10 @@ module Mismi.S3.Commands (
4141 , downloadWithModeOrFail
4242 , downloadSingle
4343 , downloadWithRange
44+ , downloadRecursive
45+ , downloadRecursiveOrFail
46+ , downloadRecursiveWithMode
47+ , downloadRecursiveWithModeOrFail
4448 , multipartDownload
4549 , listMultipartParts
4650 , listMultiparts
@@ -63,12 +67,13 @@ module Mismi.S3.Commands (
6367import Control.Arrow ((***) )
6468
6569import Control.Exception (ioError )
70+ import qualified Control.Exception as CE
6671import Control.Lens ((.~) , (^.) , to , view )
6772import Control.Monad.Catch (throwM , onException )
6873import Control.Monad.Trans.Class (lift )
6974import Control.Monad.Trans.Resource (ResourceT , allocate , runResourceT )
7075import Control.Monad.Reader (ask )
71- import Control.Monad.IO.Class (liftIO )
76+ import Control.Monad.IO.Class (MonadIO , liftIO )
7277
7378import qualified Data.ByteString as BS
7479import Data.Conduit (Conduit , Source , ResumableSource )
@@ -108,9 +113,11 @@ import P
108113
109114import System.IO (IO , IOMode (.. ), SeekMode (.. ))
110115import System.IO (hFileSize , hSetFileSize , withFile )
116+ import System.IO.Error (IOError )
111117import System.Directory (createDirectoryIfMissing , doesFileExist )
112- import System.FilePath (FilePath , takeDirectory )
118+ import System.FilePath (FilePath , (</>) , takeDirectory )
113119import System.Posix.IO (OpenMode (.. ), openFd , closeFd , fdSeek , defaultFileFlags )
120+ import System.Posix.Files (getFileStatus , isDirectory )
114121import qualified "unix-bytestring" System.Posix.IO.ByteString as UBS
115122
116123import System.Timeout.Lifted (timeout )
@@ -119,7 +126,8 @@ import System.IO.Error (userError)
119126import Twine.Data.Queue (writeQueue )
120127import Twine.Parallel (RunError (.. ), consume )
121128
122- import X.Control.Monad.Trans.Either (EitherT , eitherT , left , right , bimapEitherT , runEitherT , newEitherT )
129+ import X.Control.Monad.Trans.Either (EitherT , eitherT , left , right , bimapEitherT , hoistMaybe
130+ , runEitherT , newEitherT )
123131
124132import qualified X.Data.Conduit.Binary as XB
125133
@@ -549,6 +557,10 @@ hoistDownloadError e =
549557 throwM $ SourceMissing DownloadError a
550558 DownloadDestinationExists f ->
551559 throwM $ DestinationFileExists f
560+ DownloadDestinationNotDirectory f ->
561+ throwM $ DestinationNotDirectory f
562+ DownloadInvariant a b ->
563+ throwM $ Invariant (renderDownloadError $ DownloadInvariant a b)
552564 MultipartError (WorkerError a) ->
553565 throwM a
554566 MultipartError (BlowUpError a) ->
@@ -622,6 +634,37 @@ downloadWithRange a start end dest = withRetries 5 $ do
622634 Just () -> pure ()
623635 Nothing -> liftIO $ ioError (userError " downloadWithRange timeout" )
624636
637+ downloadRecursiveWithMode :: WriteMode -> Address -> FilePath -> EitherT DownloadError AWS ()
638+ downloadRecursiveWithMode mode src dest = do
639+ -- Check if the destination already exists and is not a directory.
640+ es <- tryIO $ getFileStatus dest
641+ case es of
642+ Left _ -> pure ()
643+ Right st -> unless (isDirectory st) . left $ DownloadDestinationNotDirectory dest
644+ -- Real business starts here.
645+ addrs <- lift $ listRecursively src
646+ mapM_ drWorker addrs
647+ where
648+ tryIO :: MonadIO m => IO a -> m (Either IOError a )
649+ tryIO = liftIO . CE. try
650+
651+ drWorker :: Address -> EitherT DownloadError AWS ()
652+ drWorker addr = do
653+ fpdest <- hoistMaybe (DownloadInvariant addr src) $
654+ ((</>) dest) . T. unpack . unKey <$> removeCommonPrefix src addr
655+ downloadWithMode mode addr fpdest
656+
657+ downloadRecursive :: Address -> FilePath -> EitherT DownloadError AWS ()
658+ downloadRecursive =
659+ downloadRecursiveWithMode Fail
660+
661+ downloadRecursiveOrFail :: Address -> FilePath -> AWS ()
662+ downloadRecursiveOrFail a f =
663+ eitherT hoistDownloadError pure $ downloadRecursive a f
664+
665+ downloadRecursiveWithModeOrFail :: WriteMode -> Address -> FilePath -> AWS ()
666+ downloadRecursiveWithModeOrFail m a f =
667+ eitherT hoistDownloadError pure $ downloadRecursiveWithMode m a f
625668
626669listMultipartParts :: Address -> Text -> AWS [Part ]
627670listMultipartParts a uploadId = do
0 commit comments