packages feed

aws 0.10.3 → 0.10.4

raw patch · 13 files changed

+1042/−48 lines, 13 filesdep +http-clientdep +transformers-basedep ~aesondep ~basedep ~bytestringnew-component:exe:MultipartUploadPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: http-client, transformers-base

Dependency ranges changed: aeson, base, bytestring, conduit, conduit-extra, errors, http-conduit, lifted-base, mtl, resourcet, tagged, text, time, transformers

API changes (from Hackage documentation)

+ Aws.S3.Commands.Multipart: AbortMultipartUpload :: Bucket -> Object -> Text -> AbortMultipartUpload
+ Aws.S3.Commands.Multipart: AbortMultipartUploadResponse :: AbortMultipartUploadResponse
+ Aws.S3.Commands.Multipart: CompleteMultipartUpload :: Bucket -> Object -> Text -> [(Integer, Text)] -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> CompleteMultipartUpload
+ Aws.S3.Commands.Multipart: CompleteMultipartUploadResponse :: Text -> Bucket -> Text -> Text -> CompleteMultipartUploadResponse
+ Aws.S3.Commands.Multipart: InitiateMultipartUpload :: Bucket -> Object -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int -> [(Text, Text)] -> Maybe StorageClass -> Maybe Text -> Maybe CannedAcl -> Maybe ServerSideEncryption -> Bool -> InitiateMultipartUpload
+ Aws.S3.Commands.Multipart: InitiateMultipartUploadResponse :: Bucket -> Text -> Text -> InitiateMultipartUploadResponse
+ Aws.S3.Commands.Multipart: UploadPart :: Text -> Bucket -> Integer -> Text -> Maybe ByteString -> Maybe (Digest MD5) -> Maybe ServerSideEncryption -> RequestBody -> UploadPart
+ Aws.S3.Commands.Multipart: UploadPartResponse :: Maybe Text -> UploadPartResponse
+ Aws.S3.Commands.Multipart: amuBucket :: AbortMultipartUpload -> Bucket
+ Aws.S3.Commands.Multipart: amuObjectName :: AbortMultipartUpload -> Object
+ Aws.S3.Commands.Multipart: amuUploadId :: AbortMultipartUpload -> Text
+ Aws.S3.Commands.Multipart: bstr2str :: ByteString -> String
+ Aws.S3.Commands.Multipart: chunkedConduit :: MonadResource m => Integer -> Conduit ByteString m ByteString
+ Aws.S3.Commands.Multipart: cmuBucket :: CompleteMultipartUpload -> Bucket
+ Aws.S3.Commands.Multipart: cmuExpiration :: CompleteMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: cmuObjectName :: CompleteMultipartUpload -> Object
+ Aws.S3.Commands.Multipart: cmuPartNumberAndEtags :: CompleteMultipartUpload -> [(Integer, Text)]
+ Aws.S3.Commands.Multipart: cmuServerSideEncryption :: CompleteMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: cmuServerSideEncryptionCustomerAlgorithm :: CompleteMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: cmuUploadId :: CompleteMultipartUpload -> Text
+ Aws.S3.Commands.Multipart: cmuVersionId :: CompleteMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: cmurBucket :: CompleteMultipartUploadResponse -> Bucket
+ Aws.S3.Commands.Multipart: cmurETag :: CompleteMultipartUploadResponse -> Text
+ Aws.S3.Commands.Multipart: cmurKey :: CompleteMultipartUploadResponse -> Text
+ Aws.S3.Commands.Multipart: cmurLocation :: CompleteMultipartUploadResponse -> Text
+ Aws.S3.Commands.Multipart: data AbortMultipartUpload
+ Aws.S3.Commands.Multipart: data AbortMultipartUploadResponse
+ Aws.S3.Commands.Multipart: data CompleteMultipartUpload
+ Aws.S3.Commands.Multipart: data CompleteMultipartUploadResponse
+ Aws.S3.Commands.Multipart: data InitiateMultipartUpload
+ Aws.S3.Commands.Multipart: data InitiateMultipartUploadResponse
+ Aws.S3.Commands.Multipart: data UploadPart
+ Aws.S3.Commands.Multipart: data UploadPartResponse
+ Aws.S3.Commands.Multipart: getUploadId :: Configuration -> S3Configuration NormalQuery -> Manager -> Text -> Text -> ResourceT IO Text
+ Aws.S3.Commands.Multipart: imuAcl :: InitiateMultipartUpload -> Maybe CannedAcl
+ Aws.S3.Commands.Multipart: imuAutoMakeBucket :: InitiateMultipartUpload -> Bool
+ Aws.S3.Commands.Multipart: imuBucket :: InitiateMultipartUpload -> Bucket
+ Aws.S3.Commands.Multipart: imuCacheControl :: InitiateMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: imuContentDisposition :: InitiateMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: imuContentEncoding :: InitiateMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: imuContentType :: InitiateMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: imuExpires :: InitiateMultipartUpload -> Maybe Int
+ Aws.S3.Commands.Multipart: imuMetadata :: InitiateMultipartUpload -> [(Text, Text)]
+ Aws.S3.Commands.Multipart: imuObjectName :: InitiateMultipartUpload -> Object
+ Aws.S3.Commands.Multipart: imuServerSideEncryption :: InitiateMultipartUpload -> Maybe ServerSideEncryption
+ Aws.S3.Commands.Multipart: imuStorageClass :: InitiateMultipartUpload -> Maybe StorageClass
+ Aws.S3.Commands.Multipart: imuWebsiteRedirectLocation :: InitiateMultipartUpload -> Maybe Text
+ Aws.S3.Commands.Multipart: imurBucket :: InitiateMultipartUploadResponse -> Bucket
+ Aws.S3.Commands.Multipart: imurKey :: InitiateMultipartUploadResponse -> Text
+ Aws.S3.Commands.Multipart: imurUploadId :: InitiateMultipartUploadResponse -> Text
+ Aws.S3.Commands.Multipart: instance AsMemoryResponse AbortMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance AsMemoryResponse CompleteMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance AsMemoryResponse InitiateMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance AsMemoryResponse UploadPartResponse
+ Aws.S3.Commands.Multipart: instance ResponseConsumer UploadPart UploadPartResponse
+ Aws.S3.Commands.Multipart: instance ResponseConsumer r AbortMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance ResponseConsumer r CompleteMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance ResponseConsumer r InitiateMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance Show AbortMultipartUpload
+ Aws.S3.Commands.Multipart: instance Show CompleteMultipartUpload
+ Aws.S3.Commands.Multipart: instance Show InitiateMultipartUpload
+ Aws.S3.Commands.Multipart: instance Show UploadPartResponse
+ Aws.S3.Commands.Multipart: instance SignQuery AbortMultipartUpload
+ Aws.S3.Commands.Multipart: instance SignQuery CompleteMultipartUpload
+ Aws.S3.Commands.Multipart: instance SignQuery InitiateMultipartUpload
+ Aws.S3.Commands.Multipart: instance SignQuery UploadPart
+ Aws.S3.Commands.Multipart: instance Transaction AbortMultipartUpload AbortMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance Transaction CompleteMultipartUpload CompleteMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance Transaction InitiateMultipartUpload InitiateMultipartUploadResponse
+ Aws.S3.Commands.Multipart: instance Transaction UploadPart UploadPartResponse
+ Aws.S3.Commands.Multipart: multipartUpload :: Configuration -> S3Configuration NormalQuery -> Manager -> Text -> Text -> Conduit () (ResourceT IO) ByteString -> Integer -> ResourceT IO ()
+ Aws.S3.Commands.Multipart: postAbortMultipartUpload :: Bucket -> Text -> Text -> AbortMultipartUpload
+ Aws.S3.Commands.Multipart: postCompleteMultipartUpload :: Bucket -> Text -> Text -> [(Integer, Text)] -> CompleteMultipartUpload
+ Aws.S3.Commands.Multipart: postInitiateMultipartUpload :: Bucket -> Text -> InitiateMultipartUpload
+ Aws.S3.Commands.Multipart: putConduit :: MonadResource m => Configuration -> S3Configuration NormalQuery -> Manager -> Text -> Text -> Text -> Conduit ByteString m String
+ Aws.S3.Commands.Multipart: sendEtag :: Configuration -> S3Configuration NormalQuery -> Manager -> Text -> Text -> Text -> [String] -> ResourceT IO ()
+ Aws.S3.Commands.Multipart: upBucket :: UploadPart -> Bucket
+ Aws.S3.Commands.Multipart: upContentMD5 :: UploadPart -> Maybe (Digest MD5)
+ Aws.S3.Commands.Multipart: upContentType :: UploadPart -> Maybe ByteString
+ Aws.S3.Commands.Multipart: upObjectName :: UploadPart -> Text
+ Aws.S3.Commands.Multipart: upPartNumber :: UploadPart -> Integer
+ Aws.S3.Commands.Multipart: upRequestBody :: UploadPart -> RequestBody
+ Aws.S3.Commands.Multipart: upServerSideEncryption :: UploadPart -> Maybe ServerSideEncryption
+ Aws.S3.Commands.Multipart: upUploadId :: UploadPart -> Text
+ Aws.S3.Commands.Multipart: uploadPart :: Bucket -> Text -> Integer -> Text -> RequestBody -> UploadPart
+ Aws.S3.Commands.Multipart: uprVersionId :: UploadPartResponse -> Maybe Text
+ Aws.Ses.Core: sesEuWest1 :: ByteString
+ Aws.Ses.Core: sesUsEast1 :: ByteString
+ Aws.Ses.Core: sesUsWest2 :: ByteString
- Aws.DynamoDb.Commands.Table: TableDescription :: Text -> Integer -> Text -> UTCTime -> Integer -> [AttributeDefinition] -> KeySchema -> ProvisionedThroughputStatus -> [LocalSecondaryIndexStatus] -> [GlobalSecondaryIndexStatus] -> TableDescription
+ Aws.DynamoDb.Commands.Table: TableDescription :: Text -> Integer -> Text -> Maybe UTCTime -> Integer -> [AttributeDefinition] -> Maybe KeySchema -> ProvisionedThroughputStatus -> [LocalSecondaryIndexStatus] -> [GlobalSecondaryIndexStatus] -> TableDescription
- Aws.DynamoDb.Commands.Table: rCreationDateTime :: TableDescription -> UTCTime
+ Aws.DynamoDb.Commands.Table: rCreationDateTime :: TableDescription -> Maybe UTCTime
- Aws.DynamoDb.Commands.Table: rKeySchema :: TableDescription -> KeySchema
+ Aws.DynamoDb.Commands.Table: rKeySchema :: TableDescription -> Maybe KeySchema

