do-spaces 0.1.0 → 0.2
raw patch · 33 files changed
+850/−1041 lines, 33 filesdep +http-api-datadep +unliftiodep ~cryptonitedep ~http-client-tlsdep ~memoryPVP ok
version bump matches the API change (PVP)
Dependencies added: http-api-data, unliftio
Dependency ranges changed: cryptonite, http-client-tls, memory, microlens, microlens-ghc, mime-types, mtl, time, transformers, xml-conduit
API changes (from Hackage documentation)
- Network.DO.Spaces: APIException :: Status -> Text -> RequestID -> Text -> APIException
- Network.DO.Spaces: Grant :: Permission -> Grantee -> Grant
- Network.DO.Spaces: [$sel:code:APIException] :: APIException -> Text
- Network.DO.Spaces: [$sel:grantee:Grant] :: Grant -> Grantee
- Network.DO.Spaces: [$sel:hostID:APIException] :: APIException -> Text
- Network.DO.Spaces: [$sel:permission:Grant] :: Grant -> Permission
- Network.DO.Spaces: [$sel:requestID:APIException] :: APIException -> RequestID
- Network.DO.Spaces: [$sel:status:APIException] :: APIException -> Status
- Network.DO.Spaces.Actions.DeleteBucket: data DeleteBucket
- Network.DO.Spaces.Actions.DeleteBucketCORS: data DeleteBucketCORS
- Network.DO.Spaces.Actions.DeleteBucketLifecycle: data DeleteBucketLifecycle
- Network.DO.Spaces.Actions.GetBucketACLs: data GetBucketACLs
- Network.DO.Spaces.Actions.GetBucketCORS: data GetBucketCORS
- Network.DO.Spaces.Actions.GetBucketCORS: data GetBucketCORSResponse
- Network.DO.Spaces.Actions.GetBucketLifecycle: data GetBucketLifecycle
- Network.DO.Spaces.Actions.GetBucketLifecycle: data GetBucketLifecycleResponse
- Network.DO.Spaces.Actions.GetBucketLocation: data GetBucketLocation
- Network.DO.Spaces.Actions.GetBucketLocation: data GetBucketLocationResponse
- Network.DO.Spaces.Types: [$sel:id':LifecycleRule] :: LifecycleRule -> LifecycleID
- Network.DO.Spaces.Types: [$sel:id':Owner] :: Owner -> OwnerID
- Network.DO.Spaces.Types: [$sel:unBucket:Bucket] :: Bucket -> Text
- Network.DO.Spaces.Types: [$sel:unCanonicalized:Canonicalized] :: Canonicalized a -> ByteString
- Network.DO.Spaces.Types: [$sel:unObject:Object] :: Object -> Text
- Network.DO.Spaces.Types: [$sel:unOwnerID:OwnerID] :: OwnerID -> Int
+ Network.DO.Spaces: Discover :: CredentialSource
+ Network.DO.Spaces: InvalidResponse :: Text -> ClientException
+ Network.DO.Spaces.Actions.DeleteBucket: newtype DeleteBucket
+ Network.DO.Spaces.Actions.DeleteBucketCORS: newtype DeleteBucketCORS
+ Network.DO.Spaces.Actions.DeleteBucketLifecycle: newtype DeleteBucketLifecycle
+ Network.DO.Spaces.Actions.GetBucketACLs: newtype GetBucketACLs
+ Network.DO.Spaces.Actions.GetBucketCORS: newtype GetBucketCORS
+ Network.DO.Spaces.Actions.GetBucketCORS: newtype GetBucketCORSResponse
+ Network.DO.Spaces.Actions.GetBucketLifecycle: newtype GetBucketLifecycle
+ Network.DO.Spaces.Actions.GetBucketLifecycle: newtype GetBucketLifecycleResponse
+ Network.DO.Spaces.Actions.GetBucketLocation: newtype GetBucketLocation
+ Network.DO.Spaces.Actions.GetBucketLocation: newtype GetBucketLocationResponse
+ Network.DO.Spaces.Types: Discover :: CredentialSource
+ Network.DO.Spaces.Types: InvalidResponse :: Text -> ClientException
+ Network.DO.Spaces.Types: [$sel:lifecycleID:LifecycleRule] :: LifecycleRule -> LifecycleID
+ Network.DO.Spaces.Types: [$sel:ownerID:Owner] :: Owner -> OwnerID
+ Network.DO.Spaces.Types: instance Web.Internal.HttpApiData.ToHttpApiData Network.DO.Spaces.Types.Bucket
+ Network.DO.Spaces.Types: instance Web.Internal.HttpApiData.ToHttpApiData Network.DO.Spaces.Types.Object
+ Network.DO.Spaces.Types: instance Web.Internal.HttpApiData.ToHttpApiData Network.DO.Spaces.Types.OwnerID
+ Network.DO.Spaces.Types: type RequestID = Text
- Network.DO.Spaces: Explicit :: AccessKey -> SecretKey -> CredentialSource
+ Network.DO.Spaces: Explicit :: Region -> AccessKey -> SecretKey -> CredentialSource
- Network.DO.Spaces: FromEnv :: Maybe (Text, Text) -> CredentialSource
+ Network.DO.Spaces: FromEnv :: Maybe (Text, Text, Text) -> CredentialSource
- Network.DO.Spaces: newSpaces :: (MonadThrow m, MonadIO m) => Region -> CredentialSource -> m Spaces
+ Network.DO.Spaces: newSpaces :: (MonadThrow m, MonadIO m) => CredentialSource -> m Spaces
- Network.DO.Spaces.Types: Explicit :: AccessKey -> SecretKey -> CredentialSource
+ Network.DO.Spaces.Types: Explicit :: Region -> AccessKey -> SecretKey -> CredentialSource
- Network.DO.Spaces.Types: FromEnv :: Maybe (Text, Text) -> CredentialSource
+ Network.DO.Spaces.Types: FromEnv :: Maybe (Text, Text, Text) -> CredentialSource
- Network.DO.Spaces.Types: [Authorization] :: ByteString -> Computed 'Auth
+ Network.DO.Spaces.Types: [Authorization] :: ByteString -> Computed Auth
- Network.DO.Spaces.Types: [Credentials] :: ByteString -> Computed 'Cred
+ Network.DO.Spaces.Types: [Credentials] :: ByteString -> Computed Cred
- Network.DO.Spaces.Types: [Hashed] :: ByteString -> Computed 'Hash
+ Network.DO.Spaces.Types: [Hashed] :: ByteString -> Computed Hash
- Network.DO.Spaces.Types: [Signature] :: ByteString -> Computed 'Sig
+ Network.DO.Spaces.Types: [Signature] :: ByteString -> Computed Sig
- Network.DO.Spaces.Types: [StringToSign] :: ByteString -> Computed 'StrToSign
+ Network.DO.Spaces.Types: [StringToSign] :: ByteString -> Computed StrToSign
- Network.DO.Spaces.Utils: slugToRegion :: MonadThrow m => Text -> m Region
+ Network.DO.Spaces.Utils: slugToRegion :: (MonadThrow m, IsString a, Eq a) => a -> m Region
Files
- CHANGELOG.org +6/−1
- README.org +18/−30
- do-spaces.cabal +36/−34
- io-tests/Main.hs +86/−147
- io-tests/creds.conf +0/−4
- io-tests/creds.conf.skel +0/−4
- src/Network/DO/Spaces.hs +101/−85
- src/Network/DO/Spaces/Actions.hs +2/−2
- src/Network/DO/Spaces/Actions/CopyObject.hs +17/−30
- src/Network/DO/Spaces/Actions/CreateBucket.hs +15/−21
- src/Network/DO/Spaces/Actions/DeleteBucket.hs +17/−20
- src/Network/DO/Spaces/Actions/DeleteBucketCORS.hs +21/−24
- src/Network/DO/Spaces/Actions/DeleteBucketLifecycle.hs +21/−24
- src/Network/DO/Spaces/Actions/DeleteObject.hs +14/−20
- src/Network/DO/Spaces/Actions/GetBucketACLs.hs +21/−24
- src/Network/DO/Spaces/Actions/GetBucketCORS.hs +25/−27
- src/Network/DO/Spaces/Actions/GetBucketLifecycle.hs +27/−37
- src/Network/DO/Spaces/Actions/GetBucketLocation.hs +24/−29
- src/Network/DO/Spaces/Actions/GetObject.hs +15/−22
- src/Network/DO/Spaces/Actions/GetObjectACLs.hs +18/−24
- src/Network/DO/Spaces/Actions/GetObjectInfo.hs +14/−21
- src/Network/DO/Spaces/Actions/ListAllBuckets.hs +16/−27
- src/Network/DO/Spaces/Actions/ListBucket.hs +19/−34
- src/Network/DO/Spaces/Actions/SetBucketACLs.hs +19/−26
- src/Network/DO/Spaces/Actions/SetBucketCORS.hs +19/−27
- src/Network/DO/Spaces/Actions/SetBucketLifecycle.hs +19/−27
- src/Network/DO/Spaces/Actions/SetObjectACLs.hs +20/−27
- src/Network/DO/Spaces/Actions/UploadMultipart.hs +94/−114
- src/Network/DO/Spaces/Actions/UploadObject.hs +15/−29
- src/Network/DO/Spaces/Request.hs +3/−2
- src/Network/DO/Spaces/Types.hs +99/−67
- src/Network/DO/Spaces/Utils.hs +24/−24
- tests/Main.hs +5/−7
CHANGELOG.org view
@@ -1,4 +1,9 @@ #+TITLE: Changelog +* v0.2++ ~newSpaces~ no longer directly takes a ~Region~ argument. The region is now instead loaded along with the credentials. See the haddocks for more details++ ~Discover~ was added to ~CredentialSource~ to try credential files first, falling back on environment variables++ ~Owner(id')~ was renamed to ~ownerID~, ~LifeCycleRule(id')~ to ~lifecyleID~+ * v0.1-Initial release++ Initial release
README.org view
@@ -9,13 +9,13 @@ - bucket CORS configuration * Usage-~Network.DO.Spaces~ exposes the types and actions requires to make transactions through the Spaces API. The first step is to configure a client by supplying your credentials and a region:+~Network.DO.Spaces~ exposes the types and actions requires to make transactions through the Spaces API. The first step is to configure a client: #+begin_src haskell someTransaction = do- mySpaces <- newSpaces NewYork (FromFile "~/.spaces-secrets" Nothing)+ mySpaces <- newSpaces $ FromFile "~/.spaces-secrets" Nothing runSpaces mySpaces $ ... #+end_src-There are a few options for supplying your credentials. See ~newSpaces~ in ~Network.DO.Spaces~ for more details.+There are a few options for supplying your credentials and region. See ~newSpaces~ in ~Network.DO.Spaces~ for more details. ~Network.DO.Spaces~ exposes several convenience actions for common transactions. These are simple wrappers around instances of the ~Action~ typeclass. Each of the actions must be run via ~runSpaces~ with a ~Spaces~ client configuration. If more granular control is required, especially in the case of actions which take several optional parameters (e.g., ~ListBucket~), you can directly import the ~Action~ instance, construct it, and run it with ~runAction~. These are available from the ~Network.DO.Spaces.Actions~ module. @@ -24,44 +24,32 @@ A small example: #+begin_src haskell-{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeApplications #-} - -import Data.Sequence ( Seq )-import Data.Generics.Product ( HasField(field) )+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE TypeApplications #-}++import Control.Monad.IO.Class+import Data.Sequence ( Seq )+import Data.Generics.Labels () import Lens.Micro -- or whatever compatible lens library you fancy import Network.DO.Spaces -myObjects :: IO (Seq ObjectInfo)+myObjects :: MonadIO m => m (Seq ObjectInfo) myObjects = do- -- Load your credentials configuration from environment variables- spaces <- newSpaces Singapore (FromEnv Nothing)- -- Use the @mkBucket@ smart constructor, which will validate the name you provide- -- and ensure it conforms to Spaces API specifications- bucket <- mkBucket "my-real-bucket"+ -- Load your region and credentials by first trying to read from+ -- $XDG_CONFIG_HOME/do-spaces/credentials or ~/.aws/credentials,+ -- falling back on environment variables if neither exist+ spaces <- newSpaces Discover -- List the contents of your bucket. @listBucket@ will list all entries until the -- Spaces limit of 1,000. There are several variants of this action in- -- "Network.DO.Spaces" with different options - response <- runSpaces spaces $ listBucket bucket+ -- "Network.DO.Spaces" with different options+ response <- runSpaces spaces $ listBucket =<< mkBucket "my-real-bucket" -- See the note about accessing record fields below- return $ response ^. field @"result" . field @"objects"+ pure $ response ^. #result . #objects #+end_src ** A note about records-Many transactions available through the Spaces API feature the same parameters. This necessitates dealing with duplicate fields in records that represent these transactions. For example, nearly every ~Action~ instance has a ~bucket~ field. I am not a fan of certain approaches to dealing with this problem: I am /highly/ averse to using pseudo-Hungarian Notation as record field prefixes, and I do not like using the leading-underscore name mangling approach in lenses. My personal preference is using the ~generic-lens~ package, and the library uses this internally along with ~DuplicateRecordFields~ etc.... There are a few options available when you use this library:--*** Use ~generic-lens~ with ~OverloadedLabels~-This is my personal preference. Note that I did *not* use the overloaded labels in the library itself, due to the (necessary) orphan ~IsLabel~ instance in ~generic-lens~. If you'd like to see an example of this, refer to the ~io-tests~ directory in this repository.--*** Use ~generic-lens~ with ~field~-This is the approach that the library uses internally, along with punning and wildcards; requires ~DataKinds~ and ~TypeApplications~ to use ~field~.--*** Use ~getField~-You can import ~getField~ from ~GHC.Records~ and use that to access record fields. Unfortunately, ~setField~ still hasn't been implemented in GHC.--*** Use the record selectors-Record selectors can be accessed by importing the relevant module directly, as they are not exported from ~Network.DO.Spaces~. You might want to import different types qualified, due to the aforementioned proliferation of record field duplication.+Many transactions available through the Spaces API feature the same parameters. This necessitates dealing with duplicate fields in records that represent these transactions. For example, nearly every ~Action~ instance has a ~bucket~ field. I have used ~DuplicateRecordFields~ throughout this library and have chosen not to export field selectors by default. Record selectors can be accessed by importing the relevant module directly, as they are not exported from ~Network.DO.Spaces~. You might want to import different types qualified, due to the aforementioned proliferation of record field duplication. My personal preference is to use the ~generic-lens~ package. * But why? Spaces is nominally compatible with existing Amazon S3 SDKs and clients, so it might seem redundant to create a dedicated client library for the service. DigitalOcean's endpoints and regions are different from Amazon's, however, requiring a stringly-typed region when configuring clients. True to form, Amazon client libraries written in Haskell (e.g., ~Amazonka~) tend to use an ADT to represent regions, precluding their use with Spaces. Furthermore, using an entire Amazon client might be overkill for an application that only requires the s3 functionality, unnecessarily requiring a heavyweight dependency.
do-spaces.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: do-spaces-version: 0.1.0+version: 0.2 synopsis: DigitalOcean Spaces API bindings description: See the README at https://gitlab.com/ngua/do-spaces-hs license: BSD-3-Clause@@ -12,13 +12,12 @@ bug-reports: https://gitlab.com/ngua/do-spaces-hs/-/issues tested-with: GHC ==8.8.4 || ==8.10.4 category: Network-extra-source-files:- README.org+extra-doc-files: CHANGELOG.org- tests/data/*.xml- io-tests/creds.conf- io-tests/creds.conf.skel+ README.org +extra-source-files: tests/data/*.xml+ source-repository head type: git location: https://gitlab.com/ngua/do-spaces-hs.git@@ -27,6 +26,7 @@ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints+ -Wmissing-deriving-strategies common common-extensions default-extensions: OverloadedStrings@@ -38,30 +38,32 @@ library import: common-options, common-extensions build-depends:- , base >=4.13 && <5+ , base >=4.13 && <5 , base16-bytestring , bytestring >=0.9- , case-insensitive >=1.0 && <1.3+ , case-insensitive >=1.0 && <1.3 , conduit ^>=1.3 , conduit-extra ^>=1.3 , config-ini ^>=0.2 , containers ^>=0.6- , cryptonite >=0.25 && <0.29+ , cryptonite >=0.25 && <0.30 , exceptions ^>=0.10- , extra >=1.6 && <1.8+ , extra >=1.6 && <1.8 , filepath ^>=1.4- , generic-lens >=1.1 && <2.2- , http-client-tls ^>=0.3.5+ , generic-lens >=1.1 && <2.2+ , http-api-data ^>=0.4+ , http-client-tls ^>=0.3 , http-conduit ^>=2.3 , http-types ^>=0.12- , memory >=0.14 && <0.16- , microlens >=0.4.10 && <0.4.13- , mime-types >=0.1.0.7 && <0.1.1- , mtl ^>=2.2+ , memory >=0.14 && <0.17+ , microlens ^>=0.4+ , mime-types ^>=0.1+ , mtl >=2.2 , text ^>=1.2- , time >=1.8 && <1.12- , transformers ^>=0.5- , xml-conduit >=1.8.0.1 && <1.10+ , time >=1.8 && <1.13+ , transformers >=0.5+ , unliftio ^>=0.2+ , xml-conduit >=1.8 && <1.10 exposed-modules: Network.DO.Spaces@@ -99,23 +101,23 @@ type: exitcode-stdio-1.0 hs-source-dirs: tests build-depends:- , base >=4.13 && <5+ , base >=4.13 && <5 , bytestring >=0.9- , case-insensitive >=1.0 && <1.3+ , case-insensitive >=1.0 && <1.3 , conduit ^>=1.3 , conduit-extra ^>=1.3 , containers ^>=0.6 , do-spaces- , generic-lens >=1.1 && <2.2- , hspec >=2.0 && <3.0- , http-client-tls ^>=0.3.5+ , generic-lens >=1.1 && <2.2+ , hspec >=2.0 && <3.0+ , http-client-tls ^>=0.3 , http-conduit ^>=2.3 , http-types ^>=0.12- , microlens >=0.4.10 && <0.4.13- , mtl ^>=2.2+ , microlens ^>=0.4+ , mtl >=2.2 , resourcet ^>=1.2 , text ^>=1.2- , time >=1.8 && <1.12+ , time >=1.8 && <1.13 main-is: Main.hs default-language: Haskell2010@@ -129,20 +131,20 @@ type: exitcode-stdio-1.0 hs-source-dirs: io-tests build-depends:- , base >=4.13 && <5+ , base >=4.13 && <5 , bytestring >=0.9 , conduit ^>=1.3 , conduit-extra ^>=1.3 , do-spaces , exceptions ^>=0.10- , extra >=1.6 && <1.8- , generic-lens >=1.1 && <2.2- , hspec >=2.0 && <3.0+ , extra >=1.6 && <1.8+ , generic-lens >=1.1 && <2.2+ , hspec >=2.0 && <3.0 , http-types ^>=0.12- , microlens >=0.4.10 && <0.4.13- , microlens-ghc >=0.4.10 && <0.4.13+ , microlens ^>=0.4+ , microlens-ghc ^>=0.4 , text ^>=1.2- , time >=1.8 && <1.12+ , time >=1.8 && <1.13 main-is: Main.hs default-language: Haskell2010
io-tests/Main.hs view
@@ -1,9 +1,11 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -- |--- WARNING+-- __WARNING__ -- Run at your own risk! -- -- Running the tests in this module requires an active Digital Ocean spaces@@ -13,86 +15,65 @@ -- * the application of rate-limiting or other limits to your account -- * overage in data transfer or concurrent active spaces, resulting in -- charges or penalties--- * the creation of extraneous spaces/buckets in your subscription+-- * the creation of extraneous spaces\/buckets in your subscription -- * data loss or corruption ----- To run:--- * copy ./creds.conf.skel to ./creds.conf--- * replace the dummy values with your active Spaces access and--- secret keys, a region slug, and the name of an existing bucket--- to use for certain object CRUD operations--- * do not add any additional fields, whitespace, quotes, etc... or--- parsing the credentials file will fail--- * run @cabal test@ with @-f test-io@+-- The author of this package does not take responsibility for any of the above+-- or any other damages not is explicitly mentioned here. --+-- Before running, make sure that the 'Discover' credentials source will+-- succeed. In addition, make sure to export the @SPACES_DEFAULT_BUCKET@+-- environment variable. This should correspond to a bucket name that will+-- be used to run some of the tests+-- -- The tests will create buckets and attempt to delete them. In the event -- that bucket deletion fails, you will have to clean them up manually.--- Similarly, the default bucket provided in the "bucket" field of the--- credentials file will be used to test certain object CRUD functionality.--- Although the tests will attempt to delete any objects created, you may--- need to delete them manually if this fails+-- Similarly, the default bucket as exported by @SPACES_DEFAULT_BUCKET@+-- will be used to test certain object CRUD functionality. Although the tests+-- will attempt to delete any objects created, you may need to delete them+-- manually if cleanup fails -- module Main where import Conduit- ( (.|)- , runConduitRes- , sinkList- , sourceFile- , sourceLazy- ) import Control.Exception ( bracket, throwIO ) import Control.Monad ( void )-import Control.Monad.Catch ( MonadCatch(catch)- , MonadThrow(throwM)- )-import Control.Monad.IO.Class ( MonadIO(liftIO) )+import Control.Monad.Catch ( MonadCatch(catch) ) -import qualified Data.ByteString.Char8 as C import qualified Data.ByteString.Lazy.Char8 as LC-import Data.Coerce ( coerce )-import qualified Data.Conduit.Binary as CB import Data.Generics.Labels () import qualified Data.Text as T import Data.Text ( Text )-import qualified Data.Text.Encoding as T import Data.Time.Clock.POSIX ( getPOSIXTime ) import Lens.Micro import Lens.Micro.GHC () import Network.DO.Spaces-import Network.DO.Spaces.Utils ( slugToRegion ) import qualified Network.HTTP.Types as H +import System.Environment ( lookupEnv ) import System.Time.Extra ( sleep ) import Test.Hspec main :: IO ()-main = sequence_ [ bucketCreateDelete- , bucketActions- , objectCreateDelete- , objectActions- , multipart- ]+main = hspec $ sequence_ [ objectActionSpec, multipartSpec, bucketSpec ] -objectActions :: IO ()-objectActions = do- (bucket, spaces) <- readConf- hspec . around withTestObject $ do- describe "Network.DO.Spaces.Actions.GetObjectInfo"- . it "retrieves object information"- $ \object -> do- info <- retry404 20 . runSpaces spaces- $ getObjectInfo bucket object- (info ^. #result . #contentType) `shouldBe` "text/plain"- (info ^. #result . #contentLength) `shouldBe` 18+{- HLINT ignore "Redundant do" -}+objectActionSpec :: Spec+objectActionSpec = around withTestObject $ do+ describe "Network.DO.Spaces.Actions.GetObjectInfo" $ do+ it "retrieves object information" $ \(bucket, spaces, object) -> do+ info <- retry404 20 . runSpaces spaces+ $ getObjectInfo bucket object+ info ^. (#result . #contentType) `shouldBe` "text/plain"+ info ^. (#result . #contentLength) `shouldBe` 18 - describe "Network.DO.Spaces.Actions.CopyObject"- . it "copies an existing object to a new object"- $ \object -> do+ describe "Network.DO.Spaces.Actions.CopyObject" $ do+ it "copies an existing object to a new object"+ $ \(bucket, spaces, object) -> do destObject <- nameWithEpoch mkObject "test-object-copy-" copied <- runSpaces spaces $ copyObjectWithin bucket object destObject@@ -101,117 +82,92 @@ $ deleteObject bucket destObject getStatus deleted `shouldBe` Just 204 - describe "Network.DO.Spaces.Actions.GetObject"- . it "retrieves object data"- $ \object -> do- gotten <- runSpaces spaces $ getObject bucket object- (gotten ^. #result . #objectData)- `shouldBe` "hello from haskell"+ describe "Network.DO.Spaces.Actions.GetObject" $ do+ it "retrieves object data" $ \(bucket, spaces, object) -> do+ gotten <- runSpaces spaces $ getObject bucket object+ gotten ^. (#result . #objectData) `shouldBe` "hello from haskell" where withTestObject = bracket uploadTestObject deleteTestObject uploadTestObject = do- (bucket, spaces) <- readConf+ (bucket, spaces) <- getCreds object <- nameWithEpoch mkObject "test-object-" void . runSpaces spaces $ uploadObject (Just "text/plain") bucket object body- return object+ pure (bucket, spaces, object) where body = sourceLazy "hello from haskell" - deleteTestObject object = do- (bucket, spaces) <- readConf+ deleteTestObject (bucket, spaces, object) = do void . runSpaces spaces $ deleteObject bucket object -multipart :: IO ()-multipart = do- (bucket, spaces) <- readConf- object <- nameWithEpoch mkObject "test-multipart-"- hspec- . after_ (cleanup spaces bucket object)- . describe "Network.DO.Spaces.multipartObject"- . it "uploads a multipart object"- $ do- mp <- runSpaces spaces- $ multipartObject Nothing bucket object 5242880 body- getStatus mp `shouldBe` Just 200- (mp ^. #result . #object) `shouldBe` object+multipartSpec :: Spec+multipartSpec = do+ around withMultipart $ do+ describe "Network.DO.Spaces.multipartObject" $ do+ it "uploads a multipart object" $ \(bucket, spaces, object) -> do+ mp <- runSpaces spaces+ $ multipartObject Nothing bucket object 5242880 body+ getStatus mp `shouldBe` Just 200+ mp ^. (#result . #object) `shouldBe` object where- cleanup spaces bucket object =- void . retry404 20 . runSpaces spaces $ deleteObject bucket object+ withMultipart = bracket uploadTestObject cleanup - body = sourceLazy $ LC.replicate 10485760 'f'+ uploadTestObject = do+ (bucket, spaces) <- getCreds+ object <- nameWithEpoch mkObject "test-multipart-"+ pure (bucket, spaces, object) -objectCreateDelete :: IO ()-objectCreateDelete = do- (bucket, spaces) <- readConf- object <- nameWithEpoch mkObject "test-object-"- hspec- . context "Network.DO.Spaces.Actions Object CRUD"- . it "creates, reads, and deletes a new Object"- $ do- created- <- runSpaces spaces $ uploadObject Nothing bucket object body- getStatus created `shouldBe` Just 200+ cleanup (bucket, spaces, object) =+ void . retry404 20 . runSpaces spaces $ deleteObject bucket object - deleted- <- retry404 20 . runSpaces spaces $ deleteObject bucket object- getStatus deleted `shouldBe` Just 204- (deleted ^. #result) `shouldBe` ()- where- body = sourceLazy "hello from haskell"+ body = sourceLazy $ LC.replicate 10485760 'f' -bucketActions :: IO ()-bucketActions = do- (bucket, spaces) <- readConf- hspec $ do- describe "Network.DO.Spaces.Actions.GetBucketLocation"- . it "retrieves a bucket's location"- $ do- location <- runSpaces spaces $ getBucketLocation bucket- getStatus location `shouldBe` Just 200- (location ^. #result . #locationConstraint)- `shouldBe` (spaces ^. #region)+bucketSpec :: Spec+bucketSpec = beforeAll getCreds $ do+ describe "Network.DO.Spaces.Actions.GetBucketLocation" $ do+ it "retrieves a bucket's location" $ \(bucket, spaces) -> do+ location <- runSpaces spaces $ getBucketLocation bucket+ getStatus location `shouldBe` Just 200+ location+ ^. (#result . #locationConstraint)+ `shouldBe` --+ spaces+ ^. #region - describe "Network.DO.Spaces.Actions.ListAllBuckets"- . it "lists account owner's buckets"- $ do- allBuckets <- runSpaces spaces listAllBuckets- (allBuckets ^.. #result . #buckets . each . #name)- `shouldContain` [ bucket ]+ describe "Network.DO.Spaces.Actions.ListAllBuckets" $ do+ it "lists account owner's buckets" $ \(bucket, spaces) -> do+ allBuckets <- runSpaces spaces listAllBuckets+ allBuckets+ ^.. (#result . #buckets . each . #name)+ `shouldContain` [ bucket ] -bucketCreateDelete :: IO ()-bucketCreateDelete = do- (_, spaces) <- readConf- bucket <- nameWithEpoch mkBucket "do-spaces-test-"- hspec- . context "Network.DO.Spaces.Actions Bucket CRUD"- . it "creates, reads, and deletes a new bucket"- $ do+ context "Network.DO.Spaces.Actions Bucket CRUD" $ do+ it "creates, reads, and deletes a new bucket" $ \(_, spaces) -> do+ bucket <- nameWithEpoch mkBucket "do-spaces-test-" created <- runSpaces spaces $ createBucket bucket Nothing Nothing getStatus created `shouldBe` Just 200- (created ^. #result) `shouldBe` ()-+ created ^. #result `shouldBe` () deleted <- retry404 20 . runSpaces spaces $ deleteBucket bucket getStatus deleted `shouldBe` Just 204- (deleted ^. #result) `shouldBe` ()+ deleted ^. #result `shouldBe` () --- A crude (yet effective) mechanism to ensure that 404s are retried (up to a--- limit), necessary when creating a new Bucket for testing+-- A crude mechanism to ensure that 404s are retried (up to a limit), necessary+-- when creating a new @Bucket@ for testing retry404 :: (MonadCatch m, MonadIO m) => Int -> m (SpacesResponse a) -> m (SpacesResponse a) retry404 maxRetries action = loop 0 where- loop n = catch @_ @ClientException action (catch404 n)+ loop !n = catch @_ @ClientException action $ catch404 n - catch404 n e@(HTTPStatus s _)+ catch404 !n e@(HTTPStatus s _) | H.statusCode s == 404, n < maxRetries = liftIO (sleep 1) >> loop (n + 1) | otherwise = throwM e - catch404 _ e = throwM e+ catch404 _ e = throwM e -- Makes a Bucket or Object name by appending the current epoch time to a base name; -- this provides some mechanism to avoid name clashes when creating new buckets,@@ -222,28 +178,11 @@ nameWithEpoch f base = f . (base <>) . T.pack . show @Integer . round =<< liftIO getPOSIXTime -readConf :: IO (Bucket, Spaces)-readConf = do- contents <- runConduitRes- $ sourceFile "./io-tests/creds.conf" .| CB.lines .| sinkList- case getVal <$> contents of- [ a, s, r, b ] -> do- let access = coerce a- secret = coerce s- region <- slugToRegion $ T.decodeUtf8 r- bucket <- mkBucket $ T.decodeUtf8 b- spaces <- newSpaces region (Explicit access secret)- return (bucket, spaces)- _ -> throwIO . userError- $ mconcat [ "io-tests: Credentials must consist of "- , "exactly four lines, in the order "- , "'access', 'secret', 'region', 'bucket'"- , "with each key separated from its "- , "value with '=', without whitespace "- , "See ./creds.conf.skel"- ]- where- getVal = snd . C.splitAt 7+getCreds :: IO (Bucket, Spaces)+getCreds = lookupEnv "SPACES_DEFAULT_BUCKET" >>= \case+ Nothing -> throwIO+ $ userError "Please export SPACES_DEFAULT_BUCKET to run the tests"+ Just b -> (,) <$> mkBucket (T.pack b) <*> newSpaces Discover getStatus :: SpacesResponse a -> Maybe Int getStatus = (^? #metadata . _Just . #status . to H.statusCode)
− io-tests/creds.conf
@@ -1,4 +0,0 @@-access=EUTZTPU7IKRELWXQ4SQW-secret=qW4cL9oE34II4uFVWnSjE8DMr6exxqJBlim9AjDHqkM-region=sgp1-bucket=tiengnam-sg
− io-tests/creds.conf.skel
@@ -1,4 +0,0 @@-access=REPLACEME-secret=REPLACEME-region=REPLACEME-bucket=REPLACEME
src/Network/DO/Spaces.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-}@@ -72,37 +73,21 @@ , Profile , CORSRule , mkCORSRule- , Grant(..)+ , Grant , Grantee(..) , Permission(..) , LifecycleID , mkLifecycleID , SpacesException , ClientException(..)- , APIException(..)+ , APIException ) where import Conduit- ( (.|)- , await- , decodeUtf8C- , runConduit- , runConduitRes- , sinkFileCautious- , sinkLazy- , sourceFile- , sourceLazy- , withSourceFile- , yield- ) import Control.Monad ( void )-import Control.Monad.Catch ( MonadThrow(throwM) )-import Control.Monad.Catch.Pure ( MonadCatch(catch) )-import Control.Monad.IO.Class ( MonadIO(liftIO) )-import Control.Monad.Reader.Class ( ask )+import Control.Monad.Catch ( MonadCatch(catch) ) -import Data.Bifunctor ( Bifunctor(bimap) ) import Data.Bool ( bool ) import qualified Data.ByteString.Char8 as C import qualified Data.ByteString.Lazy as LB@@ -116,14 +101,13 @@ import Data.Sequence ( Seq ) import qualified Data.Text as T import Data.Text ( Text )-import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as LT import Lens.Micro ( (<&>), (^.) ) import Network.DO.Spaces.Actions import Network.DO.Spaces.Types-import Network.DO.Spaces.Utils ( defaultUploadHeaders )+import Network.DO.Spaces.Utils import Network.HTTP.Client.Conduit ( RequestBody(RequestBodyLBS) ) import Network.HTTP.Client.TLS ( getGlobalManager ) import Network.Mime@@ -133,83 +117,121 @@ , mimeByExt ) -import System.Environment ( lookupEnv ) import qualified System.FilePath as F +import UnliftIO.Directory+import UnliftIO.Environment+ -- | Perform a transaction using your 'Spaces' client configuration. Note that--- this does /not/ perform any exception handling; if caught at the lower level,+-- this does not perform any exception handling; if caught at the lower level, -- exceptions are generally re-thrown as 'SpacesException's------ To run a 'SpacesT' action with arguments in the opposite order, you can use--- 'runSpacesT' directly runSpaces :: Spaces -> SpacesT m a -> m a runSpaces = flip runSpacesT --- | Create a new 'Spaces' in a given 'Region' while specifying a method to retrieve--- your credentials:+-- | Create a new 'Spaces' by specifying a method to retrieve the region and+-- your credentials. --+-- 'Discover' will first try to find a credentials file (see the notes on 'FromFile'+-- below) in @~/.aws/credentials@ or @$XDG_CONFIG_HOME/do-spaces/credentials@, in+-- that order, using the @[default]@ profile. Failing that, it will try the equivalent+-- of @FromEnv Nothing@ (see the notes below).+-- -- 'FromFile' expects a configuration file in the same format as AWS credentials -- files, with the same field names. For example: -- -- > [default] -- > aws_access_key_id=AKIAIOSFODNN7EXAMPLE -- > aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY+-- > aws_default_region=fra1 ----- 'FromEnv' will look up the following environment variables to find your--- keys: @AWS_ACCESS_KEY_ID@ , @SPACES_ACCESS_KEY_ID@ , @SPACES_ACCESS_KEY@--- for the 'AccessKey', and @AWS_SECRET_ACCESS_KEY@ , @SPACES_SECRET_ACCESS_KEY@--- , and @SPACES_SECRET_KEY@ for your 'SecretKey'. Alternatively, you can directly--- specify the environment variables to consult.+-- When provided with @Nothing@, 'FromEnv' will look up the following environment+-- variables to find your region and keys: ----- You can also choose to provide both keys yourself with 'Explicit'+-- * For the 'Region': ---newSpaces- :: (MonadThrow m, MonadIO m) => Region -> CredentialSource -> m Spaces-newSpaces region cs = do+-- - @AWS_DEFAULT_REGION@+-- - /OR/ @SPACES_DEFAULT_REGION@+--+-- * For the 'AccessKey':+--+-- - @AWS_ACCESS_KEY_ID@+-- - /OR/ @SPACES_ACCESS_KEY_ID@+-- - /OR/ @SPACES_ACCESS_KEY@+--+-- * For the 'SecretKey':+--+-- - @AWS_SECRET_ACCESS_KEY@+-- - /OR/ @SPACES_SECRET_ACCESS_KEY@+-- - /OR/ @SPACES_SECRET_KEY@+--+-- Alternatively, you can directly specify a tuple of environment variables to+-- search for.+--+-- You can also choose to provide the region and both keys yourself with 'Explicit'+--+newSpaces :: (MonadThrow m, MonadIO m) => CredentialSource -> m Spaces+newSpaces cs = do manager <- liftIO getGlobalManager- (accessKey, secretKey) <- liftIO $ source cs- return Spaces { .. }- where- source (Explicit ak sk) = return (ak, sk)- source (FromEnv (Just (akEnv, skEnv))) =- ensureKeys =<< (,) <$> lookupKey akEnv <*> lookupKey skEnv- source (FromEnv Nothing) = do- ak <- lookupKeys [ "AWS_ACCESS_KEY_ID"+ (region, accessKey, secretKey) <- liftIO $ source cs+ pure Spaces { .. }++source :: (MonadIO m, MonadCatch m)+ => CredentialSource+ -> m (Region, AccessKey, SecretKey)+source = \case+ Discover -> catch @_ @ClientException tryFile $ const tryEnv+ where+ tryFile = do+ cfgDir <- getXdgDirectory XdgConfig "do-spaces"+ findFile [ ".aws", cfgDir ] "credentials" >>= \case+ Nothing ->+ throwM $ ConfigurationError "No credentials file found"+ Just fp -> source $ FromFile fp Nothing++ tryEnv = source $ FromEnv Nothing++ Explicit region ak sk -> pure (region, ak, sk)++ FromEnv (Just (region, a, s)) -> ensureVars+ =<< (,,) <$> lookupVar region <*> lookupVar a <*> lookupVar s++ FromEnv Nothing -> do+ region <- lookupVars [ "AWS_DEFAULT_REGION", "SPACES_DEFAULT_REGION" ]+ ak <- lookupVars [ "AWS_ACCESS_KEY_ID" , "SPACES_ACCESS_KEY_ID" , "SPACES_ACCESS_KEY" ]- sk <- lookupKeys [ "AWS_SECRET_ACCESS_KEY"+ sk <- lookupVars [ "AWS_SECRET_ACCESS_KEY" , "SPACES_SECRET_ACCESS_KEY" , "SPACES_SECRET_KEY" ]- ensureKeys (ak, sk)+ ensureVars (region, ak, sk) - source (FromFile fp profile) = do+ FromFile fp profile -> do contents <- LT.toStrict- <$> runConduitRes (sourceFile fp .| decodeUtf8C .| sinkLazy)- case I.parseIniFile contents parseConf of- Left _ ->- throwM $ ConfigurationError "Failed to read credentials file"- Right ks -> return- $ bimap (coerce . T.encodeUtf8) (coerce . T.encodeUtf8) ks+ <$> liftIO (runConduitRes (sourceFile fp+ .| decodeUtf8C+ .| sinkLazy))+ either (throwM . ConfigurationError . T.pack) ensureVars+ $ I.parseIniFile contents parseConf where parseConf = I.section (fromMaybe "default" profile)- $ (,) <$> I.field "aws_access_key_id"- <*> I.field "aws_secret_access_key"-- throwMissingKeys k = throwM . ConfigurationError $ "Missing " <> k-- lookupKeys xs = asum <$> sequence (lookupEnv <$> xs)-- lookupKey = lookupEnv . T.unpack+ $ (,,)+ <$> (fmap (T.unpack . T.toLower)+ <$> I.fieldMb "aws_default_region")+ <*> (fmap T.unpack <$> I.fieldMb "aws_access_key_id")+ <*> (fmap T.unpack <$> I.fieldMb "aws_secret_access_key")+ where+ lookupVars xs = asum <$> sequence (lookupEnv <$> xs) - ensureKeys = \case- (Just a, Just s) -> return (mkKey AccessKey a, mkKey SecretKey s)- (Just _, _) -> throwMissingKeys "secret key"- (_, Just _) -> throwMissingKeys "access key"- (_, _) -> throwMissingKeys "secret and access keys"+ lookupVar = lookupEnv . T.unpack - mkKey f = f . C.pack+ ensureVars = \case+ (Just r, Just a, Just s) -> do+ reg <- slugToRegion r+ pure (reg, coerce $ C.pack a, coerce $ C.pack s)+ (_, _, _) -> throwM . ConfigurationError+ $ "Missing acces/secret keys and/or region" -- | Upload an 'Object' within a single request uploadObject :: MonadSpaces m@@ -225,7 +247,7 @@ -- | Initiate and complete a multipart upload, using default 'UploadHeaders'. -- If a 'SpacesException' is thrown while performing the transaction, an attempt--- will be made to runSpaces a 'CancelMultipart' request, and the exception will be+-- will be made to run a 'CancelMultipart' request, and the exception will be -- rethrown multipartObject :: MonadSpaces m@@ -256,20 +278,17 @@ putPart session = go 1 where- go n = await >>= \case- Nothing -> return ()- Just v -> do- spaces <- ask- etag <- liftIO- $ runSpaces spaces- (runAction NoMetadata- $ UploadPart session n (RequestBodyLBS v))- <&> (^. field @"result" . field @"etag")- yield etag >> go (n + 1)+ go !n = awaitForever $ \v -> do+ let pieceBody = RequestBodyLBS v+ uploadPart =+ runAction NoMetadata $ UploadPart session n pieceBody + etag <- lift $ uploadPart <&> (^. field @"result" . field @"etag")+ yield etag >> go (n + 1)+ inChunks = loop 0 [] where- loop n chunk = await >>= maybe (yieldChunk chunk) go+ loop !n chunk = await >>= maybe (yieldChunk chunk) go where go bs = bool (loop len newChunk) (yieldChunk newChunk >> loop 0 [])@@ -450,8 +469,8 @@ case nextMarker of Just _ | isTruncated -> go (os <> objects) nextMarker- | otherwise -> return $ os <> objects- Nothing -> return $ os <> objects+ | otherwise -> pure $ os <> objects+ Nothing -> pure $ os <> objects -- | Get the 'CORSRule's configured for a given 'Bucket' getBucketCORS :: MonadSpaces m => Bucket -> m (SpacesResponse GetBucketCORS)@@ -537,7 +556,4 @@ -- several actions which may be of use, including sinking remote 'Object' data into -- a file, uploading the contents of a file as an 'Object', and recursively listing -- the entire contents of a 'Bucket'--------- --
src/Network/DO/Spaces/Actions.hs view
@@ -127,7 +127,7 @@ =<< runConduit (body .| sinkLbs) result <- consumeResponse @_ @a raw- return SpacesResponse { .. }+ pure SpacesResponse { .. } parseErrorResponse :: (MonadThrow m, MonadIO m) => Status -> RawResponse m -> m APIException@@ -139,5 +139,5 @@ $ cursor $/ X.laxElement "RequestId" &/ X.content hostID <- X.force (xmlElemError "HostId") $ cursor $/ X.laxElement "HostId" &/ X.content- return APIException { .. }+ pure APIException { .. }
src/Network/DO/Spaces/Actions/CopyObject.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -37,28 +38,14 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket(Bucket)- , CannedACL- , ClientException(InvalidRequest)- , ETag- , Method(PUT)- , MonadSpaces- , Object(Object)- , SpacesRequestBuilder(..)- ) import Network.DO.Spaces.Utils- ( bshow- , etagP- , lastModifiedP- , showCannedACL- , xmlDocCursor- ) -- | Whether the 'Object'\'s metadata should be copied or replaced. Replace is -- required to copy an object to itself-data MetadataDirective = Copy | Replace- deriving ( Show, Eq, Generic )+data MetadataDirective+ = Copy+ | Replace+ deriving stock ( Show, Eq, Generic ) -- | Copy and 'Object' from one 'Bucket' to another. Both buckets must -- be in the same region@@ -70,11 +57,11 @@ , metadataDirective :: MetadataDirective , acl :: Maybe CannedACL }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) data CopyObjectResponse = CopyObjectResponse { etag :: ETag, lastModified :: UTCTime }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m CopyObject where type ConsumedResponse CopyObject = CopyObjectResponse@@ -88,16 +75,16 @@ , "REPLACE directive is specified" ] spaces <- ask- return SpacesRequestBuilder- { object = Just destObject- , bucket = Just destBucket- , method = Just PUT- , body = Nothing- , queryString = Nothing- , subresources = Nothing- , overrideRegion = Nothing- , ..- }+ pure SpacesRequestBuilder+ { object = Just destObject+ , bucket = Just destBucket+ , method = Just PUT+ , body = Nothing+ , queryString = Nothing+ , subresources = Nothing+ , overrideRegion = Nothing+ , ..+ } where headers = [ ( CI.mk "x-amz-copy-source" , mconcat [ "/"
src/Network/DO/Spaces/Actions/CreateBucket.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE DeriveGeneric #-}++{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-}@@ -28,15 +30,7 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , CannedACL- , Method(PUT)- , MonadSpaces- , Region- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( showCannedACL )+import Network.DO.Spaces.Utils -- | Create a new, empty 'Bucket' data CreateBucket = CreateBucket@@ -46,7 +40,7 @@ -- ^ The 'CannedACL' to use; defaults to -- 'Network.DO.Spaces.Types.CannedACL.Private' }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type CreateBucketResponse = () @@ -55,17 +49,17 @@ buildRequest CreateBucket { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Just PUT- , overrideRegion = region- , body = Nothing- , object = Nothing- , queryString = Nothing- , subresources = Nothing- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Just PUT+ , overrideRegion = region+ , body = Nothing+ , object = Nothing+ , queryString = Nothing+ , subresources = Nothing+ , ..+ } where headers = catMaybes [ (CI.mk "x-amz-acl", ) . showCannedACL <$> acl ] - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/DeleteBucket.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-}@@ -25,18 +27,13 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , Method(DELETE)- , MonadSpaces- , SpacesRequestBuilder(..)- ) -- | Delete a single 'Bucket'. Note that it must be empty-data DeleteBucket = DeleteBucket+newtype DeleteBucket = DeleteBucket { bucket :: Bucket -- ^ The name of the 'Bucket' to delete }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) type DeleteBucketResponse = () @@ -45,16 +42,16 @@ buildRequest DeleteBucket { .. } = do spaces <- ask- return SpacesRequestBuilder- { method = Just DELETE- , bucket = Just bucket- , body = Nothing- , object = Nothing- , queryString = Nothing- , subresources = Nothing- , headers = mempty- , overrideRegion = Nothing- , ..- }+ pure SpacesRequestBuilder+ { method = Just DELETE+ , bucket = Just bucket+ , body = Nothing+ , object = Nothing+ , queryString = Nothing+ , subresources = Nothing+ , headers = mempty+ , overrideRegion = Nothing+ , ..+ } - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/DeleteBucketCORS.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-}@@ -28,17 +30,12 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , Method(DELETE)- , MonadSpaces- , SpacesRequestBuilder(..)- ) import qualified Network.HTTP.Types as H -- | Delete all of a 'Bucket'\'s configured 'Network.DO.Spaces.Types.CORSRule's-data DeleteBucketCORS = DeleteBucketCORS { bucket :: Bucket }- deriving ( Show, Eq, Generic )+newtype DeleteBucketCORS = DeleteBucketCORS { bucket :: Bucket }+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) type DeleteBucketCORSResponse = () @@ -47,20 +44,20 @@ buildRequest DeleteBucketCORS { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Just DELETE- , body = Nothing- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "cors" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Just DELETE+ , body = Nothing+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "cors" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/DeleteBucketLifecycle.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-}@@ -28,16 +30,11 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , Method(DELETE)- , MonadSpaces- , SpacesRequestBuilder(..)- ) import qualified Network.HTTP.Types as H -data DeleteBucketLifecycle = DeleteBucketLifecycle { bucket :: Bucket }- deriving ( Show, Eq, Generic )+newtype DeleteBucketLifecycle = DeleteBucketLifecycle { bucket :: Bucket }+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) type DeleteBucketLifecycleResponse = () @@ -46,20 +43,20 @@ buildRequest DeleteBucketLifecycle { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Just DELETE- , body = Nothing- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "lifecycle" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Just DELETE+ , body = Nothing+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "lifecycle" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/DeleteObject.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -25,17 +26,10 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , Method(DELETE)- , MonadSpaces- , Object- , SpacesRequestBuilder(..)- ) -- | Delete a single 'Object' data DeleteObject = DeleteObject { bucket :: Bucket, object :: Object }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type DeleteObjectResponse = () @@ -44,16 +38,16 @@ buildRequest DeleteObject { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , object = Just object- , method = Just DELETE- , body = Nothing- , queryString = Nothing- , subresources = Nothing- , headers = mempty- , overrideRegion = Nothing- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , object = Just object+ , method = Just DELETE+ , body = Nothing+ , queryString = Nothing+ , subresources = Nothing+ , headers = mempty+ , overrideRegion = Nothing+ , ..+ } - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/GetBucketACLs.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-}@@ -28,18 +30,13 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( ACLResponse- , Action(..)- , Bucket- , MonadSpaces- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( aclP, xmlDocCursor )+import Network.DO.Spaces.Utils import qualified Network.HTTP.Types as H -- | Get the full Access Control List associated with a 'Bucket'-data GetBucketACLs = GetBucketACLs { bucket :: Bucket }- deriving ( Show, Eq, Generic )+newtype GetBucketACLs = GetBucketACLs { bucket :: Bucket }+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) type GetBucketACLsResponse = ACLResponse @@ -48,20 +45,20 @@ buildRequest GetBucketACLs { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Nothing- , body = Nothing- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "acl" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Nothing+ , body = Nothing+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "acl" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } consumeResponse raw = aclP =<< xmlDocCursor raw
src/Network/DO/Spaces/Actions/GetBucketCORS.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-}@@ -34,14 +36,7 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , CORSRule(..)- , MonadSpaces- , SpacesRequestBuilder(..)- , mkCORSRule- )-import Network.DO.Spaces.Utils ( xmlDocCursor, xmlElemError )+import Network.DO.Spaces.Utils import qualified Network.HTTP.Types as H import Text.Read ( readMaybe )@@ -49,32 +44,35 @@ import Text.XML.Cursor ( ($/), (&/), (&|) ) -- | Get the 'CORSRule's associated with a 'Bucket'-data GetBucketCORS = GetBucketCORS { bucket :: Bucket }- deriving ( Show, Eq, Generic )+newtype GetBucketCORS = GetBucketCORS { bucket :: Bucket }+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) -data GetBucketCORSResponse = GetBucketCORSResponse { rules :: Seq CORSRule }- deriving ( Show, Eq, Generic )+newtype GetBucketCORSResponse =+ GetBucketCORSResponse { rules :: Seq CORSRule }+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) instance MonadSpaces m => Action m GetBucketCORS where type ConsumedResponse GetBucketCORS = GetBucketCORSResponse buildRequest GetBucketCORS { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Nothing- , body = Nothing- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "cors" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Nothing+ , body = Nothing+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "cors" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } consumeResponse raw = do cursor <- xmlDocCursor raw
src/Network/DO/Spaces/Actions/GetBucketLifecycle.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-}@@ -36,21 +38,7 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , ClientException(InvalidXML)- , LifecycleExpiration(AfterDays, OnDate)- , LifecycleID(LifecycleID)- , LifecycleRule(..)- , MonadSpaces- , SpacesRequestBuilder(..)- ) import Network.DO.Spaces.Utils- ( xmlDocCursor- , xmlElemError- , xmlMaybeElem- , xmlUTCTime- ) import qualified Network.HTTP.Types as H import Text.Read ( readMaybe )@@ -62,33 +50,35 @@ -- | Get the 'LifecycleRule' configuration for a 'Bucket'. Note that unless -- you have explicitly configured lifecycle rules, this will fail with a 404 -- status and an error code of @NoSuchLifecycleConfiguration@-data GetBucketLifecycle = GetBucketLifecycle { bucket :: Bucket }- deriving ( Show, Eq, Generic )+newtype GetBucketLifecycle = GetBucketLifecycle { bucket :: Bucket }+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) -data GetBucketLifecycleResponse =+newtype GetBucketLifecycleResponse = GetBucketLifecycleResponse { rules :: [LifecycleRule] }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) instance MonadSpaces m => Action m GetBucketLifecycle where type ConsumedResponse GetBucketLifecycle = GetBucketLifecycleResponse buildRequest GetBucketLifecycle { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Nothing- , body = Nothing- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "lifecycle" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Nothing+ , body = Nothing+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "lifecycle" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } consumeResponse raw = do cursor <- xmlDocCursor raw@@ -97,11 +87,11 @@ ruleP :: MonadThrow m => Cursor X.Node -> m LifecycleRule ruleP c = do- id' <- X.force (xmlElemError "ID")+ lifecycleID <- X.force (xmlElemError "ID") $ c $/ X.laxElement "ID" &/ X.content &| coerce enabled <- X.forceM (xmlElemError "Status") $ c $/ X.laxElement "Status" &/ X.content &| readStatus- return LifecycleRule { .. }+ pure LifecycleRule { .. } where prefix = xmlMaybeElem c "Prefix" @@ -116,8 +106,8 @@ &| (readMaybe . T.unpack) readStatus = \case- "Enabled" -> return True- "Disabled" -> return False+ "Enabled" -> pure True+ "Disabled" -> pure False _ -> throwM $ InvalidXML "GetBucketLifecycle: invalid Status" -- TODO find a less hideous way of doing this
src/Network/DO/Spaces/Actions/GetBucketLocation.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-}@@ -28,54 +30,47 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , MonadSpaces- , Region(..)- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( slugToRegion- , xmlDocCursor- , xmlElemError- )+import Network.DO.Spaces.Utils import qualified Network.HTTP.Types as H import qualified Text.XML.Cursor as X import Text.XML.Cursor ( ($.//), (&/), (&|) ) -- | Query the location (the 'Region') of a 'Bucket'-data GetBucketLocation = GetBucketLocation+newtype GetBucketLocation = GetBucketLocation { bucket :: Bucket -- ^ The name of the 'Bucket' whose location you'd like to retrieve }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) -data GetBucketLocationResponse = GetBucketLocationResponse+newtype GetBucketLocationResponse = GetBucketLocationResponse { locationConstraint :: Region -- ^ The 'Region' of the queried 'Bucket' }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) instance MonadSpaces m => Action m GetBucketLocation where type ConsumedResponse GetBucketLocation = GetBucketLocationResponse buildRequest GetBucketLocation { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Nothing- , body = Nothing- , object = Nothing- , headers = mempty- , overrideRegion = Nothing- , queryString = Nothing- , subresources = Just- $ H.toQuery [ ( "location" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Nothing+ , body = Nothing+ , object = Nothing+ , headers = mempty+ , overrideRegion = Nothing+ , queryString = Nothing+ , subresources = Just+ $ H.toQuery [ ( "location" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } consumeResponse raw = do cursor <- xmlDocCursor raw
src/Network/DO/Spaces/Actions/GetObject.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -31,41 +32,33 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , MonadSpaces- , Object- , ObjectMetadata- , RawResponse(..)- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( lookupObjectMetadata )+import Network.DO.Spaces.Utils -- | Retrieve an 'Object' along with its associated metadata. The object's data -- is read into a lazy 'LB.ByteString' data GetObject = GetObject { bucket :: Bucket, object :: Object }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) data GetObjectResponse = GetObjectResponse { objectMetadata :: ObjectMetadata, objectData :: LB.ByteString }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m GetObject where type ConsumedResponse GetObject = GetObjectResponse buildRequest GetObject { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , object = Just object- , method = Nothing- , body = Nothing- , queryString = Nothing- , subresources = Nothing- , overrideRegion = Nothing- , headers = mempty- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , object = Just object+ , method = Nothing+ , body = Nothing+ , queryString = Nothing+ , subresources = Nothing+ , overrideRegion = Nothing+ , headers = mempty+ , ..+ } consumeResponse raw@RawResponse { .. } = GetObjectResponse <$> lookupObjectMetadata raw
src/Network/DO/Spaces/Actions/GetObjectACLs.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -28,19 +29,12 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( ACLResponse- , Action(..)- , Bucket- , MonadSpaces- , Object- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( aclP, xmlDocCursor )+import Network.DO.Spaces.Utils import qualified Network.HTTP.Types as H -- | Get the full Access Control List associated with a 'Bucket' data GetObjectACLs = GetObjectACLs { bucket :: Bucket, object :: Object }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type GetObjectACLsResponse = ACLResponse @@ -49,20 +43,20 @@ buildRequest GetObjectACLs { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , object = Just object- , method = Nothing- , body = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "acl" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , object = Just object+ , method = Nothing+ , body = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "acl" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } consumeResponse raw = aclP =<< xmlDocCursor raw
src/Network/DO/Spaces/Actions/GetObjectInfo.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -26,20 +27,12 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , Method(HEAD)- , MonadSpaces- , Object- , ObjectMetadata(..)- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( lookupObjectMetadata )+import Network.DO.Spaces.Utils -- | Get information about an 'Object'; the response does not contain the -- object itself data GetObjectInfo = GetObjectInfo { bucket :: Bucket, object :: Object }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type GetObjectInfoResponse = ObjectMetadata @@ -48,16 +41,16 @@ buildRequest GetObjectInfo { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , object = Just object- , method = Just HEAD- , body = Nothing- , queryString = Nothing- , subresources = Nothing- , headers = mempty- , overrideRegion = Nothing- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , object = Just object+ , method = Just HEAD+ , body = Nothing+ , queryString = Nothing+ , subresources = Nothing+ , headers = mempty+ , overrideRegion = Nothing+ , ..+ } consumeResponse = lookupObjectMetadata
src/Network/DO/Spaces/Actions/ListAllBuckets.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-}@@ -29,47 +30,35 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket(Bucket)- , BucketInfo(..)- , MonadSpaces- , Owner(..)- , SpacesRequestBuilder(..)- ) import Network.DO.Spaces.Utils- ( ownerP- , xmlDocCursor- , xmlElemError- , xmlUTCTime- ) import qualified Text.XML.Cursor as X import Text.XML.Cursor ( ($/), (&/), (&|) ) -- | List all of your 'Bucket's withing the 'Network.DO.Spaces.Region' you have configured data ListAllBuckets = ListAllBuckets- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) data ListAllBucketsResponse = ListAllBucketsResponse { owner :: Owner, buckets :: Seq BucketInfo }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m ListAllBuckets where type ConsumedResponse ListAllBuckets = ListAllBucketsResponse buildRequest _ = do spaces <- ask- return SpacesRequestBuilder- { body = Nothing- , method = Nothing- , object = Nothing- , queryString = Nothing- , subresources = Nothing- , bucket = Nothing- , headers = mempty- , overrideRegion = Nothing- , ..- }+ pure SpacesRequestBuilder+ { body = Nothing+ , method = Nothing+ , object = Nothing+ , queryString = Nothing+ , subresources = Nothing+ , bucket = Nothing+ , headers = mempty+ , overrideRegion = Nothing+ , ..+ } consumeResponse raw = do cursor <- xmlDocCursor raw@@ -77,7 +66,7 @@ $ cursor $/ X.laxElement "Owner" &| ownerP bs <- X.force (xmlElemError "Buckets") $ cursor $/ X.laxElement "Buckets" &| bucketsP- return ListAllBucketsResponse { buckets = S.fromList bs, .. }+ pure ListAllBucketsResponse { buckets = S.fromList bs, .. } where bucketsP c = X.forceM (xmlElemError "Bucket") . sequence $ c $/ X.laxElement "Bucket" &| bucketInfoP@@ -87,4 +76,4 @@ $ c $/ X.laxElement "Name" &/ X.content &| coerce creationDate <- X.forceM (xmlElemError "Creation date") $ c $/ X.laxElement "CreationDate" &/ X.content &| xmlUTCTime- return BucketInfo { .. }+ pure BucketInfo { .. }
src/Network/DO/Spaces/Actions/ListBucket.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -37,31 +38,14 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket(Bucket)- , ClientException(InvalidRequest)- , MonadSpaces- , Object(..)- , ObjectInfo(..)- , SpacesRequestBuilder(..)- ) import Network.DO.Spaces.Utils- ( bshow- , etagP- , isTruncP- , lastModifiedP- , ownerP- , xmlDocCursor- , xmlElemError- , xmlInt- , xmlMaybeElem- , xmlNum- ) import qualified Network.HTTP.Types as H import qualified Text.XML.Cursor as X import Text.XML.Cursor ( ($/), (&/), (&|) ) +import Web.HttpApiData ( ToHttpApiData(toQueryParam) )+ -- | List the contents ('Object's) of a 'Bucket' data ListBucket = ListBucket { bucket :: Bucket@@ -74,7 +58,7 @@ -- ^ String value to group keys. Only objects whose names begin with the -- prefix are returned }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) data ListBucketResponse = ListBucketResponse { bucket :: Bucket@@ -92,7 +76,7 @@ -- ^ Indicates whether the response contains all possible 'Object's , objects :: Seq ObjectInfo }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m ListBucket where type ConsumedResponse ListBucket = ListBucketResponse@@ -103,21 +87,22 @@ $ InvalidRequest "ListBucket: maxKeys must be >= 0 && <= 1000" spaces <- ask - return SpacesRequestBuilder- { bucket = Just bucket- , body = Nothing- , object = Nothing- , method = Nothing- , headers = mempty- , subresources = Nothing- , overrideRegion = Nothing- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , body = Nothing+ , object = Nothing+ , method = Nothing+ , headers = mempty+ , subresources = Nothing+ , overrideRegion = Nothing+ , ..+ } where queryString = Just $ H.toQuery [ ("delimiter" :: ByteString, ) . C.singleton <$> delimiter- , ("marker", ) . T.encodeUtf8 . unObject <$> marker+ , ("marker", ) . T.encodeUtf8 . toQueryParam+ <$> marker , ("max-keys", ) . bshow <$> maxKeys , ("prefix", ) . T.encodeUtf8 <$> prefix ]@@ -133,7 +118,7 @@ let prefix = xmlMaybeElem cursor "Prefix" marker = coerce <$> xmlMaybeElem cursor "Marker" nextMarker = coerce <$> xmlMaybeElem cursor "NextMarker"- return ListBucketResponse { .. }+ pure ListBucketResponse { .. } where objectInfoP c = do object <- X.force (xmlElemError "Key")@@ -144,4 +129,4 @@ $ c $/ X.laxElement "Size" &/ X.content &| xmlInt owner <- X.forceM (xmlElemError "Owner") $ c $/ X.laxElement "Owner" &| ownerP- return ObjectInfo { .. }+ pure ObjectInfo { .. }
src/Network/DO/Spaces/Actions/SetBucketACLs.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -29,15 +30,7 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , Grant(..)- , Method(PUT)- , MonadSpaces- , Owner- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( writeACLSetter )+import Network.DO.Spaces.Utils import Network.HTTP.Client.Conduit ( RequestBody(RequestBodyLBS) ) import qualified Network.HTTP.Types as H @@ -46,7 +39,7 @@ , acls :: [Grant] , owner :: Owner -- ^ The bucket owner }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type SetBucketACLsResponse = () @@ -55,20 +48,20 @@ buildRequest sba@SetBucketACLs { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Just PUT- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , body = Just . RequestBodyLBS $ writeACLSetter sba- , subresources = Just- $ H.toQuery [ ( "acl" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Just PUT+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , body = Just . RequestBodyLBS $ writeACLSetter sba+ , subresources = Just+ $ H.toQuery [ ( "acl" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/SetBucketCORS.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE DeriveGeneric #-}++{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -16,8 +18,6 @@ -- Stability : experimental -- Portability : GHC ---{-# OPTIONS_GHC -Wno-unused-imports #-}- module Network.DO.Spaces.Actions.SetBucketCORS ( SetBucketCORSResponse , SetBucketCORS(..)@@ -27,20 +27,12 @@ import Data.ByteString ( ByteString ) import qualified Data.CaseInsensitive as CI-import qualified Data.Text as T import qualified Data.Text.Encoding as T import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , CORSRule(..)- , Method(PUT)- , MonadSpaces- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( mkNode, tshow )+import Network.DO.Spaces.Utils import Network.HTTP.Conduit ( RequestBody(RequestBodyLBS) ) import qualified Network.HTTP.Types as H @@ -48,7 +40,7 @@ -- | Set a 'Bucket'\'s 'CORSRule's data SetBucketCORS = SetBucketCORS { bucket :: Bucket, rules :: [CORSRule] }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type SetBucketCORSResponse = () @@ -57,20 +49,20 @@ buildRequest SetBucketCORS { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Just PUT- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "cors" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Just PUT+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "cors" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } where body = Just . RequestBodyLBS . X.renderLBS X.def $ X.Document prologue root mempty@@ -86,4 +78,4 @@ , mkNode "AllowedMethod" . tshow <$> allowedMethods ] - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/SetBucketLifecycle.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -32,16 +33,7 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , LifecycleExpiration(AfterDays, OnDate)- , LifecycleID(LifecycleID)- , LifecycleRule(..)- , Method(PUT)- , MonadSpaces- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( mkNode, tshow )+import Network.DO.Spaces.Utils import Network.HTTP.Client.Conduit ( RequestBody(RequestBodyLBS) ) import qualified Network.HTTP.Types as H @@ -50,7 +42,7 @@ -- | Configure the 'LifecycleRule's for a 'Bucket' data SetBucketLifecycle = SetBucketLifecycle { bucket :: Bucket, rules :: [LifecycleRule] }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type SetBucketLifecycleResponse = () @@ -59,20 +51,20 @@ buildRequest SetBucketLifecycle { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , method = Just PUT- , object = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "lifecycle" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , method = Just PUT+ , object = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "lifecycle" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } where body = Just . RequestBodyLBS . X.renderLBS X.def $ X.Document prologue root mempty@@ -88,7 +80,7 @@ rulesNode LifecycleRule { .. } = X.NodeElement $ X.Element "Rule" mempty nodes where- nodes = [ mkNode "ID" (coerce id')+ nodes = [ mkNode "ID" (coerce lifecycleID) , mkNode "Status" (showEnabled enabled) ] <> foldMap (pure . mkNode "Prefix") prefix@@ -111,4 +103,4 @@ mempty [ mkNode "DaysAfterInitiation" (tshow days) ] - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/SetObjectACLs.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}++{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -29,16 +31,7 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , Grant(..)- , Method(PUT)- , MonadSpaces- , Object- , Owner- , SpacesRequestBuilder(..)- )-import Network.DO.Spaces.Utils ( writeACLSetter )+import Network.DO.Spaces.Utils import Network.HTTP.Client.Conduit ( RequestBody(RequestBodyLBS) ) import qualified Network.HTTP.Types as H @@ -49,7 +42,7 @@ , owner :: Owner -- ^ The object owner , acls :: [Grant] }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type SetObjectACLsResponse = () @@ -58,20 +51,20 @@ buildRequest soa@SetObjectACLs { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , object = Just object- , method = Just PUT- , body = Just . RequestBodyLBS $ writeACLSetter soa- , overrideRegion = Nothing- , queryString = Nothing- , headers = mempty- , subresources = Just- $ H.toQuery [ ( "acl" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , object = Just object+ , method = Just PUT+ , body = Just . RequestBodyLBS $ writeACLSetter soa+ , overrideRegion = Nothing+ , queryString = Nothing+ , headers = mempty+ , subresources = Just+ $ H.toQuery [ ( "acl" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } - consumeResponse _ = return ()+ consumeResponse _ = pure ()
src/Network/DO/Spaces/Actions/UploadMultipart.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-}@@ -51,32 +53,7 @@ import Lens.Micro ( (^.), (^?) ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , ClientException(OtherError)- , ETag- , Method(POST, DELETE, PUT)- , MonadSpaces- , Object- , SpacesRequestBuilder(..)- , UploadHeaders- ) import Network.DO.Spaces.Utils- ( bucketP- , etagP- , isTruncP- , lastModifiedP- , lookupHeader- , mkNode- , objectP- , quote- , readEtag- , renderUploadHeaders- , tshow- , xmlDocCursor- , xmlElemError- , xmlNum- ) import Network.HTTP.Client.Conduit ( RequestBody(RequestBodyLBS) ) import qualified Network.HTTP.Types as H import Network.Mime ( MimeType )@@ -92,14 +69,14 @@ , etag :: ETag , size :: Int -- ^ Size in bytes }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) data MultipartSession = MultipartSession { bucket :: Bucket , object :: Object , uploadID :: UploadID -- }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | A unique ID assigned to a multipart upload session type UploadID = Text@@ -111,31 +88,32 @@ , optionalHeaders :: UploadHeaders , contentType :: Maybe MimeType }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) newtype BeginMultipartResponse = BeginMultipartResponse { session :: MultipartSession }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) instance MonadSpaces m => Action m BeginMultipart where type (ConsumedResponse BeginMultipart) = BeginMultipartResponse buildRequest BeginMultipart { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , object = Just object- , method = Just POST- , body = Nothing- , overrideRegion = Nothing- , queryString = Nothing- , subresources = Just- $ H.toQuery [ ( "uploads" :: ByteString- , Nothing :: Maybe ByteString- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , object = Just object+ , method = Just POST+ , body = Nothing+ , overrideRegion = Nothing+ , queryString = Nothing+ , subresources = Just+ $ H.toQuery [ ( "uploads" :: ByteString+ , Nothing :: Maybe ByteString+ )+ ]+ , ..+ } where headers = maybe id (\ct -> (:) (CI.mk "Content-Type", ct))@@ -148,41 +126,41 @@ bucket <- bucketP cursor uploadID <- X.force (xmlElemError "UploadId") $ cursor $/ X.laxElement "UploadId" &/ X.content- return $ BeginMultipartResponse { session = MultipartSession { .. } }+ pure $ BeginMultipartResponse { session = MultipartSession { .. } } data UploadPart = UploadPart { session :: MultipartSession, partNumber :: Int, body :: RequestBody }- deriving ( Generic )+ deriving stock ( Generic ) data UploadPartResponse = UploadPartResponse { etag :: ETag }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m UploadPart where type (ConsumedResponse UploadPart) = UploadPartResponse buildRequest UploadPart { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = session ^? field @"bucket"- , object = session ^? field @"object"- , body = Just body- , method = Just PUT- , overrideRegion = Nothing- , subresources = Nothing- , headers = mempty- , queryString = Just- $ H.toQuery [ ("partNumber" :: Text, tshow partNumber)- , ("uploadId", session ^. field @"uploadID")- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = session ^? field @"bucket"+ , object = session ^? field @"object"+ , body = Just body+ , method = Just PUT+ , overrideRegion = Nothing+ , subresources = Nothing+ , headers = mempty+ , queryString = Just+ $ H.toQuery [ ("partNumber" :: Text, tshow partNumber)+ , ("uploadId", session ^. field @"uploadID")+ ]+ , ..+ } consumeResponse raw = runMaybeT (UploadPartResponse <$> (readEtag =<< lookupHeader raw "etag")) >>= \case- Nothing -> throwM $ OtherError "Missing/malformed headers"- Just r -> return r+ Nothing -> throwM $ InvalidResponse "Missing/malformed headers"+ Just r -> pure r -- | Complete a multipart session data CompleteMultipart = CompleteMultipart@@ -190,7 +168,7 @@ , parts :: [(Int, ETag)] -- ^ The part numbers and 'ETag's of each uploaded part }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) data CompleteMultipartResponse = CompleteMultipartResponse { location :: Text@@ -200,27 +178,27 @@ -- ^ The MD5 hash of the final object, i.e. all of the cumulative -- uploaded parts }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m CompleteMultipart where type ConsumedResponse CompleteMultipart = CompleteMultipartResponse buildRequest CompleteMultipart { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = session ^? field @"bucket"- , object = session ^? field @"object"- , method = Just POST- , overrideRegion = Nothing- , subresources = Nothing- , headers = mempty- , queryString = Just- $ H.toQuery [ ( "uploadId" :: Text- , session ^. field @"uploadID"- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = session ^? field @"bucket"+ , object = session ^? field @"object"+ , method = Just POST+ , overrideRegion = Nothing+ , subresources = Nothing+ , headers = mempty+ , queryString = Just+ $ H.toQuery [ ( "uploadId" :: Text+ , session ^. field @"uploadID"+ )+ ]+ , ..+ } where body = Just . RequestBodyLBS . X.renderLBS X.def $ X.Document prologue root mempty@@ -244,11 +222,12 @@ $ cursor $/ X.laxElement "Location" &/ X.content object <- objectP cursor etag <- etagP cursor- return CompleteMultipartResponse { .. }+ pure CompleteMultipartResponse { .. } -- | Cancel an active multipart upload session newtype CancelMultipart = CancelMultipart { session :: MultipartSession }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) type CancelMultipartResponse = () @@ -257,27 +236,28 @@ buildRequest CancelMultipart { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = session ^? field @"bucket"- , object = session ^? field @"object"- , method = Just DELETE- , body = Nothing- , overrideRegion = Nothing- , headers = mempty- , subresources = Nothing- , queryString = Just- $ H.toQuery [ ( "uploadId" :: Text- , session ^. field @"uploadID"- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = session ^? field @"bucket"+ , object = session ^? field @"object"+ , method = Just DELETE+ , body = Nothing+ , overrideRegion = Nothing+ , headers = mempty+ , subresources = Nothing+ , queryString = Just+ $ H.toQuery [ ( "uploadId" :: Text+ , session ^. field @"uploadID"+ )+ ]+ , ..+ } - consumeResponse _ = return ()+ consumeResponse _ = pure () -- | List all of the 'Part's of a multipart upload session newtype ListParts = ListParts { session :: MultipartSession }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Generic )+ deriving newtype ( Eq ) data ListPartsResponse = ListPartsResponse { bucket :: Bucket@@ -291,28 +271,28 @@ , maxParts :: Int , isTruncated :: Bool }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m ListParts where type (ConsumedResponse ListParts) = ListPartsResponse buildRequest ListParts { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = session ^? field @"bucket"- , object = session ^? field @"object"- , method = Nothing- , body = Nothing- , overrideRegion = Nothing- , subresources = Nothing- , headers = mempty- , queryString = Just- $ H.toQuery [ ( "uploadId" :: Text- , session ^. field @"uploadID"- )- ]- , ..- }+ pure SpacesRequestBuilder+ { bucket = session ^? field @"bucket"+ , object = session ^? field @"object"+ , method = Nothing+ , body = Nothing+ , overrideRegion = Nothing+ , subresources = Nothing+ , headers = mempty+ , queryString = Just+ $ H.toQuery [ ( "uploadId" :: Text+ , session ^. field @"uploadID"+ )+ ]+ , ..+ } consumeResponse raw = do cursor <- xmlDocCursor raw@@ -327,7 +307,7 @@ partMarker <- xmlNum "PartNumberMarker" cursor nextPartMarker <- xmlNum "NextPartNumberMarker" cursor - return ListPartsResponse { .. }+ pure ListPartsResponse { .. } where partP c = Part <$> xmlNum "PartNumber" c <*> lastModifiedP c
src/Network/DO/Spaces/Actions/UploadObject.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -29,22 +30,7 @@ import GHC.Generics ( Generic ) import Network.DO.Spaces.Types- ( Action(..)- , Bucket- , ClientException(OtherError)- , ETag- , Method(PUT)- , MonadSpaces- , Object- , SpacesRequestBuilder(..)- , UploadHeaders(..)- ) import Network.DO.Spaces.Utils- ( lookupHeader- , readContentLen- , readEtag- , renderUploadHeaders- ) import Network.HTTP.Conduit ( RequestBody ) import Network.Mime ( MimeType ) @@ -57,29 +43,29 @@ , optionalHeaders :: UploadHeaders , contentType :: Maybe MimeType }- deriving ( Generic )+ deriving stock ( Generic ) data UploadObjectResponse = UploadObjectResponse { etag :: ETag , contentLength :: Int -- ^ Length in bytes }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) instance MonadSpaces m => Action m UploadObject where type ConsumedResponse UploadObject = UploadObjectResponse buildRequest UploadObject { .. } = do spaces <- ask- return SpacesRequestBuilder- { bucket = Just bucket- , object = Just object- , method = Just PUT- , body = Just body- , queryString = Nothing- , subresources = Nothing- , overrideRegion = Nothing- , ..- }+ pure SpacesRequestBuilder+ { bucket = Just bucket+ , object = Just object+ , method = Just PUT+ , body = Just body+ , queryString = Nothing+ , subresources = Nothing+ , overrideRegion = Nothing+ , ..+ } where headers = maybe id (\ct -> (:) (CI.mk "Content-Type", ct))@@ -91,7 +77,7 @@ $ UploadObjectResponse <$> (readEtag =<< lookupHeader' "etag") <*> (readContentLen =<< lookupHeader' "Content-Length") case resp of- Just r -> return r- Nothing -> throwM $ OtherError "Missing/malformed headers"+ Just r -> pure r+ Nothing -> throwM $ InvalidResponse "Missing/malformed headers" where lookupHeader' = lookupHeader raw
src/Network/DO/Spaces/Request.hs view
@@ -38,6 +38,7 @@ import Data.Function ( (&) ) import Data.Generics.Product ( HasField(field) ) import Data.Generics.Product.Positions ( HasPosition(position) )+import Data.Generics.Wrapped import Data.List ( sort ) import Data.Maybe ( fromMaybe ) import qualified Data.Text as T@@ -103,7 +104,7 @@ (fromMaybe mempty queryString) request payloadHash- return+ pure $ SpacesRequest { method = reqMethod, headers = headers <> newHeaders, .. } where@@ -126,7 +127,7 @@ , request & H.requestHeaders & canonicalizeHeaders- & unCanonicalized+ & wrappedTo , request & H.requestHeaders & joinHeaderNames , uncompute payloadHash ]
src/Network/DO/Spaces/Types.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -16,6 +17,8 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -Wno-unticked-promoted-constructors #-}+ -- | -- Module : Network.DO.Spaces.Types -- Copyright : (c) 2021 Rory Tyler Hayford@@ -58,6 +61,7 @@ , BodyBS , Method(..) , Region(..)+ , RequestID , CacheControl , ContentDisposition , ContentEncoding@@ -132,13 +136,17 @@ import Network.HTTP.Types.Status ( Status ) import Network.Mime ( MimeType ) +import Web.HttpApiData ( ToHttpApiData )+ newtype SpacesT m a = SpacesT (ReaderT Spaces m a)- deriving ( Generic, Functor, Applicative, Monad, MonadIO, MonadThrow- , MonadCatch, MonadReader Spaces, MonadUnliftIO )+ deriving stock ( Generic )+ deriving newtype ( Functor, Applicative, Monad, MonadIO, MonadThrow+ , MonadCatch, MonadReader Spaces, MonadUnliftIO ) runSpacesT :: SpacesT m a -> Spaces -> m a runSpacesT (SpacesT x) = runReaderT x +-- | A synonym for the constraints necessary to run 'SpacesT' actions type MonadSpaces m = (MonadReader Spaces m, MonadIO m, MonadUnliftIO m, MonadCatch m) @@ -149,7 +157,7 @@ , region :: Region -- ^ The DO region , manager :: Manager -- ^ HTTP 'Manager' }- deriving ( Generic )+ deriving stock ( Generic ) instance HasHttpManager Spaces where getHttpManager = manager@@ -166,7 +174,7 @@ -- ^ The canonicalized HTTP 'Request' , time :: UTCTime }- deriving ( Generic )+ deriving stock ( Generic ) data SpacesRequestBuilder = SpacesRequestBuilder { spaces :: Spaces@@ -182,7 +190,7 @@ -- should be able to override the region configured in the 'Spaces' -- client }- deriving ( Generic )+ deriving stock ( Generic ) -- | DO regions where Spaces is available (only a subset of all regions) data Region@@ -191,16 +199,22 @@ | SanFrancisco -- ^ SFO3 | Singapore -- ^ SGP1 | Frankfurt -- ^ FRA1- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | HTTP request methods, to avoid using @http-client@'s stringly-typed @Method@ -- synonym-data Method = GET | POST | PUT | DELETE | HEAD- deriving ( Show, Eq, Generic, Ord, Read )+data Method+ = GET+ | POST+ | PUT+ | DELETE+ | HEAD+ deriving stock ( Show, Eq, Generic, Ord, Read ) -- | The name of a single storage bucket-newtype Bucket = Bucket { unBucket :: Text }- deriving ( Show, Eq, Generic )+newtype Bucket = Bucket Text+ deriving stock ( Show, Generic )+ deriving newtype ( Eq, ToHttpApiData ) -- | Smart constructor for 'Bucket's; names must conform to the following rules: --@@ -226,7 +240,7 @@ bucketErr "Name must begin with a letter or digit" | T.last t `elem` [ '.', '-' ] = bucketErr "Name must end with a letter or digit"- | otherwise = return . Bucket $ T.map toLower t+ | otherwise = pure . Bucket $ T.map toLower t where len = T.length t @@ -238,16 +252,18 @@ -- | Information about a single 'Bucket' data BucketInfo = BucketInfo { name :: Bucket, creationDate :: UTCTime }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | The name of a \"key\", in AWS parlance-newtype Object = Object { unObject :: Text }- deriving ( Show, Eq, Generic )+newtype Object = Object Text+ deriving stock ( Show, Generic )+ deriving newtype ( Eq, ToHttpApiData ) -- | Smart constructor for 'Object's; names must not be empty mkObject :: MonadThrow m => Text -> m Object-mkObject "" = throwM . OtherError $ "Object: Name must not be empty"-mkObject x = return $ Object x+mkObject t+ | T.null t = throwM . OtherError $ "Object: Name must not be empty"+ | otherwise = pure $ Object t -- | Information about a single 'Object', returned when listing a 'Bucket'\'s -- contents@@ -258,7 +274,7 @@ , size :: Int -- ^ Size in bytes , owner :: Owner }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | Metadata returned when querying information about an 'Object' data ObjectMetadata = ObjectMetadata@@ -267,15 +283,16 @@ , etag :: ETag , lastModified :: UTCTime }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | The resource owner-data Owner = Owner { id' :: OwnerID, displayName :: DisplayName }- deriving ( Show, Eq, Generic )+data Owner = Owner { ownerID :: OwnerID, displayName :: DisplayName }+ deriving stock ( Show, Eq, Generic ) -- | The ID of an 'Owner'; also serves as a display name in Spaces-newtype OwnerID = OwnerID { unOwnerID :: Int }- deriving ( Show, Eq, Generic, Num )+newtype OwnerID = OwnerID Int+ deriving stock ( Show, Generic )+ deriving newtype ( Eq, Num, ToHttpApiData ) -- | The display name is always equivalent to the owner's ID; Spaces includes -- it for AWS compatibility@@ -292,7 +309,7 @@ , contentEncoding :: Maybe ContentEncoding , metadata :: UserMetadata }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | @Cache-Control@ request header value type CacheControl = Text@@ -314,34 +331,36 @@ , allowedMethods :: [Method] , allowedHeaders :: [HeaderName] }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | An individual access grant data Grant = Grant { permission :: Permission, grantee :: Grantee }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | Access grant level; Spaces currently only supports these two levels-data Permission = ReadOnly | FullControl- deriving ( Show, Eq, Generic, Ord )+data Permission+ = ReadOnly+ | FullControl+ deriving stock ( Show, Eq, Generic, Ord ) -- | Information about who an access grant applies to data Grantee = Group -- ^ Nominally contains a URI value, but Spaces only supports a -- single value for group access grants | CanonicalUser Owner- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | A generic type for describing ACL configuration, can be applied to -- both 'Bucket' and 'Object' ACLs data ACLResponse = ACLResponse { owner :: Owner, accessControlList :: [Grant] }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) type Days = Word16 -- | Lifecycle configuration for a 'Bucket' data LifecycleRule = LifecycleRule- { id' :: LifecycleID+ { lifecycleID :: LifecycleID , enabled :: Bool -- ^ The status of the @LifecycleRule@ , prefix :: Maybe Text -- ^ When specified, only 'Object's which share the prefix will be affected@@ -350,15 +369,17 @@ , abortIncomplete :: Maybe Days -- ^ When specified, configures the deletion of incomplete multipart uploads }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | Configuration for automatically deleting expire 'Object's-data LifecycleExpiration = AfterDays Days | OnDate UTCTime- deriving ( Show, Eq, Generic )+data LifecycleExpiration+ = AfterDays Days+ | OnDate UTCTime+ deriving stock ( Show, Eq, Generic ) -- | A unique ID for a 'LifecycleRule' newtype LifecycleID = LifecycleID Text- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | Smart constructor for 'LifecycleID', which may contain a maximum of 255 -- characters, including spaces@@ -366,7 +387,7 @@ mkLifecycleID t | T.length t > 255 = throwM $ OtherError "LifecycleID: ID exceeds maximum length (255 chars)"- | otherwise = return $ LifecycleID t+ | otherwise = pure $ LifecycleID t -- | Smart constructor for 'CORSRule'. Ensures that both origins and header names -- contain a maximum of one wildcard and removes duplicates from both headers and@@ -377,20 +398,25 @@ $ OtherError "CORSRule: maximum of one wildcard permitted in origins" | or ((> 1) . C.count '*' . CI.original <$> hs) = throwM $ OtherError "CORSRule: maximum of one wildcard permitted in headers"- | otherwise = return CORSRule- { allowedOrigin = origin- , allowedMethods = nubOrd ms- , allowedHeaders = nubOrd hs- }+ | otherwise = pure CORSRule+ { allowedOrigin = origin+ , allowedMethods = nubOrd ms+ , allowedHeaders = nubOrd hs+ } -- | Represents some resource that has been canonicalized according to the -- Spaces/AWS v4 spec-newtype Canonicalized a = Canonicalized { unCanonicalized :: ByteString }- deriving ( Show, Eq, Generic )+newtype Canonicalized a = Canonicalized ByteString+ deriving stock ( Show, Eq, Generic ) -- | Different types of computed 'ByteString's-data ComputedTag = Hash | StrToSign | Sig | Cred | Auth- deriving ( Show, Eq )+data ComputedTag+ = Hash+ | StrToSign+ | Sig+ | Cred+ | Auth+ deriving stock ( Show, Eq ) -- | A strict 'ByteString' that has been computed according to some part of -- the AWS v4 spec. The AWS v4 signature is calculated from a series of@@ -401,20 +427,20 @@ -- unclear type signatures. Using a GADT with type synonyms is simpler than -- creating a @newtype@ for each type of computation data Computed (a :: ComputedTag) where- Hashed :: ByteString -> Computed 'Hash+ Hashed :: ByteString -> Computed Hash -- | Represents a \"string to sign\" that has been computed according to the -- Spaces/AWS v4 spec- StringToSign :: ByteString -> Computed 'StrToSign+ StringToSign :: ByteString -> Computed StrToSign -- | Signed hash of a 'Request' body, a 'SecretKey', and request information- Signature :: ByteString -> Computed 'Sig- Credentials :: ByteString -> Computed 'Cred+ Signature :: ByteString -> Computed Sig+ Credentials :: ByteString -> Computed Cred -- | Authorization string containing information about your 'AccessKey' and -- your request- Authorization :: ByteString -> Computed 'Auth+ Authorization :: ByteString -> Computed Auth -deriving instance Show (Computed a)+deriving stock instance Show (Computed a) -deriving instance Eq (Computed a)+deriving stock instance Eq (Computed a) type StringToSign = Computed 'StrToSign @@ -437,11 +463,11 @@ -- | Spaces access key newtype AccessKey = AccessKey { unAccessKey :: ByteString }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | Spaces secret key newtype SecretKey = SecretKey { unSecretKey :: ByteString }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | The name of a per-project configuration profile to select when loading -- credentials from a file@@ -457,7 +483,7 @@ -- A response, before being transformed into a 'ConsumedResponse' data RawResponse m = RawResponse { headers :: [Header], body :: BodyBS m }- deriving ( Generic )+ deriving stock ( Generic ) -- | A request or response body type BodyBS m = ConduitT () ByteString m ()@@ -471,11 +497,13 @@ , date :: Maybe UTCTime , status :: Status -- ^ HTTP status }- deriving ( Show, Eq, Generic )+ deriving stock ( Show, Eq, Generic ) -- | Whether or not to retain 'SpacesMetadata' when consuming responses-data WithMetadata = KeepMetadata | NoMetadata- deriving ( Show, Eq, Generic )+data WithMetadata+ = KeepMetadata+ | NoMetadata+ deriving stock ( Show, Eq, Generic ) -- | A 'ConsumedResponse' with optional 'SpacesMetadata' data SpacesResponse a = SpacesResponse@@ -485,12 +513,12 @@ -- ^ 'SpacesMetadata', the retention of which can be controlled using -- 'WithMetadata' }- deriving ( Generic )+ deriving stock ( Generic ) -deriving instance (Show (ConsumedResponse a)) => Show (SpacesResponse a)+deriving stock instance (Show (ConsumedResponse a)) => Show (SpacesResponse a) -- This instance is necessary to make the polymorphic @result@ field work with--- HasField+-- @HasField@ instance {-# OVERLAPPING #-}( GL.HasField' name (SpacesResponse a) s , s ~ t , a ~ b@@ -501,21 +529,24 @@ -- | A unique ID that is assigned to each request type RequestID = Text --- | How to discover 'AccessKey's and 'SecretKey's when creating a new 'Spaces'--- client.+-- | How to discover the 'Region', 'AccessKey', and 'SecretKey' when creating a+-- new 'Spaces' client. data CredentialSource- = FromEnv (Maybe (Text, Text)) -- ^ 'AccessKey' and 'SecretKey' env vars+ = Discover+ -- ^ Try a sequence of different sources until one succeeds+ | FromEnv (Maybe (Text, Text, Text))+ -- ^ 'Region', 'AccessKey' and 'SecretKey' env vars | FromFile FilePath (Maybe Profile) -- ^ Load your credentials from a file, optionally providing the profile -- to use (or @default@ as the... default).- | Explicit AccessKey SecretKey -- ^ Provide both keys explicitly+ | Explicit Region AccessKey SecretKey -- ^ Provide all values explicitly -- | \"Canned\" access controls; Spaces doesn't support the full range offered -- by s3 data CannedACL = Private -- ^ No unauthenticated public access | PublicRead -- ^ Unauthenticated public read access permitted- deriving ( Eq, Show )+ deriving stock ( Eq, Show ) -- | The base 'Exception' type for both 'ClientException's and 'APIException's data SpacesException = forall e. Exception e => SpacesException e@@ -536,13 +567,14 @@ -- | An exception generated within the 'Spaces' client data ClientException = InvalidRequest Text+ | InvalidResponse Text | InvalidXML Text | ConfigurationError Text -- | This includes the raw 'Network.HTTP.Types.Response' body, read into a -- lazy 'LB.ByteString' | HTTPStatus Status LB.ByteString | OtherError Text- deriving ( Show, Eq, Generic, Typeable )+ deriving stock ( Show, Eq, Generic, Typeable ) instance Exception ClientException where toException = spsExToException@@ -556,7 +588,7 @@ , requestID :: RequestID -- ^ The unique ID of the request , hostID :: Text }- deriving ( Show, Eq, Generic, Typeable )+ deriving stock ( Show, Eq, Generic, Typeable ) instance Exception APIException where toException = spsExToException
src/Network/DO/Spaces/Utils.hs view
@@ -117,14 +117,14 @@ Singapore -> "sgp1" Frankfurt -> "fra1" -slugToRegion :: MonadThrow m => Text -> m Region+slugToRegion :: (MonadThrow m, IsString a, Eq a) => a -> m Region slugToRegion = \case- "nyc3" -> return NewYork- "ams3" -> return Amsterdam- "sfo3" -> return SanFrancisco- "sgp1" -> return Singapore- "fra1" -> return Frankfurt- reg -> throwM . OtherError $ "Unrecognized region " <> quote reg+ "nyc3" -> pure NewYork+ "ams3" -> pure Amsterdam+ "sfo3" -> pure SanFrancisco+ "sgp1" -> pure Singapore+ "fra1" -> pure Frankfurt+ _ -> throwM . OtherError $ "Unrecognized region " -- | Map 'ByteString' chars to lower-case toLowerBS :: ByteString -> ByteString@@ -156,12 +156,12 @@ FullControl -> "FULL_CONTROL" handleMaybe :: MonadCatch m => (a -> m b) -> a -> m (Maybe b)-handleMaybe g x = handleAll (\_ -> return Nothing) (Just <$> g x)+handleMaybe g x = handleAll (\_ -> pure Nothing) (Just <$> g x) -- | Convert a 'RequestBody' to a 'Data.ByteString.Lazy.ByteString' bodyLBS :: MonadThrow m => RequestBody -> m LB.ByteString-bodyLBS (RequestBodyBS b) = return $ LB.fromStrict b-bodyLBS (RequestBodyLBS lb) = return lb+bodyLBS (RequestBodyBS b) = pure $ LB.fromStrict b+bodyLBS (RequestBodyLBS lb) = pure lb bodyLBS _ = throwM $ InvalidRequest "Unsupported request body type" @@ -186,9 +186,9 @@ -- | XML parser for 'Owner' attribute ownerP :: MonadThrow m => Cursor Node -> m Owner ownerP c = do- id' <- X.forceM (xmlElemError "ID")+ ownerID <- X.forceM (xmlElemError "ID") $ c $/ X.laxElement "ID" &/ X.content &| xmlInt @_ @OwnerID- return Owner { displayName = id', id' }+ pure Owner { displayName = ownerID, ownerID } -- | XML parser for 'ETag' attribute etagP :: MonadThrow m => Cursor Node -> m ETag@@ -203,7 +203,7 @@ -- | Read a 'Num' type from 'Text' xmlInt :: (MonadThrow m, Num a) => Text -> m a xmlInt txt = case readMaybe $ T.unpack txt of- Just n -> return $ fromInteger n+ Just n -> pure $ fromInteger n Nothing -> throwM $ InvalidXML "Failed to read integer value" -- | Read a 'Num' type, encoded as an integer, from XML@@ -214,7 +214,7 @@ -- | Read a 'UTCTime' from an ISO-O8601-formatted 'Text' xmlUTCTime :: MonadThrow m => Text -> m UTCTime xmlUTCTime txt = case iso8601ParseM $ T.unpack txt of- Just t -> return t+ Just t -> pure t Nothing -> throwM $ InvalidXML "Failed to read ISO-8601 value" isTruncP :: MonadThrow m => Cursor Node -> m Bool@@ -250,27 +250,27 @@ <*> (readEtag =<< lookupHeader' "Etag") <*> (readDate =<< lookupHeader' "Last-Modified") case metadata of- Just md -> return md+ Just md -> pure md Nothing -> throwM $ OtherError "Missing/malformed headers" where lookupHeader' = lookupHeader raw - readDate = MaybeT . return . parseAmzTime . C.unpack+ readDate = MaybeT . pure . parseAmzTime . C.unpack parseAmzTime :: [Char] -> Maybe UTCTime parseAmzTime = parseTimeM True defaultTimeLocale "%a, %d %b %Y %H:%M:%S %EZ" -- | Lookup the value of a 'HeaderName' from a 'RawResponse' in a monadic context lookupHeader :: Monad m => RawResponse m -> HeaderName -> MaybeT m ByteString-lookupHeader raw = MaybeT . return . flip lookup (raw ^. field @"headers")+lookupHeader raw = MaybeT . pure . flip lookup (raw ^. field @"headers") -- | Transform a 'Header' value into an 'ETag' readEtag :: Monad m => ByteString -> MaybeT m ETag-readEtag = MaybeT . return . fmap unquote . eitherToMaybe . T.decodeUtf8'+readEtag = MaybeT . pure . fmap unquote . eitherToMaybe . T.decodeUtf8' -- | Transform a 'Header' value into an 'Int' (for @Content-Length@) readContentLen :: Monad m => ByteString -> MaybeT m Int-readContentLen = MaybeT . return . readMaybe @Int . C.unpack+readContentLen = MaybeT . pure . readMaybe @Int . C.unpack aclP :: MonadThrow m => Cursor Node -> m ACLResponse aclP cursor = ACLResponse@@ -289,14 +289,14 @@ $ c $/ X.laxElement "Grantee" &| granteeP) where readPerm = \case- "FULL_CONTROL" -> return FullControl- "READ" -> return ReadOnly+ "FULL_CONTROL" -> pure FullControl+ "READ" -> pure ReadOnly _ -> throwM $ InvalidXML "Unrecognized ACL Permission" granteeP c = case X.node c of X.NodeElement (X.Element _ as _) -> case M.lookup typeName as of- Just "Group" -> return Group+ Just "Group" -> pure Group Just "CanonicalUser" -> CanonicalUser <$> ownerP c _ -> throwM $ InvalidXML "Invalid ACL Grantee type" _ -> throwM $ InvalidXML "Invalid ACL Grantee"@@ -322,7 +322,7 @@ $ X.Element "Owner" mempty [ mkNode "ID"- (r ^. field' @"owner" . field @"id'"+ (r ^. field' @"owner" . field @"ownerID" & coerce @_ @Int & tshow) ]@@ -344,7 +344,7 @@ $ X.Element "Grantee" (granteeAttrs "CanonicalUser") [ mkNode "ID"- (owner ^. field @"id'"+ (owner ^. field @"ownerID" & coerce @_ @Int & tshow) ]
tests/Main.hs view
@@ -3,7 +3,6 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeApplications #-} --- | module Main where import Conduit ( sourceLazy, withSourceFile )@@ -86,8 +85,7 @@ , "266d2fb56a251205c42c7e0deb7d2e370574cf190f366ecf53179c27697c8e38" ] - sig =- Signature "3d0da77e916e588d05f0190f8c350eddb47337953897b1e0cfdb44075fd6b2b9"+ sig = Signature "3d0da77e916e588d05f0190f8c350eddb47337953897b1e0cfdb44075fd6b2b9" auth = Authorization $ mconcat [ "AWS4-HMAC-SHA256 Credential="@@ -118,8 +116,8 @@ testSpaces :: IO Spaces testSpaces =- newSpaces Singapore- (Explicit (AccessKey "II5JDQBAN3JYM4DNEB6C")+ newSpaces (Explicit Singapore+ (AccessKey "II5JDQBAN3JYM4DNEB6C") (SecretKey "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")) errorResponse :: IO ()@@ -440,14 +438,14 @@ . it "parses the response correctly" $ (rs ^. #rules) `shouldBe` [ LifecycleRule- { id' = LifecycleID "Expire old logs"+ { lifecycleID = LifecycleID "Expire old logs" , enabled = True , expiration = Just (AfterDays 90) , prefix = Just "logs/" , abortIncomplete = Nothing } , LifecycleRule- { id' =+ { lifecycleID = LifecycleID "Remove uncompleted uploads" , enabled = True , abortIncomplete = Just 1