Skip to content

Commit 72cd378

Browse files
committed
mismis-s3: Implement recursive upload
1 parent 327cb57 commit 72cd378

File tree

3 files changed

+70
-5
lines changed

3 files changed

+70
-5
lines changed

mismi-s3/mismi-s3.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ library
3131
, conduit-extra == 1.1.*
3232
, directory == 1.2.*
3333
, exceptions >= 0.6 && < 0.9
34+
, extra == 1.6.*
3435
, filepath >= 1.3 && < 1.5
3536
, http-client >= 0.4.18 && < 0.5
3637
, http-types == 0.8.*

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

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ module Mismi.S3.Commands (
2424
, uploadOrFail
2525
, uploadWithMode
2626
, uploadWithModeOrFail
27+
, uploadRecursive
28+
, uploadRecursiveOrFail
29+
, uploadRecursiveWithMode
30+
, uploadRecursiveWithModeOrFail
2731
, multipartUpload
2832
, uploadSingle
2933
, write
@@ -70,6 +74,7 @@ import Control.Exception (ioError)
7074
import qualified Control.Exception as CE
7175
import Control.Lens ((.~), (^.), to, view)
7276
import Control.Monad.Catch (throwM, onException)
77+
import Control.Monad.Extra (concatMapM)
7378
import Control.Monad.Trans.Class (lift)
7479
import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT)
7580
import Control.Monad.Reader (ask)
@@ -114,10 +119,10 @@ import P
114119
import System.IO (IO, IOMode (..), SeekMode (..))
115120
import System.IO (hFileSize, hSetFileSize, withFile)
116121
import System.IO.Error (IOError)
117-
import System.Directory (createDirectoryIfMissing, doesFileExist)
122+
import System.Directory (createDirectoryIfMissing, doesFileExist, getDirectoryContents)
118123
import System.FilePath (FilePath, (</>), takeDirectory)
119124
import System.Posix.IO (OpenMode(..), openFd, closeFd, fdSeek, defaultFileFlags)
120-
import System.Posix.Files (getFileStatus, isDirectory)
125+
import System.Posix.Files (getFileStatus, isDirectory, isRegularFile)
121126
import qualified "unix-bytestring" System.Posix.IO.ByteString as UBS
122127

123128
import System.Timeout.Lifted (timeout)
@@ -365,6 +370,14 @@ upload :: FilePath -> Address -> EitherT UploadError AWS ()
365370
upload =
366371
uploadWithMode Fail
367372

373+
uploadRecursive :: FilePath -> Address -> EitherT UploadError AWS ()
374+
uploadRecursive =
375+
uploadRecursiveWithMode Fail
376+
377+
uploadRecursiveOrFail :: FilePath -> Address -> AWS ()
378+
uploadRecursiveOrFail f a =
379+
eitherT hoistUploadError pure $ uploadRecursive f a
380+
368381
uploadOrFail :: FilePath -> Address -> AWS ()
369382
uploadOrFail f a =
370383
eitherT hoistUploadError pure $ upload f a
@@ -373,13 +386,19 @@ uploadWithModeOrFail :: WriteMode -> FilePath -> Address -> AWS ()
373386
uploadWithModeOrFail w f a =
374387
eitherT hoistUploadError pure $ uploadWithMode w f a
375388

389+
uploadRecursiveWithModeOrFail :: WriteMode -> FilePath -> Address -> AWS ()
390+
uploadRecursiveWithModeOrFail w f a =
391+
eitherT hoistUploadError pure $ uploadRecursiveWithMode w f a
392+
376393
hoistUploadError :: UploadError -> AWS ()
377394
hoistUploadError e =
378395
case e of
379396
UploadSourceMissing f ->
380397
throwM $ SourceFileMissing f
381398
UploadDestinationExists a ->
382399
throwM $ DestinationAlreadyExists a
400+
UploadSourceNotDirectory f ->
401+
throwM $ SourceNotDirectory f
383402
MultipartUploadError (WorkerError a) ->
384403
throwM $ a
385404
MultipartUploadError (BlowUpError a) ->
@@ -455,6 +474,45 @@ multipartUploadWorker e mpu file a (o, c, i) =
455474
pure $! Right $! PartResponse i m
456475

457476