Files

Aws/Aws.hs view
@@ -209,12 +209,18 @@ unsafeAwsRef cfg info manager metadataRef request = do   sd <- liftIO $ signatureData <$> timeInfo <*> credentials $ cfg   let q = signQuery request info sd-  liftIO $ logger cfg Debug $ T.pack $ "String to sign: " ++ show (sqStringToSign q)+  let logDebug = liftIO . logger cfg Debug . T.pack+  logDebug $ "String to sign: " ++ show (sqStringToSign q)   httpRequest <- liftIO $ queryToHttpRequest q-  liftIO $ logger cfg Debug $ T.pack $ "Host: " ++ show (HTTP.host httpRequest)-  liftIO $ logger cfg Debug $ T.pack $ "Path: " ++ show (HTTP.path httpRequest)-  liftIO $ logger cfg Debug $ T.pack $ "Query string: " ++ show (HTTP.queryString httpRequest)+  logDebug $ "Host: " ++ show (HTTP.host httpRequest)+  logDebug $ "Path: " ++ show (HTTP.path httpRequest)+  logDebug $ "Query string: " ++ show (HTTP.queryString httpRequest)+  case HTTP.requestBody httpRequest of+    HTTP.RequestBodyLBS lbs -> logDebug $ "Body: " ++ show lbs+    HTTP.RequestBodyBS bs -> logDebug $ "Body: " ++ show bs+    _ -> return ()   hresp <- HTTP.http httpRequest manager+  logDebug $ "Response status: " ++ show (HTTP.responseStatus hresp)   forM_ (HTTP.responseHeaders hresp) $ \(hname,hvalue) -> liftIO $     logger cfg Debug $ T.decodeUtf8 $ "Response header '" `mappend` CI.original hname `mappend` "': '" `mappend` hvalue `mappend` "'"   responseConsumer request metadataRef hresp
Aws/DynamoDb/Commands/Table.hs view
@@ -263,10 +263,10 @@         rTableName              :: T.Text       , rTableSizeBytes         :: Integer       , rTableStatus            :: T.Text -- ^ one of CREATING, UPDATING, DELETING, ACTIVE-      , rCreationDateTime       :: UTCTime+      , rCreationDateTime       :: Maybe UTCTime       , rItemCount              :: Integer       , rAttributeDefinitions   :: [AttributeDefinition]-      , rKeySchema              :: KeySchema+      , rKeySchema              :: Maybe KeySchema       , rProvisionedThroughput  :: ProvisionedThroughputStatus       , rLocalSecondaryIndexes  :: [LocalSecondaryIndexStatus]       , rGlobalSecondaryIndexes :: [GlobalSecondaryIndexStatus]@@ -282,10 +282,10 @@         TableDescription <$> t .: "TableName"                          <*> t .: "TableSizeBytes"                          <*> t .: "TableStatus"-                         <*> (posixSecondsToUTCTime . fromInteger <$> t .: "CreationDateTime")+                         <*> (fmap (posixSecondsToUTCTime . fromInteger) <$> t .:? "CreationDateTime")                          <*> t .: "ItemCount"-                         <*> t .: "AttributeDefinitions"-                         <*> t .: "KeySchema"+                         <*> t .:? "AttributeDefinitions" .!= []+                         <*> t .:? "KeySchema"                          <*> t .: "ProvisionedThroughput"                          <*> t .:? "LocalSecondaryIndexes" .!= []                          <*> t .:? "GlobalSecondaryIndexes" .!= []@@ -391,7 +391,12 @@       }     deriving (Show, Generic) instance A.ToJSON UpdateTable where-    toJSON = A.genericToJSON $ dropOpt 6+    toJSON a = A.object+        $ "TableName" .= updateTableName a+        : "ProvisionedThroughput" .= updateProvisionedThroughput a+        : case updateGlobalSecondaryIndexUpdates a of+            [] -> []+            l -> [ "GlobalSecondaryIndexUpdates" .= l ]  -- | ServiceConfiguration: 'DdbConfiguration' instance SignQuery UpdateTable where
Aws/DynamoDb/Core.hs view
@@ -114,6 +114,7 @@ import qualified Data.Aeson                   as A import           Data.Aeson.Types             (Pair, parseEither) import qualified Data.Aeson.Types             as A+import qualified Data.Attoparsec.ByteString   as AttoB (endOfInput) import qualified Data.Attoparsec.Text         as Atto import           Data.Byteable import qualified Data.ByteString.Base16       as Base16@@ -826,7 +827,7 @@ ------------------------------------------------------------------------------- ddbResponseConsumer :: A.FromJSON a => IORef DdbResponse -> HTTPResponseConsumer a ddbResponseConsumer ref resp = do-    val <- HTTP.responseBody resp $$+- sinkParser A.json'+    val <- HTTP.responseBody resp $$+- sinkParser (A.json' <* AttoB.endOfInput)     case statusCode of       200 -> rSuccess val       _   -> rError val@@ -973,10 +974,13 @@   instance ToJSON CondOp where-    toJSON c = object-      [ "ComparisonOperator" .= String (renderCondOp c)-      , "AttributeValueList" .= getCondValues c ]-+    toJSON c = object $ ("ComparisonOperator" .= String (renderCondOp c)) : valueList+      where+        valueList =+          let vs = getCondValues c in+            if null vs+            then []+            else ["AttributeValueList" .= vs]  ------------------------------------------------------------------------------- dyApiVersion :: B.ByteString
Aws/S3/Commands.hs view
@@ -11,6 +11,7 @@ , module Aws.S3.Commands.HeadObject , module Aws.S3.Commands.PutBucket , module Aws.S3.Commands.PutObject+, module Aws.S3.Commands.Multipart ) where @@ -25,3 +26,4 @@ import Aws.S3.Commands.HeadObject import Aws.S3.Commands.PutBucket import Aws.S3.Commands.PutObject+import Aws.S3.Commands.Multipart
+ Aws/S3/Commands/Multipart.hs view
@@ -0,0 +1,430 @@+module Aws.S3.Commands.Multipart+       where+import           Aws.Aws+import           Aws.Core+import           Aws.S3.Core+import           Control.Applicative+import           Control.Arrow         (second)+import           Control.Monad.Trans.Resource+import           Crypto.Hash+import qualified Crypto.Hash.MD5       as MD5+import           Data.ByteString.Char8 ({- IsString -})+import           Data.Conduit+import qualified Data.Conduit.Binary   as CB+import qualified Data.Conduit.List     as CL+import           Data.Maybe+import           Text.XML.Cursor       (($/))+import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString.Lazy  as BL+import qualified Data.CaseInsensitive  as CI+import qualified Data.Map              as M+import qualified Data.Text             as T+import qualified Data.Text.Encoding    as T+import qualified Network.HTTP.Conduit  as HTTP+import qualified Network.HTTP.Types    as HTTP+import           Text.Printf(printf)+import qualified Text.XML              as XML++{-+Aws supports following 6 api for Multipart-Upload.+Currently this code does not support number 3 and 6.++1. Initiate Multipart Upload+2. Upload Part+3. Upload Part - Copy+4. Complete Multipart Upload+5. Abort Multipart Upload+6. List Parts++-}++data InitiateMultipartUpload+  = InitiateMultipartUpload {+      imuBucket :: Bucket+    , imuObjectName :: Object+    , imuCacheControl :: Maybe T.Text+    , imuContentDisposition :: Maybe T.Text+    , imuContentEncoding :: Maybe T.Text+    , imuContentType :: Maybe T.Text+    , imuExpires :: Maybe Int+    , imuMetadata :: [(T.Text,T.Text)]+    , imuStorageClass :: Maybe StorageClass+    , imuWebsiteRedirectLocation :: Maybe T.Text+    , imuAcl :: Maybe CannedAcl+    , imuServerSideEncryption :: Maybe ServerSideEncryption+    , imuAutoMakeBucket :: Bool -- ^ Internet Archive S3 nonstandard extension+    }+  deriving (Show)++postInitiateMultipartUpload :: Bucket -> T.Text -> InitiateMultipartUpload+postInitiateMultipartUpload b o =+  InitiateMultipartUpload+    b o+    Nothing Nothing Nothing Nothing Nothing+    [] Nothing Nothing Nothing Nothing+    False++data InitiateMultipartUploadResponse+  = InitiateMultipartUploadResponse {+      imurBucket   :: Bucket+    , imurKey      :: T.Text+    , imurUploadId :: T.Text+    }++-- | ServiceConfiguration: 'S3Configuration'+instance SignQuery InitiateMultipartUpload where+    type ServiceConfiguration InitiateMultipartUpload = S3Configuration+    signQuery InitiateMultipartUpload {..} = s3SignQuery S3Query {+        s3QMethod = Post+      , s3QBucket = Just $ T.encodeUtf8 imuBucket+      , s3QObject = Just $ T.encodeUtf8 $ imuObjectName+      , s3QSubresources = HTTP.toQuery[ ("uploads" :: B8.ByteString , Nothing :: Maybe B8.ByteString)]+      , s3QQuery = []+      , s3QContentType = T.encodeUtf8 <$> imuContentType+      , s3QContentMd5 = Nothing+      , s3QAmzHeaders = map (second T.encodeUtf8) $ catMaybes [+          ("x-amz-acl",) <$> writeCannedAcl <$> imuAcl+        , ("x-amz-storage-class",) <$> writeStorageClass <$> imuStorageClass+        , ("x-amz-website-redirect-location",) <$> imuWebsiteRedirectLocation+        , ("x-amz-server-side-encryption",) <$> writeServerSideEncryption <$> imuServerSideEncryption+        , if imuAutoMakeBucket then Just ("x-amz-auto-make-bucket", "1")  else Nothing+        ] ++ map( \x -> (CI.mk . T.encodeUtf8 $ T.concat ["x-amz-meta-", fst x], snd x)) imuMetadata+      , s3QOtherHeaders = map (second T.encodeUtf8) $ catMaybes [+          ("Expires",) . T.pack . show <$> imuExpires+        , ("Cache-Control",) <$> imuCacheControl+        , ("Content-Disposition",) <$> imuContentDisposition+        , ("Content-Encoding",) <$> imuContentEncoding+        ]+      , s3QRequestBody = Nothing+      }++instance ResponseConsumer r InitiateMultipartUploadResponse where+    type ResponseMetadata InitiateMultipartUploadResponse = S3Metadata++    responseConsumer _ = s3XmlResponseConsumer parse+        where parse cursor+                  = do bucket <- force "Missing Bucket Name" $ cursor $/ elContent "Bucket"+                       key <- force "Missing Key" $ cursor $/ elContent "Key"+                       uploadId <- force "Missing UploadID" $ cursor $/ elContent "UploadId"+                       return InitiateMultipartUploadResponse{+                                                imurBucket         = bucket+                                              , imurKey            = key+                                              , imurUploadId       = uploadId+                                              }++instance Transaction InitiateMultipartUpload InitiateMultipartUploadResponse++instance AsMemoryResponse InitiateMultipartUploadResponse where+    type MemoryResponse InitiateMultipartUploadResponse = InitiateMultipartUploadResponse+    loadToMemory = return+++----------------------------------++++data UploadPart = UploadPart {+    upObjectName :: T.Text+  , upBucket :: Bucket+  , upPartNumber :: Integer+  , upUploadId :: T.Text+  , upContentType :: Maybe B8.ByteString+  , upContentMD5 :: Maybe (Digest MD5)+  , upServerSideEncryption :: Maybe ServerSideEncryption+  , upRequestBody  :: HTTP.RequestBody+}++uploadPart :: Bucket -> T.Text -> Integer -> T.Text -> HTTP.RequestBody -> UploadPart+uploadPart bucket obj p i body =+  UploadPart obj bucket p i+  Nothing Nothing Nothing body++data UploadPartResponse+  = UploadPartResponse {+      uprVersionId :: Maybe T.Text+    }+  deriving (Show)++-- | ServiceConfiguration: 'S3Configuration'+instance SignQuery UploadPart where+    type ServiceConfiguration UploadPart = S3Configuration+    signQuery UploadPart {..} = s3SignQuery S3Query {+                                 s3QMethod = Put+                               , s3QBucket = Just $ T.encodeUtf8 upBucket+                               , s3QObject = Just $ T.encodeUtf8 upObjectName+                               , s3QSubresources = HTTP.toQuery[+                                   ("partNumber" :: B8.ByteString , Just (T.pack (show upPartNumber)) :: Maybe T.Text)+                                 , ("uploadId" :: B8.ByteString, Just upUploadId :: Maybe T.Text)+                                 ]+                               , s3QQuery = []+                               , s3QContentType = upContentType+                               , s3QContentMd5 = upContentMD5+                               , s3QAmzHeaders = map (second T.encodeUtf8) $ catMaybes [+                                   ("x-amz-server-side-encryption",) <$> writeServerSideEncryption <$> upServerSideEncryption+                                 ]+                               , s3QOtherHeaders = []+                               , s3QRequestBody = Just upRequestBody+                               }++instance ResponseConsumer UploadPart UploadPartResponse where+    type ResponseMetadata UploadPartResponse = S3Metadata+    responseConsumer _ = s3ResponseConsumer $ \resp -> do+      let vid = T.decodeUtf8 `fmap` lookup "x-amz-version-id" (HTTP.responseHeaders resp)+      return $ UploadPartResponse vid++instance Transaction UploadPart UploadPartResponse++instance AsMemoryResponse UploadPartResponse where+    type MemoryResponse UploadPartResponse = UploadPartResponse+    loadToMemory = return++----------------------------++++data CompleteMultipartUpload+  = CompleteMultipartUpload {+      cmuBucket :: Bucket+    , cmuObjectName :: Object+    , cmuUploadId :: T.Text+    , cmuPartNumberAndEtags :: [(Integer,T.Text)]+    , cmuExpiration :: Maybe T.Text+    , cmuServerSideEncryption :: Maybe T.Text+    , cmuServerSideEncryptionCustomerAlgorithm :: Maybe T.Text+    , cmuVersionId :: Maybe T.Text+    }+  deriving (Show)++postCompleteMultipartUpload :: Bucket -> T.Text -> T.Text -> [(Integer,T.Text)]-> CompleteMultipartUpload+postCompleteMultipartUpload b o i p = CompleteMultipartUpload b o i p Nothing  Nothing  Nothing  Nothing++data CompleteMultipartUploadResponse+  = CompleteMultipartUploadResponse {+      cmurLocation :: T.Text+    , cmurBucket   :: Bucket+    , cmurKey      :: T.Text+    , cmurETag     :: T.Text+    }++-- | ServiceConfiguration: 'S3Configuration'+instance SignQuery CompleteMultipartUpload where+    type ServiceConfiguration CompleteMultipartUpload = S3Configuration+    signQuery CompleteMultipartUpload {..} = s3SignQuery S3Query {+      s3QMethod = Post+      , s3QBucket = Just $ T.encodeUtf8 cmuBucket+      , s3QObject = Just $ T.encodeUtf8 cmuObjectName+      , s3QSubresources = HTTP.toQuery[+        ("uploadId" :: B8.ByteString, Just cmuUploadId :: Maybe T.Text)+        ]+      , s3QQuery = []+      , s3QContentType = Nothing+      , s3QContentMd5 = Nothing+      , s3QAmzHeaders = catMaybes [ ("x-amz-expiration",) <$> (T.encodeUtf8 <$> cmuExpiration)+                                  , ("x-amz-server-side-encryption",) <$> (T.encodeUtf8 <$> cmuServerSideEncryption)+                                  , ("x-amz-server-side-encryption-customer-algorithm",)+                                    <$> (T.encodeUtf8 <$> cmuServerSideEncryptionCustomerAlgorithm)+                                  , ("x-amz-version-id",) <$> (T.encodeUtf8 <$> cmuVersionId)+                                  ]+      , s3QOtherHeaders = []+      , s3QRequestBody  = Just $ HTTP.RequestBodyLBS reqBody+      }+        where reqBody = XML.renderLBS XML.def XML.Document {+                    XML.documentPrologue = XML.Prologue [] Nothing []+                  , XML.documentRoot = root+                  , XML.documentEpilogue = []+                  }+              root = XML.Element {+                    XML.elementName = "CompleteMultipartUpload"+                  , XML.elementAttributes = M.empty+                  , XML.elementNodes = (partNode <$> cmuPartNumberAndEtags)+                  }+              partNode (partNumber, etag) = XML.NodeElement XML.Element {+                    XML.elementName = "Part"+                  , XML.elementAttributes = M.empty+                  , XML.elementNodes = [keyNode (T.pack (show partNumber)),etagNode etag]+                  }+              etagNode = toNode "ETag"+              keyNode     = toNode "PartNumber"+              toNode name content = XML.NodeElement XML.Element {+                    XML.elementName = name+                  , XML.elementAttributes = M.empty+                  , XML.elementNodes = [XML.NodeContent content]+                  }++instance ResponseConsumer r CompleteMultipartUploadResponse where+    type ResponseMetadata CompleteMultipartUploadResponse = S3Metadata++    responseConsumer _ = s3XmlResponseConsumer parse+        where parse cursor+                  = do location <- force "Missing Location" $ cursor $/ elContent "Location"+                       bucket <- force "Missing Bucket Name" $ cursor $/ elContent "Bucket"+                       key <- force "Missing Key" $ cursor $/ elContent "Key"+                       etag <- force "Missing ETag" $ cursor $/ elContent "ETag"+                       return CompleteMultipartUploadResponse{+                                                cmurLocation       = location+                                              , cmurBucket         = bucket+                                              , cmurKey            = key+                                              , cmurETag           = etag+                                              }++instance Transaction CompleteMultipartUpload CompleteMultipartUploadResponse++instance AsMemoryResponse CompleteMultipartUploadResponse where+    type MemoryResponse CompleteMultipartUploadResponse = CompleteMultipartUploadResponse+    loadToMemory = return++----------------------------++++data AbortMultipartUpload+  = AbortMultipartUpload {+      amuBucket :: Bucket+    , amuObjectName :: Object+    , amuUploadId :: T.Text+    }+  deriving (Show)++postAbortMultipartUpload :: Bucket -> T.Text -> T.Text -> AbortMultipartUpload+postAbortMultipartUpload b o i = AbortMultipartUpload b o i++data AbortMultipartUploadResponse+  = AbortMultipartUploadResponse {+    }++-- | ServiceConfiguration: 'S3Configuration'+instance SignQuery AbortMultipartUpload where+    type ServiceConfiguration AbortMultipartUpload = S3Configuration+    signQuery AbortMultipartUpload {..} = s3SignQuery S3Query {+      s3QMethod = Delete+      , s3QBucket = Just $ T.encodeUtf8 amuBucket+      , s3QObject = Just $ T.encodeUtf8 amuObjectName+      , s3QSubresources = HTTP.toQuery[+        ("uploadId" :: B8.ByteString, Just amuUploadId :: Maybe T.Text)+        ]+      , s3QQuery = []+      , s3QContentType = Nothing+      , s3QContentMd5 = Nothing+      , s3QAmzHeaders = []+      , s3QOtherHeaders = []+      , s3QRequestBody = Nothing+      }++instance ResponseConsumer r AbortMultipartUploadResponse where+    type ResponseMetadata AbortMultipartUploadResponse = S3Metadata++    responseConsumer _ = s3XmlResponseConsumer parse+        where parse _cursor+                  = return AbortMultipartUploadResponse {}++instance Transaction AbortMultipartUpload AbortMultipartUploadResponse+++instance AsMemoryResponse AbortMultipartUploadResponse where+    type MemoryResponse AbortMultipartUploadResponse = AbortMultipartUploadResponse+    loadToMemory = return+++----------------------------++getUploadId ::+  Configuration+  -> S3Configuration NormalQuery+  -> HTTP.Manager+  -> T.Text+  -> T.Text+  -> ResourceT IO T.Text+getUploadId cfg s3cfg mgr bucket object = do+  InitiateMultipartUploadResponse {+      imurBucket = _bucket+    , imurKey = _object'+    , imurUploadId = uploadId+    } <- pureAws cfg s3cfg mgr $ postInitiateMultipartUpload bucket object+  return uploadId+++sendEtag  ::+  Configuration+  -> S3Configuration NormalQuery+  -> HTTP.Manager+  -> T.Text+  -> T.Text+  -> T.Text+  -> [String]+  -> ResourceT IO ()+sendEtag cfg s3cfg mgr bucket object uploadId etags = do+  _ <- pureAws cfg s3cfg mgr $+       postCompleteMultipartUpload bucket object uploadId (zip [1..] (map T.pack etags))+  return ()+++bstr2str :: B8.ByteString -> String+bstr2str bstr =+  foldr1 (++) $ map toHex $ B8.unpack bstr+  where+    toHex :: Char -> String+    toHex chr = printf "%02x" chr++putConduit ::+  MonadResource m =>+  Configuration+  -> S3Configuration NormalQuery+  -> HTTP.Manager+  -> T.Text+  -> T.Text+  -> T.Text+  -> Conduit B8.ByteString m String+putConduit cfg s3cfg mgr bucket object uploadId = loop 1+  where+    loop n = do+      v' <- await+      case v' of+        Just v -> do+          let str= (BL.fromStrict v)+          _ <- liftResourceT $ pureAws cfg s3cfg mgr $+            uploadPart bucket object n uploadId+            (HTTP.requestBodySource+             (BL.length str)+             (CB.sourceLbs str)+            )+          let etag= bstr2str $ MD5.hash v+          yield etag+          loop (n+1)+        Nothing -> return ()++chunkedConduit :: (MonadResource m) => Integer -> Conduit B8.ByteString m B8.ByteString+chunkedConduit size = do+  loop 0 ""+  where+    loop cnt str = do+      line' <- await+      case line' of +        Nothing -> do+          yield str+          return ()+        Just line -> do+          let len = (B8.length line)+cnt+          let newStr = B8.concat [str, line]+          if len >= (fromIntegral size)+            then do+            yield newStr+            loop 0 ""+            else+            loop len newStr++multipartUpload ::+  Configuration+  -> S3Configuration NormalQuery+  -> HTTP.Manager+  -> T.Text+  -> T.Text+  -> Conduit () (ResourceT IO) B8.ByteString+  -> Integer+  -> ResourceT IO ()+multipartUpload cfg s3cfg mgr bucket object src chunkSize = do+  uploadId <- getUploadId cfg s3cfg mgr bucket object+  etags <- src+           $= chunkedConduit chunkSize+           $= putConduit cfg s3cfg mgr bucket object uploadId+           $$ CL.consume+  sendEtag cfg s3cfg mgr bucket object uploadId etags
Aws/Ses/Core.hs view
@@ -3,7 +3,10 @@     , SesMetadata(..)      , SesConfiguration(..)+    , sesEuWest1     , sesUsEast+    , sesUsEast1+    , sesUsWest2     , sesHttpsGet     , sesHttpsPost @@ -70,13 +73,22 @@  -- HTTP is not supported right now, always use HTTPS instance DefaultServiceConfiguration (SesConfiguration NormalQuery) where-    defServiceConfig = sesHttpsPost sesUsEast+    defServiceConfig = sesHttpsPost sesUsEast1  instance DefaultServiceConfiguration (SesConfiguration UriOnlyQuery) where-    defServiceConfig = sesHttpsGet sesUsEast+    defServiceConfig = sesHttpsGet sesUsEast1 +sesEuWest1 :: B.ByteString+sesEuWest1 = "email.eu-west-1.amazonaws.com"+ sesUsEast :: B.ByteString-sesUsEast = "email.us-east-1.amazonaws.com"+sesUsEast = sesUsEast1++sesUsEast1 :: B.ByteString+sesUsEast1 = "email.us-east-1.amazonaws.com"++sesUsWest2 :: B.ByteString+sesUsWest2 = "email.us-west-2.amazonaws.com"  sesHttpsGet :: B.ByteString -> SesConfiguration qt sesHttpsGet endpoint = SesConfiguration Get endpoint
+ Examples/MultipartUpload.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++import qualified Aws+import qualified Aws.S3 as S3+import           Data.Conduit (($$+-))+import           Data.Conduit.Binary (sourceFile)+import qualified Data.Text as T+import           Network.HTTP.Conduit (withManager, responseBody)+import           System.Environment (getArgs)++main :: IO ()+main = do+  {- Set up AWS credentials and the default configuration. -}+  cfg <- Aws.baseConfiguration+  let s3cfg = Aws.defServiceConfig :: S3.S3Configuration Aws.NormalQuery++  args <- getArgs++  let doUpload bucket obj file chunkSize =+        withManager $ \mgr -> do+          S3.multipartUpload cfg s3cfg mgr (T.pack bucket) (T.pack obj) (sourceFile file) (chunkSize*1024*1024)++  case args of+    [bucket,obj,file] -> +      doUpload bucket obj file 10+    [bucket,obj,file,chunkSize] -> +      doUpload bucket obj file (read chunkSize)+    _ -> do+      putStrLn "Usage: MultipartUpload bucket objectname filename (chunksize(MB)::optinal)"
README.org view
@@ -90,6 +90,15 @@  ** 0.10 series +- 0.10.4+  - S3: support for multi-part uploads+  - DynamoDB: fixes for JSON serialization+      WARNING: This includes making some fields in TableDescription Maybe fields, which is breaking. But DynamoDB support was+               and is also marked as EXPERIMENTAL.+  - DynamoDB: TCP connection reuse where possible (improving performance)+  - DynamoDB: Added test suite+  - SES: support for additional regions+ - 0.10.3   - fix bug introduced in 0.10.2 that broke SQS and IAM connections without STS 
aws.cabal view
@@ -1,5 +1,5 @@ Name:                aws-Version:             0.10.3+Version:             0.10.4 Synopsis:            Amazon Web Services (AWS) for Haskell Description:         Bindings for Amazon Web Services (AWS), with the aim of supporting all AWS services. To see a high level overview of the library, see the README at <https://github.com/aristidb/aws/blob/master/README.org>. Homepage:            http://github.com/aristidb/aws@@ -20,7 +20,7 @@ Source-repository this   type: git   location: https://github.com/aristidb/aws.git-  tag: 0.10.3+  tag: 0.10.4  Source-repository head   type: git@@ -76,6 +76,7 @@                        Aws.S3.Commands.HeadObject                        Aws.S3.Commands.PutBucket                        Aws.S3.Commands.PutObject+                       Aws.S3.Commands.Multipart                        Aws.S3.Core                        Aws.Ses                        Aws.Ses.Commands@@ -182,6 +183,24 @@    Default-Language: Haskell2010 +Executable MultipartUpload+  Main-is: MultipartUpload.hs+  Hs-source-dirs: Examples++  if !flag(Examples)+    Buildable: False+  else+    Buildable: True+    Build-depends:+                       base == 4.*,+                       aws,+                       http-conduit,+                       conduit,+                       conduit-extra,+                       text++  Default-Language: Haskell2010+ Executable NukeBucket   Main-is: NukeBucket.hs   Hs-source-dirs: Examples@@ -269,13 +288,50 @@         base == 4.*,         bytestring >= 0.10,         errors >= 1.4.7,+        http-client >= 0.3,+        lifted-base >= 0.2,+        monad-control >= 0.3,         mtl >= 2.1,         quickcheck-instances >= 0.3,+        resourcet >= 1.1,         tagged >= 0.7,         tasty >= 0.8,         tasty-quickcheck >= 0.8,         text >= 1.1,-        transformers >= 0.3+        time,+        transformers >= 0.3,+        transformers-base >= 0.4      ghc-options: -Wall -threaded++test-suite dynamodb-tests+    type: exitcode-stdio-1.0+    default-language: Haskell2010+    hs-source-dirs: tests+    main-is: DynamoDb/Main.hs++    other-modules:+        Utils+        DynamoDb.Utils++    build-depends:+        QuickCheck >= 2.7,+        aeson >= 0.7,+        aws,+        base == 4.*,+        bytestring >= 0.10,+        errors >= 1.4.7,+        http-client >= 0.3,+        lifted-base >= 0.2,+        monad-control >= 0.3,+        mtl >= 2.1,+        quickcheck-instances >= 0.3,+        resourcet >= 1.1,+        tagged >= 0.7,+        tasty >= 0.8,+        tasty-quickcheck >= 0.8,+        text >= 1.1,+        time,+        transformers >= 0.3,+        transformers-base >= 0.4 
+ tests/DynamoDb/Main.hs view
@@ -0,0 +1,157 @@+-- ------------------------------------------------------ --+-- Copyright © 2014 AlephCloud Systems, Inc.+-- ------------------------------------------------------ --++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE FlexibleContexts #-}++-- |+-- Module: Main+-- Copyright: Copyright © 2014 AlephCloud Systems, Inc.+-- License: BSD3+-- Maintainer: Lars Kuhtz <lars@alephcloud.com>+-- Stability: experimental+--+-- Tests for Haskell AWS DynamoDb bindings+--++module Main+( main+) where++import Aws+import qualified Aws.DynamoDb as DY++import Control.Arrow (second)+import Control.Error+import Control.Monad+import Control.Monad.IO.Class++import Data.IORef+import qualified Data.List as L+import qualified Data.Text as T++import qualified Network.HTTP.Client as HTTP++import Test.Tasty+import Test.QuickCheck.Instances ()++import System.Environment+import System.Exit++import Utils+import DynamoDb.Utils++-- -------------------------------------------------------------------------- --+-- Main++main :: IO ()+main = do+    args <- getArgs+    runMain args $ map (second tail . span (/= '=')) args+  where+    runMain :: [String] -> [(String,String)] -> IO ()+    runMain args _argsMap+        | any (`elem` helpArgs) args = defaultMain tests+        | "--run-with-aws-credentials" `elem` args =+            withArgs (tastyArgs args) . defaultMain $ tests+        | otherwise = putStrLn help >> exitFailure++    helpArgs = ["--help", "-h"]+    mainArgs =+        [ "--run-with-aws-credentials"+        ]+    tastyArgs args = flip filter args $ \x -> not+        $ any (`L.isPrefixOf` x) mainArgs+++help :: String+help = L.intercalate "\n"+    [ ""+    , "NOTE"+    , ""+    , "This test suite accesses the AWS account that is associated with"+    , "the default credentials from the credential file ~/.aws-keys."+    , ""+    , "By running the tests in this test-suite costs for usage of AWS"+    , "services may incur."+    , ""+    , "In order to actually excute the tests in this test-suite you must"+    , "provide the command line options:"+    , ""+    , "    --run-with-aws-credentials"+    , ""+    , "When running this test-suite through cabal you may use the following"+    , "command:"+    , ""+    , "    cabal test --test-option=--run-with-aws-credentials dynamodb-tests"+    , ""+    ]++tests :: TestTree+tests = testGroup "DynamoDb Tests"+    [ test_table+    -- , test_message+    , test_core+    ]++-- -------------------------------------------------------------------------- --+-- Table Tests++test_table :: TestTree+test_table = testGroup "Table Tests"+    [ eitherTOnceTest1 "CreateDescribeDeleteTable" (prop_createDescribeDeleteTable 10 10)+    ]++-- |+--+prop_createDescribeDeleteTable+    :: Int -- ^ read capacity (#(non-consistent) reads * itemsize/4KB)+    -> Int -- ^ write capacity (#writes * itemsize/1KB)+    -> T.Text -- ^ table name+    -> EitherT T.Text IO ()+prop_createDescribeDeleteTable readCapacity writeCapacity tableName = do+    tTableName <- testData tableName+    tryT $ createTestTable tTableName readCapacity writeCapacity+    let deleteTable = retryT 6 . void $ simpleDyT (DY.DeleteTable tTableName)+    handleT (\e -> deleteTable >> left e) $ do+        retryT 6 . void . simpleDyT $ DY.DescribeTable tTableName+        deleteTable++-- -------------------------------------------------------------------------- --+-- Test core functionality++test_core :: TestTree+test_core = testGroup "Core Tests"+        [ eitherTOnceTest0 "connectionReuse" prop_connectionReuse+        ]++prop_connectionReuse+    :: EitherT T.Text IO ()+prop_connectionReuse = do+    c <- liftIO $ do+        cfg <- baseConfiguration++        -- counts the number of TCP connections+        ref <- newIORef (0 :: Int)++        void . HTTP.withManager (managerSettings ref) $ \manager -> runEitherT $+            handleT (error . T.unpack) . replicateM_ 3 $ do+                void $ dyT cfg manager DY.ListTables+                mustFail . dyT cfg manager $ DY.DescribeTable "____"++        readIORef ref+    unless (c == 1) $+        left "The TCP connection has not been reused"+  where+    managerSettings ref = HTTP.defaultManagerSettings+        { HTTP.managerRawConnection = do+            mkConn <- HTTP.managerRawConnection HTTP.defaultManagerSettings+            return $ \a b c -> do+                atomicModifyIORef ref $ \i -> (succ i, ())+                mkConn a b c+        }+
+ tests/DynamoDb/Utils.hs view
@@ -0,0 +1,167 @@+-- ------------------------------------------------------ --+-- Copyright © 2014 AlephCloud Systems, Inc.+-- ------------------------------------------------------ --++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE FlexibleContexts #-}++-- |+-- Module: DynamoDb.Utils+-- Copyright: Copyright © 2014 AlephCloud Systems, Inc.+-- License: BSD3+-- Maintainer: Lars Kuhtz <lars@alephcloud.com>+-- Stability: experimental+--+-- Tests for Haskell SQS bindings+--++module DynamoDb.Utils+(+-- * Static Parameters+  testProtocol+, testRegion+, defaultTableName++-- * Static Configuration+, dyConfiguration++-- * DynamoDb Utils+, simpleDy+, simpleDyT+, dyT+, withTable+, withTable_+, createTestTable+) where++import Aws+import Aws.Core+import qualified Aws.DynamoDb as DY++import Control.Error+import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.Trans.Control+import Control.Monad.Trans.Resource++import Data.Monoid+import qualified Data.Text as T+import qualified Data.Text.IO as T++import qualified Network.HTTP.Client as HTTP++import Test.Tasty+import Test.QuickCheck.Instances ()++import System.IO++import Utils++-- -------------------------------------------------------------------------- --+-- Static Test parameters+--+-- TODO make these configurable++testProtocol :: Protocol+testProtocol = HTTP++testRegion :: DY.Region+testRegion = DY.ddbUsWest2++defaultTableName :: T.Text+defaultTableName = "test-table"++-- -------------------------------------------------------------------------- --+-- Dynamo Utils++dyConfiguration :: DY.DdbConfiguration qt+dyConfiguration = DY.DdbConfiguration+    { DY.ddbcRegion = testRegion+    , DY.ddbcProtocol = testProtocol+    , DY.ddbcPort = Nothing+    }++simpleDy+    :: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ DY.DdbConfiguration, MonadIO m)+    => r+    -> m (MemoryResponse a)+simpleDy command = do+    c <- dbgConfiguration+    simpleAws c dyConfiguration command++simpleDyT+    :: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ DY.DdbConfiguration, MonadBaseControl IO m, MonadIO m)+    => r+    -> EitherT T.Text m (MemoryResponse a)+simpleDyT = tryT . simpleDy++dyT+    :: (Transaction r a, ServiceConfiguration r ~ DY.DdbConfiguration)+    => Configuration+    -> HTTP.Manager+    -> r+    -> EitherT T.Text IO a+dyT cfg manager req = do+    Response _ r <- liftIO . runResourceT $ aws cfg dyConfiguration manager req+    hoistEither $ fmapL sshow r++withTable+    :: T.Text -- ^ table Name+    -> Int -- ^ read capacity (#(non-consistent) reads * itemsize/4KB)+    -> Int -- ^ write capacity (#writes * itemsize/1KB)+    -> (T.Text -> IO a) -- ^ test tree+    -> IO a+withTable = withTable_ True++withTable_+    :: Bool -- ^ whether to prefix te table name+    -> T.Text -- ^ table Name+    -> Int -- ^ read capacity (#(non-consistent) reads * itemsize/4KB)+    -> Int -- ^ write capacity (#writes * itemsize/1KB)+    -> (T.Text -> IO a) -- ^ test tree+    -> IO a+withTable_ prefix tableName readCapacity writeCapacity f =+    do+      tTableName <- if prefix then testData tableName else return tableName++      let deleteTable = do+            r <- runEitherT . retryT 6 $+                void (simpleDyT $ DY.DeleteTable tTableName) `catchT` \e ->+                    liftIO . T.hPutStrLn stderr $ "attempt to delete table failed: " <> e+            either (error . T.unpack) (const $ return ()) r++      let createTable = do+            r <- runEitherT $ do+                retryT 3 $ tryT $ createTestTable tTableName readCapacity writeCapacity+                retryT 6 $ do+                    tableDesc <- simpleDyT $ DY.DescribeTable tTableName+                    when (DY.rTableStatus tableDesc == "CREATING") $ left "Table not ready: status CREATING"+            either (error . T.unpack) return r++      bracket_ createTable deleteTable $ f tTableName++createTestTable+    :: T.Text -- ^ table Name+    -> Int -- ^ read capacity (#(non-consistent) reads * itemsize/4KB)+    -> Int -- ^ write capacity (#writes * itemsize/1KB)+    -> IO ()+createTestTable tableName readCapacity writeCapacity = void . simpleDy $+    DY.createTable+        tableName+        attrs+        (DY.HashOnly keyName)+        throughPut+  where+    keyName = "Id"+    keyType = DY.AttrString+    attrs = [DY.AttributeDefinition keyName keyType]+    throughPut = DY.ProvisionedThroughput+        { DY.readCapacityUnits = readCapacity+        , DY.writeCapacityUnits = writeCapacity+        }++
tests/Sqs/Main.hs view
@@ -6,6 +6,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE GADTs #-}+{-# LANGUAGE FlexibleContexts #-}  -- | -- Module: Main@@ -29,11 +30,16 @@ import Control.Error import Control.Monad import Control.Monad.IO.Class+import Control.Monad.Trans.Control+import Control.Monad.Trans.Resource +import Data.IORef import qualified Data.List as L import Data.Monoid import qualified Data.Text as T +import qualified Network.HTTP.Client as HTTP+ import Test.Tasty import Test.QuickCheck.Instances () @@ -84,14 +90,15 @@     , "When running this test-suite through cabal you may use the following"     , "command:"     , ""-    , "    cabal test sqs-tests --test-option=--run-with-aws-credentials"+    , "    cabal test --test-option=--run-with-aws-credentials sqs-tests"     , ""     ]  tests :: TestTree-tests = testGroup "SQS Tests"+tests = withQueueTest defaultQueueName $ \getQueueParams -> testGroup "SQS Tests"     [ test_queue-    , test_message+    , test_message getQueueParams+    , test_core getQueueParams     ]  -- -------------------------------------------------------------------------- --@@ -129,6 +136,16 @@     , SQS.sqsDefaultExpiry = 180     } +sqsT+    :: (Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration)+    => Configuration+    -> HTTP.Manager+    -> r+    -> EitherT T.Text IO a+sqsT cfg manager req = do+    Response _ r <- liftIO . runResourceT $ aws cfg sqsConfiguration manager req+    hoistEither $ fmapL sshow r+ simpleSqs     :: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration, MonadIO m)     => r@@ -138,7 +155,7 @@     simpleAws c sqsConfiguration command  simpleSqsT-    :: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration, MonadIO m)+    :: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration, MonadBaseControl IO m, MonadIO m)     => r     -> EitherT T.Text m (MemoryResponse a) simpleSqsT = tryT . simpleSqs@@ -171,6 +188,7 @@     :: T.Text -- ^ queue name     -> EitherT T.Text IO () prop_createListDeleteQueue queueName = do+    tQueueName <- testData queueName     SQS.CreateQueueResponse queueUrl <- simpleSqsT $ SQS.CreateQueue Nothing tQueueName     let queue = sqsQueueName queueUrl     handleT (\e -> deleteQueue queue >> left e) $ do@@ -180,25 +198,23 @@                 . left $ "queue " <> sshow queueUrl <> " not listed"         deleteQueue queue   where-    tQueueName = testData queueName     deleteQueue queueUrl = void $ simpleSqsT (SQS.DeleteQueue queueUrl)  -- -------------------------------------------------------------------------- -- -- Message Tests -test_message :: TestTree-test_message =-    withQueueTest defaultQueueName $ \getQueueParams -> testGroup "Queue Tests"-        [ eitherTOnceTest0 "SendReceiveDeleteMessage" $ do-            (_, queue) <- liftIO getQueueParams-            prop_sendReceiveDeleteMessage queue-        , eitherTOnceTest0 "SendReceiveDeleteMessageLongPolling" $ do-            (_, queue) <- liftIO getQueueParams-            prop_sendReceiveDeleteMessageLongPolling queue-        , eitherTOnceTest0 "SendReceiveDeleteMessageLongPolling1" $ do-            (_, queue) <- liftIO getQueueParams-            prop_sendReceiveDeleteMessageLongPolling1 queue-        ]+test_message :: IO (T.Text, SQS.QueueName) -> TestTree+test_message getQueueParams = testGroup "Queue Tests"+    [ eitherTOnceTest0 "SendReceiveDeleteMessage" $ do+        (_, queue) <- liftIO getQueueParams+        prop_sendReceiveDeleteMessage queue+    , eitherTOnceTest0 "SendReceiveDeleteMessageLongPolling" $ do+        (_, queue) <- liftIO getQueueParams+        prop_sendReceiveDeleteMessageLongPolling queue+    , eitherTOnceTest0 "SendReceiveDeleteMessageLongPolling1" $ do+        (_, queue) <- liftIO getQueueParams+        prop_sendReceiveDeleteMessageLongPolling1 queue+    ]  -- | Simple send and short-polling receive. First sends all messages -- and receives messages thereafter one by one.@@ -314,3 +330,50 @@     unless (sent == recv)         $ left $ "received messages don't match send messages; sent: "             <> sshow sent <> "; got: " <> sshow recv+++-- -------------------------------------------------------------------------- --+-- Test core functionality++test_core :: IO (T.Text, SQS.QueueName) -> TestTree+test_core getQueueParams = testGroup "Core Tests"+    [ eitherTOnceTest0 "connectionReuse" $ do+        (_, queue) <- liftIO getQueueParams+        prop_connectionReuse queue+    ]++prop_connectionReuse+    :: SQS.QueueName+    -> EitherT T.Text IO ()+prop_connectionReuse queue = do+    c <- liftIO $ do+        cfg <- baseConfiguration++        -- used for counting the number of TCP connections+        ref <- newIORef (0 :: Int)++        -- Use a single manager for all HTTP requests+        void . HTTP.withManager (managerSettings ref) $ \manager -> runEitherT $++            handleT (error . T.unpack) . replicateM_ 3 $ do+                void . sqsT cfg manager $ SQS.ListQueues Nothing+                mustFail . sqsT cfg manager $+                    SQS.SendMessage "" (SQS.QueueName "" "") [] Nothing+                void . sqsT cfg manager $+                    SQS.SendMessage "test-message" queue [] Nothing+                void . sqsT cfg manager $+                    SQS.ReceiveMessage Nothing [] Nothing [] queue (Just 20)++        readIORef ref+    unless (c == 1) $+        left "The TCP connection has not been reused"+  where++    managerSettings ref = HTTP.defaultManagerSettings+        { HTTP.managerRawConnection = do+            mkConn <- HTTP.managerRawConnection HTTP.defaultManagerSettings+            return $ \a b c -> do+                atomicModifyIORef ref $ \i -> (succ i, ())+                mkConn a b c+        }+
tests/Utils.hs view
@@ -2,6 +2,8 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TupleSections #-}  -- | -- Module: Utils@@ -19,8 +21,10 @@  -- * General Utils , sshow+, mustFail , tryT , retryT+, retryT_ , testData  , evalTestT@@ -34,17 +38,23 @@ , prop_jsonRoundtrip ) where +import Control.Applicative import Control.Concurrent (threadDelay)-import Control.Error+import qualified Control.Exception.Lifted as LE+import Control.Error hiding (syncIO) import Control.Monad import Control.Monad.Identity import Control.Monad.IO.Class+import Control.Monad.Base+import Control.Monad.Trans.Control  import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)+import Data.Dynamic (Dynamic) import Data.Monoid import Data.Proxy import Data.String import qualified Data.Text as T+import qualified Data.Text.IO as T import Data.Typeable  import Test.QuickCheck.Property@@ -52,6 +62,11 @@ import Test.Tasty import Test.Tasty.QuickCheck +import System.Exit (ExitCode)+import System.IO (stderr)++import Data.Time.Clock.POSIX (getPOSIXTime)+ -- -------------------------------------------------------------------------- -- -- Static Test parameters --@@ -59,29 +74,68 @@ -- | This prefix is used for the IDs and names of all entities that are -- created in the AWS account. ---testDataPrefix :: IsString a => a-testDataPrefix = "__TEST_AWSHASKELLBINDINGS__"+testDataPrefix :: IsString a => MonadBase IO m => m a+testDataPrefix = do+    t <- liftBase $ getPOSIXTime+    let t' :: Int+        t' = floor (t * 1000)+    return . fromString $ "__TEST_AWSHASKELLBINDINGS__" ++ show t'  -- -------------------------------------------------------------------------- -- -- General Utils -tryT :: MonadIO m => IO a -> EitherT T.Text m a+-- | Catches all exceptions except for asynchronous exceptions found in base.+--+tryT :: MonadBaseControl IO m => m a -> EitherT T.Text m a tryT = fmapLT (T.pack . show) . syncIO -testData :: (IsString a, Monoid a) => a -> a-testData a = testDataPrefix <> a+-- | Lifted Version of 'syncIO' form "Control.Error.Util".+--+syncIO :: MonadBaseControl IO m => m a -> EitherT LE.SomeException m a+syncIO a = EitherT $ LE.catches (Right <$> a)+    [ LE.Handler $ \e -> LE.throw (e :: LE.ArithException)+    , LE.Handler $ \e -> LE.throw (e :: LE.ArrayException)+    , LE.Handler $ \e -> LE.throw (e :: LE.AssertionFailed)+    , LE.Handler $ \e -> LE.throw (e :: LE.AsyncException)+    , LE.Handler $ \e -> LE.throw (e :: LE.BlockedIndefinitelyOnMVar)+    , LE.Handler $ \e -> LE.throw (e :: LE.BlockedIndefinitelyOnSTM)+    , LE.Handler $ \e -> LE.throw (e :: LE.Deadlock)+    , LE.Handler $ \e -> LE.throw (e ::    Dynamic)+    , LE.Handler $ \e -> LE.throw (e :: LE.ErrorCall)+    , LE.Handler $ \e -> LE.throw (e ::    ExitCode)+    , LE.Handler $ \e -> LE.throw (e :: LE.NestedAtomically)+    , LE.Handler $ \e -> LE.throw (e :: LE.NoMethodError)+    , LE.Handler $ \e -> LE.throw (e :: LE.NonTermination)+    , LE.Handler $ \e -> LE.throw (e :: LE.PatternMatchFail)+    , LE.Handler $ \e -> LE.throw (e :: LE.RecConError)+    , LE.Handler $ \e -> LE.throw (e :: LE.RecSelError)+    , LE.Handler $ \e -> LE.throw (e :: LE.RecUpdError)+    , LE.Handler $ return . Left+    ] +testData :: (IsString a, Monoid a, MonadBaseControl IO m) => a -> m a+testData a = fmap (<> a) testDataPrefix+ retryT :: MonadIO m => Int -> EitherT T.Text m a -> EitherT T.Text m a-retryT i f = go 1+retryT n f = snd <$> retryT_ n f++retryT_ :: MonadIO m => Int -> EitherT T.Text m a -> EitherT T.Text m (Int, a)+retryT_ n f = go 1   where     go x-        | x >= i = fmapLT (\e -> "error after " <> sshow x <> " retries: " <> e) f-        | otherwise = f `catchT` \_ -> do+        | x >= n = fmapLT (\e -> "error after " <> sshow x <> " retries: " <> e) ((x,) <$> f)+        | otherwise = ((x,) <$> f) `catchT` \e -> do+            liftIO $ T.hPutStrLn stderr $ "Retrying after error: " <> e             liftIO $ threadDelay (1000000 * min 60 (2^(x-1)))             go (succ x)  sshow :: (Show a, IsString b) => a -> b sshow = fromString . show++mustFail :: Monad m => EitherT e m a -> EitherT T.Text m ()+mustFail = EitherT . eitherT+    (const . return $ Right ())+    (const . return $ Left "operation succeeded when a failure was expected")  evalTestTM     :: Functor f