diff --git a/antiope-s3.cabal b/antiope-s3.cabal
--- a/antiope-s3.cabal
+++ b/antiope-s3.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 
 name:                   antiope-s3
-version:                7.4.2
+version:                7.4.3
 synopsis:               Please see the README on Github at <https://github.com/arbor/antiope#readme>
 description:            Please see the README on Github at <https://github.com/arbor/antiope#readme>.
 category:               Services
@@ -54,6 +54,7 @@
                       , exceptions
                       , generic-lens
                       , http-types
+                      , dlist                 >= 0.8.0.7
                       , lens
                       , mtl
                       , network-uri
diff --git a/src/Antiope/S3.hs b/src/Antiope/S3.hs
--- a/src/Antiope/S3.hs
+++ b/src/Antiope/S3.hs
@@ -109,24 +109,6 @@
 
 -- Private --
 
--- Builds the request for the next page of a NextObjectsV2 request,
--- based on the original request and the most recent response.
-nextPageReq :: ListObjectsV2 -> ListObjectsV2Response -> ListObjectsV2
-nextPageReq initial resp =
-  initial & lovContinuationToken .~ resp ^. lovrsNextContinuationToken
-
--- The type signature is like this so that it can be used with `unfoldM`
-lsBucketPage :: MonadAWS m
-  => Maybe ListObjectsV2
-  -> m (Maybe (ListObjectsV2Response, Maybe ListObjectsV2))
-lsBucketPage Nothing    = pure Nothing
-lsBucketPage (Just req) = do
-  resp <- AWS.send req
-  pure . Just . (resp, ) $
-    case resp ^. lovrsIsTruncated of
-      Just True -> Just $ nextPageReq req resp
-      _         -> Nothing
-
 -- | Streams all pages of the result (ListObjectsV2Responses) of a ListObjectsV2
 -- request from S3.
 -- lsBucketResponseStream :: MonadAWS m => ListObjectsV2 -> ConduitT i ListObjectsV2Response m ()
diff --git a/src/Antiope/S3/Internal.hs b/src/Antiope/S3/Internal.hs
--- a/src/Antiope/S3/Internal.hs
+++ b/src/Antiope/S3/Internal.hs
@@ -1,10 +1,32 @@
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TupleSections     #-}
 
 module Antiope.S3.Internal where
 
+import Control.Lens
 import Data.Monoid    ((<>))
 import Data.Text      (Text)
-import Network.AWS.S3 (BucketName (..), ObjectKey (..))
+import Network.AWS    (MonadAWS)
+import Network.AWS.S3
 
+import qualified Network.AWS as AWS
+
 toS3Uri :: BucketName -> ObjectKey -> Text
 toS3Uri (BucketName b) (ObjectKey k) = "s3://" <> b <> "/" <> k
