diff --git a/antiope-s3.cabal b/antiope-s3.cabal
--- a/antiope-s3.cabal
+++ b/antiope-s3.cabal
@@ -1,98 +1,98 @@
-cabal-version: 2.2
+cabal-version: 2.4
 
-name:               antiope-s3
-version:            7.3.2
-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
-homepage:           https://github.com/arbor/antiope#readme
-bug-reports:        https://github.com/arbor/antiope/issues
-author:             Arbor Networks
-maintainer:         mayhem@arbor.net
-copyright:          Arbor Networks
-license:            MIT
-license-file:       LICENSE
-build-type:         Simple
-extra-source-files:
-    README.md
-    ChangeLog.md
+name:                   antiope-s3
+version:                7.3.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
+homepage:               https://github.com/arbor/antiope#readme
+bug-reports:            https://github.com/arbor/antiope/issues
+author:                 Arbor Networks
+maintainer:             mayhem@arbor.net
+copyright:              Arbor Networks
+license:                MIT
+license-file:           LICENSE
+build-type:             Simple
+extra-source-files:     README.md
+                        ChangeLog.md
 
 source-repository head
   type: git
   location: https://github.com/arbor/antiope
 
+common base     { build-depends:  base      >= 4.7    && < 5 }
+
+common config
+  default-language:     Haskell2010
+  ghc-options:          -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+
 library
-  exposed-modules:
-      Antiope.S3
-      Antiope.S3.GetObject
-      Antiope.S3.Internal
-      Antiope.S3.Lazy
-      Antiope.S3.Messages
-      Antiope.S3.Range
-      Antiope.S3.Request
-      Antiope.S3.Strict
-      Antiope.S3.Syntax
-      Antiope.S3.Types
-  hs-source-dirs: src
-  default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -msse4.2
+  import:               base, config
+  exposed-modules:      Antiope.S3
+                        Antiope.S3.GetObject
+                        Antiope.S3.Internal
+                        Antiope.S3.Lazy
+                        Antiope.S3.Messages
+                        Antiope.S3.Range
+                        Antiope.S3.Request
+                        Antiope.S3.Strict
+                        Antiope.S3.Syntax
+                        Antiope.S3.Types
+  hs-source-dirs:       src
+  default-extensions:   BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
   build-depends:
-      aeson
-    , amazonka >=1.6
-    , amazonka-core >=1.6
-    , amazonka-s3 >=1.6
-    , antiope-core
-    , antiope-messages
-    , attoparsec
-    , base >=4.7 && <5
-    , bytestring
-    , conduit
-    , conduit-extra
-    , exceptions
-    , generic-lens
-    , http-types
-    , lens
-    , mtl
-    , network-uri
-    , resourcet
-    , text
-    , time
-    , unliftio-core
-  default-language: Haskell2010
+                        aeson
+                      , amazonka              >= 1.6.1
+                      , amazonka-core         >= 1.6.1
+                      , amazonka-s3           >= 1.6.1
+                      , antiope-core
+                      , antiope-messages
+                      , attoparsec
+                      , bytestring
+                      , conduit
+                      , conduit-extra
+                      , exceptions
+                      , generic-lens
+                      , http-types
+                      , lens
+                      , mtl
+                      , network-uri
+                      , resourcet
+                      , text
+                      , time
+                      , unliftio-core
 
 test-suite antiope-s3-test
-  type: exitcode-stdio-1.0
-  main-is: Spec.hs
-  other-modules:
-      Antiope.S3.MessagesSpec
-      Antiope.S3.TypesSpec
-      Antiope.S3Spec
-  hs-source-dirs: test
-  default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -msse4.2 -threaded -rtsopts -with-rtsopts=-N
-  build-depends:
-      aeson
-    , amazonka >=1.6
-    , amazonka-core >=1.6
-    , amazonka-s3 >=1.6
-    , antiope-core
-    , antiope-s3
-    , attoparsec
-    , base >=4.7 && <5
-    , bytestring
-    , conduit
-    , conduit-extra
-    , exceptions
-    , generic-lens
-    , hedgehog >=0.5 && <0.7
-    , hspec >=2.4 && <2.7
-    , http-types
-    , hw-hspec-hedgehog >=0.1 && <0.3
-    , lens
-    , mtl
-    , network-uri
-    , resourcet
-    , text
-    , time
-    , unliftio-core
-  default-language: Haskell2010
+  import:               base, config
+  type:                 exitcode-stdio-1.0
+  main-is:              Spec.hs
+  other-modules:        Antiope.S3.MessagesSpec
+                        Antiope.S3.TypesSpec
+                        Antiope.S3Spec
+  hs-source-dirs:       test
+  default-extensions:   BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
+  ghc-options:          -threaded -rtsopts -with-rtsopts=-N
+  build-tool-depends:   hspec-discover:hspec-discover
+  build-depends:        aeson
+                      , amazonka              >= 1.6.1
+                      , amazonka-core         >= 1.6.1
+                      , amazonka-s3           >= 1.6.1
+                      , antiope-core
+                      , antiope-s3
+                      , attoparsec
+                      , bytestring
+                      , conduit
+                      , conduit-extra
+                      , exceptions
+                      , generic-lens
+                      , hedgehog              >= 0.5      && < 0.7
+                      , hspec                 >= 2.4      && < 2.7
+                      , http-types
+                      , hw-hspec-hedgehog     >= 0.1      && < 0.3
+                      , lens
+                      , mtl
+                      , network-uri
+                      , resourcet
+                      , text
+                      , time
+                      , unliftio-core
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
@@ -9,6 +9,7 @@
   , S3Uri(..)
   , readBucketName
   , readWhile
+  , dirname
   , Range(..)
   ) where
 
@@ -96,3 +97,7 @@
     bn <- readBucketName
     ok <- ObjectKey . T.pack . unEscapeString . drop 1 <$> readWhile (/= ' ')
     return (S3Uri bn ok)
+
+dirname :: S3Uri -> S3Uri
+dirname (S3Uri bucket (ObjectKey key)) = S3Uri bucket (ObjectKey newKey)
+  where newKey = T.intercalate "/" (reverse (drop 1 (dropWhile T.null (reverse (T.splitOn "/" key)))))
