diff --git a/antiope-s3.cabal b/antiope-s3.cabal
--- a/antiope-s3.cabal
+++ b/antiope-s3.cabal
@@ -1,11 +1,13 @@
--- This file has been generated from package.yaml by hpack version 0.28.2.
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.1.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: f7db32bbb25a8bc478a6baef9a6cfb9d6fd91a44bdf95b511e861ad4cfec0583
+-- hash: 887a27d58e6bcfd56a2a70265fa45a4b9aeda0ad3194e4c7e264fe8c4c445592
 
 name:           antiope-s3
-version:        6.1.5
+version:        6.2.0
 description:    Please see the README on Github at <https://github.com/arbor/antiope#readme>
 category:       Services
 homepage:       https://github.com/arbor/antiope#readme
@@ -16,27 +18,15 @@
 license:        MIT
 license-file:   LICENSE
 build-type:     Simple
-cabal-version:  >= 1.10
 extra-source-files:
-    ChangeLog.md
     README.md
+    ChangeLog.md
 
 source-repository head
   type: git
   location: https://github.com/arbor/antiope
 
 library
-  exposed-modules:
-      Antiope.S3
-      Antiope.S3.GetObject
-      Antiope.S3.Internal
-      Antiope.S3.Lazy
-      Antiope.S3.Range
-      Antiope.S3.Request
-      Antiope.S3.Strict
-      Antiope.S3.Types
-  other-modules:
-      Paths_antiope_s3
   hs-source-dirs:
       src
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
@@ -61,15 +51,22 @@
     , resourcet
     , text
     , unliftio-core
+  exposed-modules:
+      Antiope.S3
+      Antiope.S3.GetObject
+      Antiope.S3.Internal
+      Antiope.S3.Lazy
+      Antiope.S3.Range
+      Antiope.S3.Request
+      Antiope.S3.Strict
+      Antiope.S3.Types
+  other-modules:
+      Paths_antiope_s3
   default-language: Haskell2010
 
 test-suite antiope-s3-test
   type: exitcode-stdio-1.0
   main-is: Spec.hs
-  other-modules:
-      Antiope.S3.TypesSpec
-      Antiope.S3Spec
-      Paths_antiope_s3
   hs-source-dirs:
       test
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
@@ -98,4 +95,8 @@
     , resourcet
     , text
     , unliftio-core
+  other-modules:
+      Antiope.S3.TypesSpec
+      Antiope.S3Spec
+      Paths_antiope_s3
   default-language: Haskell2010
diff --git a/src/Antiope/S3.hs b/src/Antiope/S3.hs
--- a/src/Antiope/S3.hs
+++ b/src/Antiope/S3.hs
@@ -9,6 +9,9 @@
 , toS3Uri
 , lsBucketResponseStream
 , lsBucketStream
+, lsPrefix
+, deleteFiles
+, deleteFilesExcept
 , Region(..)
 , BucketName(..)
 , ObjectKey(..)
@@ -17,29 +20,32 @@
 ) where
 
 import Antiope.S3.Internal
-import Antiope.S3.Types             (S3Uri (S3Uri))
+import Antiope.S3.Types             (S3Uri (S3Uri, objectKey))
+import Conduit
 import Control.Lens
 import Control.Monad
 import Control.Monad.Trans.AWS      hiding (send)
 import Control.Monad.Trans.Resource
-import Data.Conduit
-import Data.Conduit.Combinators     as CC (concatMap)
 import Data.Conduit.List            (unfoldM)
+import Data.Maybe                   (catMaybes)
 import Data.Monoid                  ((<>))
 import Data.Text                    as T (Text, pack, unpack)
 import Network.AWS                  (MonadAWS)
-import Network.AWS.Data
 import Network.AWS.Data.Body        (_streamBody)
+import Network.AWS.Data.Text        (toText)
 import Network.AWS.S3
 import Network.URI                  (URI (..), URIAuth (..), parseURI, unEscapeString)
 
 import qualified Data.ByteString      as BS
 import qualified Data.ByteString.Lazy as LBS
+import qualified Data.List            as List
 import qualified Network.AWS          as AWS
 
 chunkSize :: ChunkSize
 chunkSize = ChunkSize (1024 * 1024)
 
+type Prefix = Text
+
 fromS3Uri :: Text -> Maybe S3Uri
 fromS3Uri uri = do
   puri <- parseURI (unpack uri)
@@ -123,4 +129,44 @@
 lsBucketStream :: MonadAWS m
   => ListObjectsV2
   -> ConduitM a Object m ()
-lsBucketStream bar = lsBucketResponseStream bar .| CC.concatMap (^. lovrsContents)
+lsBucketStream bar = lsBucketResponseStream bar .| concatMapC (^. lovrsContents)
+
+-- | Lists the specified prefix in a bucket.
+lsPrefix :: MonadAWS m
+  => BucketName
+  -> Prefix
+  -> m [S3Uri]
+lsPrefix b p =
+  runConduit $
+    lsBucketStream (listObjectsV2 b & lovPrefix ?~ p)
+    .| mapC (S3Uri b . view oKey)
+    .| sinkList
+
+-- | Deletes specified keys in a bucket.
+-- Returns a list of keys that were successfully deleted.
+--
+-- Will fail monadically (using 'fail') if the response indicates any errors.
+deleteFiles :: MonadAWS m
+  => BucketName
+  -> [ObjectKey]
+  -> m [S3Uri]
+deleteFiles b ks = do
+  let dObjs = delete' & dObjects .~ (objectIdentifier <$> ks)
+  resp <- AWS.send (deleteObjects b dObjs)
+  unless (List.null $ resp ^. drsErrors) $
+    fail (resp ^. drsErrors & show)
+  let deleted = resp ^.. drsDeleted . each . dKey & catMaybes <&> S3Uri b
+  pure deleted
+
+-- | Deletes all the keys in a specified prefix EXCEPT the specified ones.
+-- Returns a list of objects that were successfully deleted.
+deleteFilesExcept :: MonadAWS m
+  => BucketName
+  -> Prefix
+  -> [ObjectKey]
+  -> m [S3Uri]
+deleteFilesExcept b p uris = do
+  existing <- lsPrefix b p
+  case (objectKey <$> existing) List.\\ uris of
+    [] -> pure []
+    xs -> deleteFiles b xs