+
+-- Builds the request for the next page of a NextObjectsV2 request,
+-- based on the original request and the most recent response.
+nextPageReq :: ListObjectsV2 -> ListObjectsV2Response -> ListObjectsV2
+nextPageReq initial resp = initial & lovContinuationToken .~ resp ^. lovrsNextContinuationToken
+
+-- The type signature is like this so that it can be used with `unfoldM`
+lsBucketPage :: MonadAWS m
+  => Maybe ListObjectsV2
+  -> m (Maybe (ListObjectsV2Response, Maybe ListObjectsV2))
+lsBucketPage Nothing    = pure Nothing
+lsBucketPage (Just req) = do
+  resp <- AWS.send req
+  pure . Just . (resp, ) $
+    case resp ^. lovrsIsTruncated of
+      Just True -> Just $ nextPageReq req resp
+      _         -> Nothing
diff --git a/src/Antiope/S3/Lazy.hs b/src/Antiope/S3/Lazy.hs
--- a/src/Antiope/S3/Lazy.hs
+++ b/src/Antiope/S3/Lazy.hs
@@ -1,4 +1,6 @@
+{-# LANGUAGE DataKinds           #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications    #-}
 
 module Antiope.S3.Lazy
   ( unsafeDownload
@@ -6,18 +8,27 @@
   , download
   , downloadRequest
   , downloadFromS3Uri
+  , listObjectsV2
+  , listS3Uris
+  , dlistObjectsV2
+  , dlistS3Uris
+  , s3UriToListObjectsV2
   ) where
 
 import Antiope.Core.Error
 import Antiope.S3.GetObject
-import Antiope.S3.Types             (S3Uri (S3Uri))
+import Antiope.S3.Types             (S3Uri (S3Uri), s3UriToListObjectsV2)
 import Control.Lens
+import Control.Monad.IO.Unlift
 import Control.Monad.Trans.Resource
 import Network.AWS                  (MonadAWS)
 
+import qualified Antiope.S3.Internal  as I
 import qualified Data.ByteString.Lazy as LBS
+import qualified Data.DList           as DL
 import qualified Network.AWS          as AWS
 import qualified Network.AWS.S3       as AWS
+import qualified System.IO.Unsafe     as IO
 
 unsafeDownloadRequest :: (MonadAWS m, MonadResource m)
   => AWS.GetObject
@@ -47,3 +58,34 @@
   => S3Uri
   -> m (Maybe LBS.ByteString)
 downloadFromS3Uri (S3Uri b k) = download b k
+
+dlistObjectsV2 :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
+  => AWS.ListObjectsV2
+  -> m (DL.DList AWS.ListObjectsV2Response)
+dlistObjectsV2 req = do
+  f <- askUnliftIO
+  r <- AWS.send req
+  case r ^. AWS.lovrsIsTruncated of
+    Just True -> do
+      rs <- liftIO $ IO.unsafeInterleaveIO (unliftIO f (dlistObjectsV2 (I.nextPageReq req r)))
+      return (DL.cons r rs)
+    _ -> return (DL.singleton r)
+
+dlistS3Uris :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
+  => AWS.ListObjectsV2
+  -> m (DL.DList S3Uri)
+dlistS3Uris req = dlistObjectsV2 req >>= toS3Uris
+  where toS3Uris responses  = return (responses >>= toS3Uris')
+        toS3Uris' response  = do
+          c <- response ^. AWS.lovrsContents & DL.fromList
+          response ^.. AWS.lovrsName . _Just & DL.fromList <&> \bucketName -> S3Uri bucketName (c ^. AWS.oKey)
+
+listObjectsV2 :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
+  => AWS.ListObjectsV2
+  -> m [AWS.ListObjectsV2Response]
+listObjectsV2 req = DL.toList <$> dlistObjectsV2 req
+
+listS3Uris :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
+  => AWS.ListObjectsV2
+  -> m [S3Uri]
+listS3Uris req = DL.toList <$> dlistS3Uris req
diff --git a/src/Antiope/S3/Types.hs b/src/Antiope/S3/Types.hs
--- a/src/Antiope/S3/Types.hs
+++ b/src/Antiope/S3/Types.hs
@@ -12,6 +12,7 @@
   , readWhile
   , dirname
   , Range(..)
+  , s3UriToListObjectsV2
   ) where
 
 import Antiope.S3.Internal
@@ -33,6 +34,7 @@
 import qualified Data.Attoparsec.Combinator      as DAC
 import qualified Data.Attoparsec.Text            as DAT
 import qualified Data.Text                       as T
+import qualified Network.AWS.S3                  as AWS
 import qualified Network.AWS.S3.Types            as X
 import qualified Text.ParserCombinators.ReadPrec as RP
 
@@ -102,3 +104,7 @@
 dirname :: S3Uri -> S3Uri
 dirname (S3Uri bk (ObjectKey key)) = S3Uri bk (ObjectKey newKey)
   where newKey = T.intercalate "/" (reverse (drop 1 (dropWhile T.null (reverse (T.splitOn "/" key)))))
+
+s3UriToListObjectsV2 :: S3Uri -> AWS.ListObjectsV2
+s3UriToListObjectsV2 s3Uri = AWS.listObjectsV2 (s3Uri ^. the @"bucket")
+  & AWS.lovPrefix ?~ (s3Uri ^. the @"objectKey" . the @1)
diff --git a/test/Antiope/S3/MessagesSpec.hs b/test/Antiope/S3/MessagesSpec.hs
--- a/test/Antiope/S3/MessagesSpec.hs
+++ b/test/Antiope/S3/MessagesSpec.hs
@@ -64,5 +64,3 @@
   it "Can encode and decode S3Message" $ require $ property $ do
     msg <- forAll $ s3Message
     tripping msg encode decode
-
-