477+
uploadRecursiveWithMode :: WriteMode -> FilePath -> Address -> EitherT UploadError AWS ()
478+
uploadRecursiveWithMode m src (Address buck ky) = do
479+
es <- tryIO $ getFileStatus src
480+
case es of
481+
Left _ -> left $ UploadSourceMissing src
482+
Right st -> unless (isDirectory st) . left $ UploadSourceNotDirectory src
483+
files <- liftIO $ listRecursivelyLocal src
484+
let prefixLen = L.length (src </> "a") - 1
485+
outputAddrs = fmap (\fp -> Address buck (ky // Key (T.pack $ L.drop prefixLen fp))) files
486+
mapM_ (uncurry (uploadWithMode m)) $ L.zip files outputAddrs
487+
488+
-- | Like `listRecursively` but for the local filesystem.
489+
listRecursivelyLocal :: MonadIO m => FilePath -> m [FilePath]
490+
listRecursivelyLocal topdir = do
491+
entries <- liftIO $ listDirectory topdir
492+
(dirs, files) <- liftIO . partitionDirsFiles $ fmap (topdir </>) entries
493+
others <- concatMapM listRecursivelyLocal dirs
494+
pure $ files <> others
495+
496+
497+
-- Not available with ghc 7.10 so copy it here.
498+
listDirectory :: FilePath -> IO [FilePath]
499+
listDirectory path =
500+
filter f <$> getDirectoryContents path
501+
where
502+
f filename =
503+
filename /= "." && filename /= ".."
504+
505+
partitionDirsFiles :: MonadIO m => [FilePath] -> m ([FilePath], [FilePath])
506+
partitionDirsFiles =
507+
pworker ([], [])
508+
where
509+
pworker (dirs, files) [] = pure (dirs, files)
510+
pworker (dirs, files) (x:xs) = do
511+
xstat <- liftIO $ getFileStatus x
512+
pworker
513+
(if isDirectory xstat then x : dirs else dirs, if isRegularFile xstat then x : files else files)
514+
xs
515+
458516
write :: Address -> Text -> AWS WriteResult
459517
write =
460518
writeWithMode Fail
@@ -645,9 +703,6 @@ downloadRecursiveWithMode mode src dest = do
645703
addrs <- lift $ listRecursively src
646704
mapM_ drWorker addrs
647705
where
648-
tryIO :: MonadIO m => IO a -> m (Either IOError a)
649-
tryIO = liftIO . CE.try
650-
651706
drWorker :: Address -> EitherT DownloadError AWS ()
652707
drWorker addr = do
653708
fpdest <- hoistMaybe (DownloadInvariant addr src) $
@@ -750,3 +805,6 @@ worker input output mode env f = runEitherT . runAWST env SyncAws $ do
750805
(liftCopy $ copyWithMode Overwrite f out)
751806
(ifM (lift $ exists out) (right ()) cp)
752807
mode
808+
809+
tryIO :: MonadIO m => IO a -> m (Either IOError a)
810+
tryIO = liftIO . CE.try

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ data S3Error =
7676
| DestinationFileExists FilePath
7777
| DestinationNotDirectory FilePath
7878
| DestinationMissing FilePath
79+
| SourceNotDirectory FilePath
7980
| AccessDenied Address
8081
| Invariant Text
8182
| Target Address Address
@@ -100,6 +101,8 @@ s3ErrorRender s3err = "[Mismi internal error] - " <> case s3err of
100101
"Expecting destination " <> T.pack f <> " to be a directory."
101102
DestinationMissing f ->
102103
"Download destination directory " <> T.pack f <> " does not exist."
104+
SourceNotDirectory f ->
105+
"Recursive upload source " <> T.pack f <> " must be a directory."
103106
DestinationDoesNotExist a ->
104107
"This address does not exist [" <> addressToText a <> "]"
105108
AccessDenied a ->
@@ -196,6 +199,7 @@ renderCopyError e =
196199
data UploadError =
197200
UploadSourceMissing FilePath
198201
| UploadDestinationExists Address
202+
| UploadSourceNotDirectory FilePath
199203
| MultipartUploadError (RunError Error)
200204
deriving Show
201205

@@ -206,6 +210,8 @@ renderUploadError e =
206210
"Can not upload when the source file does not exist [" <> T.pack f <> "]"
207211
UploadDestinationExists a ->
208212
"Can not upload when the destination object already exists [" <> addressToText a <> "]"
213+
UploadSourceNotDirectory f ->
214+
"Recursive upload source " <> T.pack f <> " must be a directory."
209215
MultipartUploadError a ->
210216
renderRunError a ((<>) "Multipart upload failed on a worker: " . renderError)
211217

0 commit comments

Comments
 (0)