Skip to content

Commit d05fec9

Browse files
committed
mismi-s3: Add downloadRecursive function
1 parent abd32e2 commit d05fec9

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

mismi-s3/src/Mismi/S3/Commands.hs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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 (
6367
import Control.Arrow ((***))
6468

6569
import Control.Exception (ioError)
70+
import qualified Control.Exception as CE
6671
import Control.Lens ((.~), (^.), to, view)
6772
import Control.Monad.Catch (throwM, onException)
6873
import Control.Monad.Trans.Class (lift)
6974
import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT)
7075
import Control.Monad.Reader (ask)
71-
import Control.Monad.IO.Class (liftIO)
76+
import Control.Monad.IO.Class (MonadIO, liftIO)
7277

7378
import qualified Data.ByteString as BS
7479
import Data.Conduit (Conduit, Source, ResumableSource)
@@ -108,9 +113,11 @@ import P
108113

109114
import System.IO (IO, IOMode (..), SeekMode (..))
110115
import System.IO (hFileSize, hSetFileSize, withFile)
116+
import System.IO.Error (IOError)
111117
import System.Directory (createDirectoryIfMissing, doesFileExist)
112-
import System.FilePath (FilePath, takeDirectory)
118+
import System.FilePath (FilePath, (</>), takeDirectory)
113119
import System.Posix.IO (OpenMode(..), openFd, closeFd, fdSeek, defaultFileFlags)
120+
import System.Posix.Files (getFileStatus, isDirectory)
114121
import qualified "unix-bytestring" System.Posix.IO.ByteString as UBS
115122

116123
import System.Timeout.Lifted (timeout)
@@ -119,7 +126,8 @@ import System.IO.Error (userError)
119126
import Twine.Data.Queue (writeQueue)
120127
import 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

124132
import 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

626669
listMultipartParts :: Address -> Text -> AWS [Part]
627670
listMultipartParts a uploadId = do

mismi-s3/src/Mismi/S3/Data.hs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ data S3Error =
7474
| DestinationAlreadyExists Address
7575
| DestinationDoesNotExist Address
7676
| DestinationFileExists FilePath
77+
| DestinationNotDirectory FilePath
78+
| DestinationMissing FilePath
7779
| AccessDenied Address
7880
| Invariant Text
7981
| Target Address Address
@@ -94,6 +96,10 @@ s3ErrorRender s3err = "[Mismi internal error] - " <> case s3err of
9496
"Can not upload to an address that already exists [" <> addressToText a <> "]"
9597
DestinationFileExists f ->
9698
"Can not download to a target that already exists [" <> T.pack f <> "]"
99+
DestinationNotDirectory f ->
100+
"Expecting destination " <> T.pack f <> " to be a directory."
101+
DestinationMissing f ->
102+
"Download destination directory " <> T.pack f <> " does not exist."
97103
DestinationDoesNotExist a ->
98104
"This address does not exist [" <> addressToText a <> "]"
99105
AccessDenied a ->
@@ -118,6 +124,8 @@ renderErrorType e = case e of
118124
data DownloadError =
119125
DownloadSourceMissing Address
120126
| DownloadDestinationExists FilePath
127+
| DownloadDestinationNotDirectory FilePath
128+
| DownloadInvariant Address Address
121129
| MultipartError (RunError Error)
122130
deriving Show
123131

@@ -128,6 +136,12 @@ renderDownloadError d =
128136
"Can not download when the source object does not exist [" <> addressToText a <> "]"
129137
DownloadDestinationExists f ->
130138
"Can not download to a target that already exists [" <> T.pack f <> "]"
139+
DownloadDestinationNotDirectory f ->
140+
"Destination for a recursive download, " <> T.pack f <> " is not a directory."
141+
DownloadInvariant a b ->
142+
"Remove common prefix invariant: " <>
143+
"[" <> addressToText b <> "] is not a common prefix of " <>
144+
"[" <> addressToText a <> "]"
131145
MultipartError r ->
132146
"Multipart download error: " <> renderRunError r renderError
133147

0 commit comments

Comments
 (0)