Skip to content

Commit 35797f8

Browse files
Merge pull request #376 from ambiata/topic/recursuve-download
Recursive download
2 parents 38d599f + c3d0116 commit 35797f8

File tree

5 files changed

+86
-7
lines changed

5 files changed

+86
-7
lines changed

lib/x

Submodule x updated from 2c622d8 to 2e5dc0f

mismi-cli/main/s3.hs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ data UnitPrefix =
6969

7070
data Command =
7171
Upload FilePath Address WriteMode
72-
| Download Address FilePath
72+
| Download Recursive Address FilePath
7373
| Copy Address Address WriteMode
7474
| Concat [Address] Address WriteMode Int
7575
| Move Address Address
@@ -128,8 +128,10 @@ run c = do
128128
orDie O.renderError . O.runAWS e' $ case c of
129129
Upload s d m ->
130130
uploadWithModeOrFail m s d
131-
Download s d ->
131+
Download NotRecursive s d ->
132132
renderExit renderDownloadError . download s . optAppendFileName d $ key s
133+
Download Recursive s d ->
134+
renderExit renderDownloadError $ downloadRecursive s d
133135
Copy s d m ->
134136
renderExit renderCopyError $ copyWithMode m s d
135137
Concat ss d m f ->
@@ -165,6 +167,7 @@ sizeRecursive root d p =
165167
bytes <- sizeRecursively' root $$ DC.map sizedBytes =$= DC.fold (+) 0
166168
liftIO . T.putStrLn $ renderSizedAddress p (Sized bytes root)
167169

170+
168171
renderSizedAddress :: UnitPrefix -> Sized Address -> Text
169172
renderSizedAddress p (Sized bytes address) =
170173
let
@@ -299,7 +302,7 @@ commandP' f = XOA.subparser $
299302
(Upload <$> filepath' <*> address' <*> writeMode' f)
300303
<> command' "download"
301304
"Download a file from s3."
302-
(Download <$> address' <*> filepath')
305+
(Download <$> recursive' <*> address' <*> filepath')
303306
<> command' "copy"
304307
"Copy a file from an S3 address to another S3 address."
305308
(Copy <$> address' <*> address' <*> writeMode' f)

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

mismi-s3/test/Test/IO/Mismi/S3/Commands.hs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,25 @@ prop_read_empty k = ioProperty $ do
497497
t <- runAWSDefaultRegion . read $ Address bucket' k
498498
pure $ t === Nothing
499499

500+
prop_download_recursive :: Property
501+
prop_download_recursive = once . testAWS $ do
502+
let name1 = "first name"
503+
name2 = "second name"
504+
name3 = "third name"
505+
tmpdir <- newFilePath
506+
addr <- withKey (// Key "top") <$> newAddress
507+
writeOrFail (withKey (// Key "a") addr) name1
508+
writeOrFail (withKey (// Key "b/c") addr) name2
509+
writeOrFail (withKey (// Key "c/d/e") addr) name3
510+
511+
eitherT (fail . show) pure $ downloadRecursive addr tmpdir
512+
513+
a <- liftIO $ T.readFile (tmpdir </> "a")
514+
c <- liftIO $ T.readFile (tmpdir </> "b" </> "c")
515+
e <- liftIO $ T.readFile (tmpdir </> "c" </> "d" </> "e")
516+
517+
pure $ a === name1 .&&. c == name2 .&&. e == name3
518+
500519
----------
501520
-- HELPERS
502521
----------

0 commit comments

Comments
 (0)