packages feed

minio-hs 0.3.1 → 0.3.2

raw patch · 18 files changed

+1416/−394 lines, 18 filesdep +aesondep +base64-bytestringdep +vectordep ~protoludePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: aeson, base64-bytestring, vector

Dependency ranges changed: protolude

API changes (from Hackage documentation)

- Network.Minio: ODFile :: FilePath -> (Maybe Int64) -> ObjectData m
- Network.Minio: ODStream :: (Producer m ByteString) -> (Maybe Int64) -> ObjectData m
- Network.Minio: data ObjectData m
+ Network.Minio: Filter :: FilterKey -> Filter
+ Network.Minio: FilterKey :: FilterRules -> FilterKey
+ Network.Minio: FilterRule :: Text -> Text -> FilterRule
+ Network.Minio: FilterRules :: [FilterRule] -> FilterRules
+ Network.Minio: MErrVInvalidUrlExpiry :: Int -> MErrV
+ Network.Minio: Notification :: [NotificationConfig] -> [NotificationConfig] -> [NotificationConfig] -> Notification
+ Network.Minio: NotificationConfig :: Text -> Arn -> [Event] -> Filter -> NotificationConfig
+ Network.Minio: ObjectCreated :: Event
+ Network.Minio: ObjectCreatedCopy :: Event
+ Network.Minio: ObjectCreatedMultipartUpload :: Event
+ Network.Minio: ObjectCreatedPost :: Event
+ Network.Minio: ObjectCreatedPut :: Event
+ Network.Minio: ObjectRemoved :: Event
+ Network.Minio: ObjectRemovedDelete :: Event
+ Network.Minio: ObjectRemovedDeleteMarkerCreated :: Event
+ Network.Minio: PPEBucketNotSpecified :: PostPolicyError
+ Network.Minio: PPEConditionKeyEmpty :: PostPolicyError
+ Network.Minio: PPEKeyNotSpecified :: PostPolicyError
+ Network.Minio: PPERangeInvalid :: PostPolicyError
+ Network.Minio: ReducedRedundancyLostObject :: Event
+ Network.Minio: [fFilter] :: Filter -> FilterKey
+ Network.Minio: [fkKey] :: FilterKey -> FilterRules
+ Network.Minio: [frFilterRules] :: FilterRules -> [FilterRule]
+ Network.Minio: [frName] :: FilterRule -> Text
+ Network.Minio: [frValue] :: FilterRule -> Text
+ Network.Minio: [nCloudFunctionConfigurations] :: Notification -> [NotificationConfig]
+ Network.Minio: [nQueueConfigurations] :: Notification -> [NotificationConfig]
+ Network.Minio: [nTopicConfigurations] :: Notification -> [NotificationConfig]
+ Network.Minio: [ncArn] :: NotificationConfig -> Arn
+ Network.Minio: [ncEvents] :: NotificationConfig -> [Event]
+ Network.Minio: [ncFilter] :: NotificationConfig -> Filter
+ Network.Minio: [ncId] :: NotificationConfig -> Text
+ Network.Minio: data Event
+ Network.Minio: data Filter
+ Network.Minio: data FilterKey
+ Network.Minio: data FilterRule
+ Network.Minio: data FilterRules
+ Network.Minio: data Notification
+ Network.Minio: data NotificationConfig
+ Network.Minio: data PostPolicy
+ Network.Minio: data PostPolicyCondition
+ Network.Minio: data PostPolicyError
+ Network.Minio: getBucketNotification :: Bucket -> Minio Notification
+ Network.Minio: listIncompleteParts :: Bucket -> Object -> UploadId -> Producer Minio ObjectPartInfo
+ Network.Minio: listObjectsV1 :: Bucket -> Maybe Text -> Bool -> Producer Minio ObjectInfo
+ Network.Minio: newPostPolicy :: UTCTime -> [PostPolicyCondition] -> Either PostPolicyError PostPolicy
+ Network.Minio: ppCondBucket :: Bucket -> PostPolicyCondition
+ Network.Minio: ppCondContentLengthRange :: Int64 -> Int64 -> PostPolicyCondition
+ Network.Minio: ppCondContentType :: Text -> PostPolicyCondition
+ Network.Minio: ppCondKey :: Object -> PostPolicyCondition
+ Network.Minio: ppCondKeyStartsWith :: Object -> PostPolicyCondition
+ Network.Minio: ppCondSuccessActionStatus :: Int -> PostPolicyCondition
+ Network.Minio: presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> Query -> RequestHeaders -> Minio ByteString
+ Network.Minio: presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry -> RequestHeaders -> Minio ByteString
+ Network.Minio: presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map Text ByteString)
+ Network.Minio: presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> RequestHeaders -> Minio ByteString
+ Network.Minio: putBucketNotification :: Bucket -> Notification -> Minio ()
+ Network.Minio: removeAllBucketNotification :: Bucket -> Minio ()
+ Network.Minio: removeIncompleteUpload :: Bucket -> Object -> Minio ()
+ Network.Minio: showPostPolicy :: PostPolicy -> ByteString
+ Network.Minio: type Arn = Text
+ Network.Minio: type Region = Text
+ Network.Minio: type UrlExpiry = Int
+ Network.Minio.S3API: Filter :: FilterKey -> Filter
+ Network.Minio.S3API: FilterKey :: FilterRules -> FilterKey
+ Network.Minio.S3API: FilterRule :: Text -> Text -> FilterRule
+ Network.Minio.S3API: FilterRules :: [FilterRule] -> FilterRules
+ Network.Minio.S3API: ListObjectsV1Result :: Bool -> Maybe Text -> [ObjectInfo] -> [Text] -> ListObjectsV1Result
+ Network.Minio.S3API: Notification :: [NotificationConfig] -> [NotificationConfig] -> [NotificationConfig] -> Notification
+ Network.Minio.S3API: NotificationConfig :: Text -> Arn -> [Event] -> Filter -> NotificationConfig
+ Network.Minio.S3API: ObjectCreated :: Event
+ Network.Minio.S3API: ObjectCreatedCopy :: Event
+ Network.Minio.S3API: ObjectCreatedMultipartUpload :: Event
+ Network.Minio.S3API: ObjectCreatedPost :: Event
+ Network.Minio.S3API: ObjectCreatedPut :: Event
+ Network.Minio.S3API: ObjectRemoved :: Event
+ Network.Minio.S3API: ObjectRemovedDelete :: Event
+ Network.Minio.S3API: ObjectRemovedDeleteMarkerCreated :: Event
+ Network.Minio.S3API: PPCEquals :: Text -> Text -> PostPolicyCondition
+ Network.Minio.S3API: PPCRange :: Text -> Int64 -> Int64 -> PostPolicyCondition
+ Network.Minio.S3API: PPCStartsWith :: Text -> Text -> PostPolicyCondition
+ Network.Minio.S3API: PPEBucketNotSpecified :: PostPolicyError
+ Network.Minio.S3API: PPEConditionKeyEmpty :: PostPolicyError
+ Network.Minio.S3API: PPEKeyNotSpecified :: PostPolicyError
+ Network.Minio.S3API: PPERangeInvalid :: PostPolicyError
+ Network.Minio.S3API: PostPolicy :: UTCTime -> [PostPolicyCondition] -> PostPolicy
+ Network.Minio.S3API: ReducedRedundancyLostObject :: Event
+ Network.Minio.S3API: [conditions] :: PostPolicy -> [PostPolicyCondition]
+ Network.Minio.S3API: [expiration] :: PostPolicy -> UTCTime
+ Network.Minio.S3API: [fFilter] :: Filter -> FilterKey
+ Network.Minio.S3API: [fkKey] :: FilterKey -> FilterRules
+ Network.Minio.S3API: [frFilterRules] :: FilterRules -> [FilterRule]
+ Network.Minio.S3API: [frName] :: FilterRule -> Text
+ Network.Minio.S3API: [frValue] :: FilterRule -> Text
+ Network.Minio.S3API: [lorCPrefixes'] :: ListObjectsV1Result -> [Text]
+ Network.Minio.S3API: [lorHasMore'] :: ListObjectsV1Result -> Bool
+ Network.Minio.S3API: [lorNextMarker] :: ListObjectsV1Result -> Maybe Text
+ Network.Minio.S3API: [lorObjects'] :: ListObjectsV1Result -> [ObjectInfo]
+ Network.Minio.S3API: [nCloudFunctionConfigurations] :: Notification -> [NotificationConfig]
+ Network.Minio.S3API: [nQueueConfigurations] :: Notification -> [NotificationConfig]
+ Network.Minio.S3API: [nTopicConfigurations] :: Notification -> [NotificationConfig]
+ Network.Minio.S3API: [ncArn] :: NotificationConfig -> Arn
+ Network.Minio.S3API: [ncEvents] :: NotificationConfig -> [Event]
+ Network.Minio.S3API: [ncFilter] :: NotificationConfig -> Filter
+ Network.Minio.S3API: [ncId] :: NotificationConfig -> Text
+ Network.Minio.S3API: data Event
+ Network.Minio.S3API: data Filter
+ Network.Minio.S3API: data FilterKey
+ Network.Minio.S3API: data FilterRule
+ Network.Minio.S3API: data FilterRules
+ Network.Minio.S3API: data ListObjectsV1Result
+ Network.Minio.S3API: data Notification
+ Network.Minio.S3API: data NotificationConfig
+ Network.Minio.S3API: data PostPolicy
+ Network.Minio.S3API: data PostPolicyCondition
+ Network.Minio.S3API: data PostPolicyError
+ Network.Minio.S3API: getBucketNotification :: Bucket -> Minio Notification
+ Network.Minio.S3API: listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int -> Minio ListObjectsV1Result
+ Network.Minio.S3API: makePresignedUrl :: UrlExpiry -> Method -> Maybe Bucket -> Maybe Object -> Maybe Region -> Query -> RequestHeaders -> Minio ByteString
+ Network.Minio.S3API: newPostPolicy :: UTCTime -> [PostPolicyCondition] -> Either PostPolicyError PostPolicy
+ Network.Minio.S3API: ppCondBucket :: Bucket -> PostPolicyCondition
+ Network.Minio.S3API: ppCondContentLengthRange :: Int64 -> Int64 -> PostPolicyCondition
+ Network.Minio.S3API: ppCondContentType :: Text -> PostPolicyCondition
+ Network.Minio.S3API: ppCondKey :: Object -> PostPolicyCondition
+ Network.Minio.S3API: ppCondKeyStartsWith :: Object -> PostPolicyCondition
+ Network.Minio.S3API: ppCondSuccessActionStatus :: Int -> PostPolicyCondition
+ Network.Minio.S3API: presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> Query -> RequestHeaders -> Minio ByteString
+ Network.Minio.S3API: presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry -> RequestHeaders -> Minio ByteString
+ Network.Minio.S3API: presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map Text ByteString)
+ Network.Minio.S3API: presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> RequestHeaders -> Minio ByteString
+ Network.Minio.S3API: putBucketNotification :: Bucket -> Notification -> Minio ()
+ Network.Minio.S3API: removeAllBucketNotification :: Bucket -> Minio ()
+ Network.Minio.S3API: showPostPolicy :: PostPolicy -> ByteString
+ Network.Minio.S3API: type Arn = Text
+ Network.Minio.S3API: type UrlExpiry = Int

Files

minio-hs.cabal view
@@ -1,5 +1,5 @@ name:                minio-hs-version:             0.3.1+version:             0.3.2 synopsis:            A Minio Haskell Library for Amazon S3 compatible cloud                      storage. description:         The Minio Haskell client library provides simple APIs to@@ -29,14 +29,17 @@                      , Network.Minio.Data.Time                      , Network.Minio.Errors                      , Network.Minio.ListOps+                     , Network.Minio.PresignedOperations                      , Network.Minio.PutObject                      , Network.Minio.Sign.V4                      , Network.Minio.Utils                      , Network.Minio.XmlGenerator                      , Network.Minio.XmlParser   build-depends:       base >= 4.7 && < 5-                     , protolude >= 0.1.6 && < 0.2+                     , protolude >= 0.1.6+                     , aeson                      , async+                     , base64-bytestring                      , bytestring                      , case-insensitive                      , conduit@@ -61,10 +64,12 @@                      , time                      , transformers                      , transformers-base+                     , vector                      , xml-conduit   default-language:    Haskell2010   default-extensions:  FlexibleContexts                      , FlexibleInstances+                     , BangPatterns                      , MultiParamTypeClasses                      , MultiWayIf                      , NoImplicitPrelude@@ -84,7 +89,8 @@   main-is:             LiveServer.hs   ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N   default-language:    Haskell2010-  default-extensions:  FlexibleContexts+  default-extensions:  BangPatterns+                     , FlexibleContexts                      , FlexibleInstances                      , OverloadedStrings                      , NoImplicitPrelude@@ -103,6 +109,7 @@                      , Network.Minio.Data.Time                      , Network.Minio.Errors                      , Network.Minio.ListOps+                     , Network.Minio.PresignedOperations                      , Network.Minio.PutObject                      , Network.Minio.S3API                      , Network.Minio.Sign.V4@@ -115,8 +122,10 @@                      , Network.Minio.XmlParser.Test   build-depends:       base                      , minio-hs-                     , protolude >= 0.1.6 && < 0.2+                     , protolude >= 0.1.6+                     , aeson                      , async+                     , base64-bytestring                      , bytestring                      , case-insensitive                      , conduit@@ -148,6 +157,7 @@                      , time                      , transformers                      , transformers-base+                     , vector                      , xml-conduit   if !flag(live-test)     buildable: False@@ -158,8 +168,10 @@   main-is:             Spec.hs   build-depends:       base                      , minio-hs-                     , protolude >= 0.1.6 && < 0.2+                     , protolude >= 0.1.6+                     , aeson                      , async+                     , base64-bytestring                      , bytestring                      , case-insensitive                      , conduit@@ -191,10 +203,12 @@                      , time                      , transformers                      , transformers-base+                     , vector                      , xml-conduit   ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N   default-language:    Haskell2010-  default-extensions:  FlexibleContexts+  default-extensions:  BangPatterns+                     , FlexibleContexts                      , FlexibleInstances                      , OverloadedStrings                      , NoImplicitPrelude@@ -213,6 +227,7 @@                      , Network.Minio.Data.Time                      , Network.Minio.Errors                      , Network.Minio.ListOps+                     , Network.Minio.PresignedOperations                      , Network.Minio.PutObject                      , Network.Minio.S3API                      , Network.Minio.Sign.V4
src/Lib/Prelude.hs view
@@ -14,17 +14,12 @@ -- limitations under the License. -- -{--Welcome to your custom Prelude-Export here everything that should always be in your library scope-For more info on what is exported by Protolude check:-https://github.com/sdiehl/protolude/blob/master/Symbols.md--} module Lib.Prelude     ( module Exports     , both      , format+    , formatBS     ) where  import           Protolude as Exports@@ -37,10 +32,12 @@ import           Data.Text.Format as Exports (Shown(..)) import qualified Data.Text.Format as TF import           Data.Text.Format.Params (Params)-import qualified Data.Text.Lazy as LT  format :: Params ps => TF.Format -> ps -> Text-format f args = LT.toStrict $ TF.format f args+format f args = toS $ TF.format f args++formatBS :: Params ps => TF.Format -> ps -> ByteString+formatBS f args = toS $ TF.format f args  -- import Data.Tuple as Exports (uncurry) 
src/Network/Minio.hs view
@@ -14,72 +14,135 @@ -- limitations under the License. -- ++ module Network.Minio   ( +  -- * Connecting to object storage+  ---------------------------------     ConnectInfo(..)   , awsCI++  -- ** Connection helpers+  ------------------------   , awsWithRegionCI   , minioPlayCI   , minioCI +  -- * Minio Monad+  ----------------+  -- | The Minio monad provides connection-reuse, bucket-location+  -- caching, resource management and simpler error handling+  -- functionality. All actions on object storage are performed within+  -- this Monad.+   , Minio   , runMinio   , def -  -- * Error handling-  ------------------------  -- | Data types representing various errors that may occur while working-  -- with an object storage service.-  , MinioErr(..)-  , MErrV(..)-  , ServiceErr(..)--  -- * Data Types-  -----------------  -- | Data types representing various object store concepts.-  , Bucket-  , Object-  , BucketInfo(..)-  , ObjectInfo(..)-  , UploadInfo(..)-  , ObjectPartInfo(..)-  , UploadId-  , ObjectData(..)-  , CopyPartSource(..)-   -- * Bucket Operations   -----------------------  , listBuckets-  , getLocation-  , bucketExists++  -- ** Creation, removal and querying+  , Bucket   , makeBucket   , removeBucket+  , bucketExists+  , Region+  , getLocation +  -- ** Listing+  , BucketInfo(..)+  , listBuckets+  , ObjectInfo(..)   , listObjects+  , listObjectsV1+  , UploadId+  , UploadInfo(..)   , listIncompleteUploads+  , ObjectPartInfo(..)+  , listIncompleteParts +  -- ** Notifications+  , Notification(..)+  , NotificationConfig(..)+  , Arn+  , Event(..)+  , Filter(..)+  , FilterKey(..)+  , FilterRules(..)+  , FilterRule(..)+  , getBucketNotification+  , putBucketNotification+  , removeAllBucketNotification+   -- * Object Operations   ----------------------++  , Object++  -- ** File operations   , fGetObject   , fPutObject++  -- ** Conduit-based streaming operations   , putObject+  , getObject++  -- ** Server-side copying+  , CopyPartSource(..)   , copyObject-  , removeObject -  , getObject+  -- ** Querying   , statObject -  ) where+  -- ** Object removal functions+  , removeObject+  , removeIncompleteUpload +  -- * Presigned Operations+  -------------------------+  , UrlExpiry+  , presignedPutObjectUrl+  , presignedGetObjectUrl+  , presignedHeadObjectUrl++  -- ** Utilities for POST (browser) uploads+  , PostPolicy+  , PostPolicyError(..)+  , newPostPolicy+  , presignedPostPolicy+  , showPostPolicy++  -- *** Utilities to specify Post Policy conditions+  , PostPolicyCondition+  , ppCondBucket+  , ppCondContentLengthRange+  , ppCondContentType+  , ppCondKey+  , ppCondKeyStartsWith+  , ppCondSuccessActionStatus++  -- * Error handling+  -----------------------+  -- | Data types representing various errors that may occur while working+  -- with an object storage service.+  , MinioErr(..)+  , MErrV(..)+  , ServiceErr(..)++) where+ {- This module exports the high-level Minio API for object storage. -} -import qualified Data.Conduit as C-import qualified Data.Conduit.Binary as CB-import           Data.Default (def)-import qualified Data.Map as Map+import qualified Data.Conduit             as C+import qualified Data.Conduit.Binary      as CB+import qualified Data.Conduit.Combinators as CC+import           Data.Default             (def)+import qualified Data.Map                 as Map  import           Lib.Prelude @@ -139,11 +202,12 @@ -- configured in ConnectInfo, which is by default, the US Standard -- Region. makeBucket :: Bucket -> Maybe Region -> Minio ()-makeBucket bucket regionMay= do+makeBucket bucket regionMay = do   region <- maybe (asks $ connectRegion . mcConnInfo) return regionMay   putBucket bucket region   modify (Map.insert bucket region) +-- | Removes a bucket from the object store. removeBucket :: Bucket -> Minio () removeBucket bucket = do   deleteBucket bucket@@ -152,3 +216,10 @@ -- | Query the object store if a given bucket is present. bucketExists :: Bucket -> Minio Bool bucketExists = headBucket+++-- | Removes an ongoing multipart upload of an object.+removeIncompleteUpload :: Bucket -> Object -> Minio ()+removeIncompleteUpload bucket object = do+  uploads <- listIncompleteUploads bucket (Just object) False C.$$ CC.sinkList+  mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs view
@@ -36,9 +36,11 @@ import qualified Data.Char as C import qualified Data.Text as T import qualified Data.ByteString as B+ import           Network.HTTP.Conduit (Response) import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Types as HT+import Network.HTTP.Types.Header (hHost)  import           Lib.Prelude @@ -120,15 +122,25 @@                    (Map.lookup r awsRegionMap)               else return $ connectHost ci +  sha256Hash <- if | connectIsSecure ci ->+                       -- if secure connection+                       return "UNSIGNED-PAYLOAD" -  sha256Hash <- getPayloadSHA256Hash (riPayload ri)-  let newRi = ri { riPayloadHash = sha256Hash-                 , riHeaders = sha256Header sha256Hash : riHeaders ri+                   -- otherwise compute sha256+                   | otherwise -> getPayloadSHA256Hash (riPayload ri)++  let hostHeader = (hHost, formatBS "{}:{}" [connectHost ci,+                                             show $ connectPort ci])++      newRi = ri { riPayloadHash = Just sha256Hash+                 , riHeaders = hostHeader+                             : sha256Header sha256Hash+                             : riHeaders ri                  , riRegion = region                  }       newCi = ci { connectHost = regionHost } -  reqHeaders <- liftIO $ signV4 newCi newRi+  signHeaders <- liftIO $ signV4 newCi newRi Nothing    return NC.defaultRequest {       NC.method = riMethod newRi@@ -137,7 +149,7 @@     , NC.port = connectPort newCi     , NC.path = getPathFromRI newRi     , NC.queryString = HT.renderQuery False $ riQueryParams newRi-    , NC.requestHeaders = reqHeaders+    , NC.requestHeaders = riHeaders newRi ++ mkHeaderFromPairs signHeaders     , NC.requestBody = getRequestBody (riPayload newRi)     } 
src/Network/Minio/Data.hs view
@@ -34,6 +34,8 @@ import           Network.Minio.Errors import           Text.XML +import           GHC.Show (Show(..))+ import           Lib.Prelude  @@ -224,6 +226,14 @@   , lorCPrefixes :: [Text]   } deriving (Show, Eq) +-- | Represents result from a listing of objects version 1 in a bucket.+data ListObjectsV1Result = ListObjectsV1Result {+    lorHasMore' :: Bool+  , lorNextMarker :: Maybe Text+  , lorObjects' :: [ObjectInfo]+  , lorCPrefixes' :: [Text]+  } deriving (Show, Eq)+ -- | Represents information about an object. data ObjectInfo = ObjectInfo {     oiObject :: Object@@ -233,10 +243,10 @@   } deriving (Show, Eq)  data CopyPartSource = CopyPartSource {-    cpSource :: Text -- | formatted like "\/sourceBucket\/sourceObject"-  , cpSourceRange :: Maybe (Int64, Int64) -- | (0, 9) means first ten-                                          -- bytes of the source-                                          -- object+    -- | formatted like "\/sourceBucket\/sourceObject"+    cpSource :: Text+    -- | (0, 9) means first ten bytes of the source object+  , cpSourceRange :: Maybe (Int64, Int64)   , cpSourceIfMatch :: Maybe Text   , cpSourceIfNoneMatch :: Maybe Text   , cpSourceIfUnmodifiedSince :: Maybe UTCTime@@ -272,6 +282,106 @@   where     splits = T.splitOn "/" $ cpSource cps +-- | A data-type for events that can occur in the object storage+-- server. Reference:+-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types+data Event = ObjectCreated+           | ObjectCreatedPut+           | ObjectCreatedPost+           | ObjectCreatedCopy+           | ObjectCreatedMultipartUpload+           | ObjectRemoved+           | ObjectRemovedDelete+           | ObjectRemovedDeleteMarkerCreated+           | ReducedRedundancyLostObject+           deriving (Eq)++instance Show Event where+  show ObjectCreated                    = "s3:ObjectCreated:*"+  show ObjectCreatedPut                 = "s3:ObjectCreated:Put"+  show ObjectCreatedPost                = "s3:ObjectCreated:Post"+  show ObjectCreatedCopy                = "s3:ObjectCreated:Copy"+  show ObjectCreatedMultipartUpload     = "s3:ObjectCreated:MultipartUpload"+  show ObjectRemoved                    = "s3:ObjectRemoved:*"+  show ObjectRemovedDelete              = "s3:ObjectRemoved:Delete"+  show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"+  show ReducedRedundancyLostObject      = "s3:ReducedRedundancyLostObject"++textToEvent :: Text -> Maybe Event+textToEvent t = case t of+  "s3:ObjectCreated:*"                   -> Just ObjectCreated+  "s3:ObjectCreated:Put"                 -> Just ObjectCreatedPut+  "s3:ObjectCreated:Post"                -> Just ObjectCreatedPost+  "s3:ObjectCreated:Copy"                -> Just ObjectCreatedCopy+  "s3:ObjectCreated:MultipartUpload"     -> Just ObjectCreatedMultipartUpload+  "s3:ObjectRemoved:*"                   -> Just ObjectRemoved+  "s3:ObjectRemoved:Delete"              -> Just ObjectRemovedDelete+  "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated+  "s3:ReducedRedundancyLostObject"       -> Just ReducedRedundancyLostObject+  _                                      -> Nothing+++data Filter = Filter+  { fFilter :: FilterKey+  } deriving (Show, Eq)++instance Default Filter where+  def = Filter def++data FilterKey = FilterKey+  { fkKey :: FilterRules+  } deriving (Show, Eq)++instance Default FilterKey where+  def = FilterKey def++data FilterRules = FilterRules+  { frFilterRules :: [FilterRule]+  } deriving (Show, Eq)++instance Default FilterRules where+  def = FilterRules []++-- | A filter rule that can act based on the suffix or prefix of an+-- object. As an example, let's create two filter rules:+--+--    > let suffixRule = FilterRule "suffix" ".jpg"+--    > let prefixRule = FilterRule "prefix" "images/"+--+-- The `suffixRule` restricts the notification to be triggered only+-- for objects having a suffix of ".jpg", and the `prefixRule`+-- restricts it to objects having a prefix of "images/".+data FilterRule = FilterRule+  { frName :: Text+  , frValue :: Text+  } deriving (Show, Eq)++type Arn = Text++-- | A data-type representing the configuration for a particular+-- notification system. It could represent a Queue, Topic or Lambda+-- Function configuration.+data NotificationConfig = NotificationConfig+  { ncId :: Text+  , ncArn :: Arn+  , ncEvents :: [Event]+  , ncFilter :: Filter+  } deriving (Show, Eq)++-- | A data-type to represent bucket notification configuration. It is+-- a collection of queue, topic or lambda function configurations. The+-- structure of the types follow closely the XML representation+-- described at+-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>+data Notification = Notification+  { nQueueConfigurations :: [NotificationConfig]+  , nTopicConfigurations :: [NotificationConfig]+  , nCloudFunctionConfigurations :: [NotificationConfig]+  } deriving (Eq, Show)++instance Default Notification where+  def = Notification [] [] []+ -- | Represents different kinds of payload that are used with S3 API -- requests. data Payload = PayloadBS ByteString@@ -289,19 +399,26 @@   , riQueryParams :: Query   , riHeaders :: [Header]   , riPayload :: Payload-  , riPayloadHash :: ByteString+  , riPayloadHash :: Maybe ByteString   , riRegion :: Maybe Region   , riNeedsLocation :: Bool   }  instance Default RequestInfo where-  def = RequestInfo HT.methodGet def def def def def "" def True+  def = RequestInfo HT.methodGet def def def def def Nothing def True  getPathFromRI :: RequestInfo -> ByteString-getPathFromRI ri = B.concat parts-  where-    objPart = maybe [] (\o -> ["/", encodeUtf8 o]) $ riObject ri-    parts = maybe ["/"] (\b -> "/" : encodeUtf8 b : objPart) $ riBucket ri+getPathFromRI ri =+  let+    b = riBucket ri+    o = riObject ri+    segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)+  in+    B.concat ["/", B.intercalate "/" segments]++-- | Time to expire for a presigned URL. It interpreted as a number of+-- seconds. The maximum duration that can be specified is 7 days.+type UrlExpiry = Int  type RegionMap = Map.Map Bucket Region 
src/Network/Minio/Data/Time.hs view
@@ -21,6 +21,7 @@   , awsDateFormat   , awsDateFormatBS   , awsParseTime+  , iso8601TimeFormat   ) where  @@ -43,3 +44,6 @@  awsParseTime :: [Char] -> Maybe UTCTime awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"++iso8601TimeFormat :: UTCTime -> [Char]+iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
src/Network/Minio/Errors.hs view
@@ -37,6 +37,7 @@            | MErrVXmlParse Text            | MErrVInvalidBucketName Text            | MErrVInvalidObjectName Text+           | MErrVInvalidUrlExpiry Int   deriving (Show, Eq)  instance Exception MErrV
src/Network/Minio/ListOps.hs view
@@ -40,6 +40,21 @@       when (lorHasMore res) $         loop (lorNextToken res) +-- | List objects in a bucket matching the given prefix. If recurse is+-- set to True objects matching prefix are recursively listed.+listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo+listObjectsV1 bucket prefix recurse = loop Nothing+  where+    loop :: Maybe Text -> C.Producer Minio ObjectInfo+    loop nextMarker = do+      let+        delimiter = bool (Just "/") Nothing recurse++      res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing+      CL.sourceList $ lorObjects' res+      when (lorHasMore' res) $+        loop (lorNextMarker res)+ -- | List incomplete uploads in a bucket matching the given prefix. If -- recurse is set to True incomplete uploads for the given prefix are -- recursively listed.
+ src/Network/Minio/PresignedOperations.hs view
@@ -0,0 +1,281 @@+--+-- Minio Haskell SDK, (C) 2017 Minio, Inc.+--+-- Licensed under the Apache License, Version 2.0 (the "License");+-- you may not use this file except in compliance with the License.+-- You may obtain a copy of the License at+--+--     http://www.apache.org/licenses/LICENSE-2.0+--+-- Unless required by applicable law or agreed to in writing, software+-- distributed under the License is distributed on an "AS IS" BASIS,+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+-- See the License for the specific language governing permissions and+-- limitations under the License.+--++module Network.Minio.PresignedOperations+  ( UrlExpiry+  , makePresignedUrl+  , presignedPutObjectUrl+  , presignedGetObjectUrl+  , presignedHeadObjectUrl++  , PostPolicyCondition(..)+  , ppCondBucket+  , ppCondContentLengthRange+  , ppCondContentType+  , ppCondKey+  , ppCondKeyStartsWith+  , ppCondSuccessActionStatus++  , PostPolicy(..)+  , PostPolicyError(..)+  , newPostPolicy+  , showPostPolicy+  , presignedPostPolicy+  ) where++import           Data.Aeson                ((.=))+import qualified Data.Aeson                as Json+import           Data.ByteString.Builder   (byteString, toLazyByteString)+import           Data.Default              (def)+import qualified Data.Map.Strict           as Map+import qualified Data.Text                 as T+import qualified Data.Time                 as Time+import qualified Network.HTTP.Types        as HT+import           Network.HTTP.Types.Header (hHost)++import           Lib.Prelude++import           Network.Minio.Data+import           Network.Minio.Data.Time+import           Network.Minio.Errors+import           Network.Minio.Sign.V4++-- | Generate a presigned URL. This function allows for advanced usage+-- - for simple cases prefer the `presigned*Url` functions.+--+-- If region is Nothing, it is picked up from the connection+-- information (no check of bucket existence is performed).+--+-- All extra query parameters or headers are signed, and therefore are+-- required to be sent when the generated URL is actually used.+makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object+                 -> Maybe Region -> HT.Query -> HT.RequestHeaders+                 -> Minio ByteString+makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do+  when (expiry > 7*24*3600 || expiry < 0) $+    throwM $ MErrVInvalidUrlExpiry expiry++  ci <- asks mcConnInfo++  let+    host = formatBS "{}:{}" (connectHost ci, connectPort ci)+    hostHeader = (hHost, host)+    ri = def { riMethod = method+             , riBucket = bucket+             , riObject = object+             , riQueryParams = extraQuery+             , riHeaders = hostHeader : extraHeaders+             , riRegion = Just $ maybe (connectRegion ci) identity region+             }++  signPairs <- liftIO $ signV4 ci ri (Just expiry)++  let+    qpToAdd = (fmap . fmap) Just signPairs+    queryStr = HT.renderQueryBuilder True (riQueryParams ri ++ qpToAdd)+    scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci++  return $ toS $ toLazyByteString $+    scheme <> byteString host <> byteString (getPathFromRI ri) <> queryStr++-- | Generate a URL with authentication signature to PUT (upload) an+-- object. Any extra headers if passed, are signed, and so they are+-- required when the URL is used to upload data. This could be used,+-- for example, to set user-metadata on the object.+--+-- For a list of possible headers to pass, please refer to the PUT+-- object REST API AWS S3 documentation.+presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders+                      -> Minio ByteString+presignedPutObjectUrl bucket object expirySeconds extraHeaders =+  makePresignedUrl expirySeconds HT.methodPut+  (Just bucket) (Just object) Nothing [] extraHeaders++-- | Generate a URL with authentication signature to GET (download) an+-- object. All extra query parameters and headers passed here will be+-- signed and are required when the generated URL is used. Query+-- parameters could be used to change the response headers sent by the+-- server. Headers can be used to set Etag match conditions among+-- others.+--+-- For a list of possible request parameters and headers, please refer+-- to the GET object REST API AWS S3 documentation.+presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query+                      -> HT.RequestHeaders -> Minio ByteString+presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders =+  makePresignedUrl expirySeconds HT.methodGet+  (Just bucket) (Just object) Nothing extraQuery extraHeaders++-- | Generate a URL with authentication signature to make a HEAD+-- request on an object. This is used to fetch metadata about an+-- object. All extra headers passed here will be signed and are+-- required when the generated URL is used.+--+-- For a list of possible headers to pass, please refer to the HEAD+-- object REST API AWS S3 documentation.+presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry+                       -> HT.RequestHeaders -> Minio ByteString+presignedHeadObjectUrl bucket object expirySeconds extraHeaders =+  makePresignedUrl expirySeconds HT.methodHead+  (Just bucket) (Just object) Nothing [] extraHeaders++-- | Represents individual conditions in a Post Policy document.+data PostPolicyCondition = PPCStartsWith Text Text+                         | PPCEquals Text Text+                         | PPCRange Text Int64 Int64+                         deriving (Show, Eq)++instance Json.ToJSON PostPolicyCondition where+  toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]+  toJSON (PPCEquals k v) = Json.object [k .= v]+  toJSON (PPCRange k minVal maxVal) =+    Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]++  toEncoding (PPCStartsWith k v) = Json.foldable ["starts-with", k, v]+  toEncoding (PPCEquals k v) = Json.pairs (k .= v)+  toEncoding (PPCRange k minVal maxVal) =+    Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]++-- | A PostPolicy is required to perform uploads via browser forms.+data PostPolicy = PostPolicy {+    expiration :: UTCTime+  , conditions :: [PostPolicyCondition]+  } deriving (Show, Eq)++instance Json.ToJSON PostPolicy where+  toJSON (PostPolicy e c) =+    Json.object $ [ "expiration" .= iso8601TimeFormat e+                  , "conditions" .= c+                  ]+  toEncoding (PostPolicy e c) =+    Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)++-- | Possible validation errors when creating a PostPolicy.+data PostPolicyError = PPEKeyNotSpecified+                     | PPEBucketNotSpecified+                     | PPEConditionKeyEmpty+                     | PPERangeInvalid+                     deriving (Eq, Show)++-- | Set the bucket name that the upload should use.+ppCondBucket :: Bucket -> PostPolicyCondition+ppCondBucket = PPCEquals "bucket"++-- | Set the content length range constraint with minimum and maximum+-- byte count values.+ppCondContentLengthRange :: Int64 -> Int64+                         -> PostPolicyCondition+ppCondContentLengthRange = PPCRange "content-length-range"++-- | Set the content-type header for the upload.+ppCondContentType :: Text -> PostPolicyCondition+ppCondContentType = PPCEquals "Content-Type"++-- | Set the object name constraint for the upload.+ppCondKey :: Object -> PostPolicyCondition+ppCondKey = PPCEquals "key"++-- | Set the object name prefix constraint for the upload.+ppCondKeyStartsWith :: Object -> PostPolicyCondition+ppCondKeyStartsWith = PPCStartsWith "key"++-- | Status code that the S3-server should send on a successful POST+-- upload+ppCondSuccessActionStatus :: Int -> PostPolicyCondition+ppCondSuccessActionStatus n =+  PPCEquals "success_action_status" (show n)++-- | This function creates a PostPolicy after validating its+-- arguments.+newPostPolicy :: UTCTime -> [PostPolicyCondition]+              -> Either PostPolicyError PostPolicy+newPostPolicy expirationTime conds+  -- object name condition must be present+  | not $ any (keyEquals "key") conds =+      Left PPEKeyNotSpecified++  -- bucket name condition must be present+  | not $ any (keyEquals "bucket") conds =+      Left PPEBucketNotSpecified++  -- a condition with an empty key is invalid+  | any (keyEquals "") conds || any isEmptyRangeKey conds =+      Left PPEConditionKeyEmpty++  -- invalid range check+  | any isInvalidRange conds =+      Left PPERangeInvalid++  -- all good!+  | otherwise =+      return $ PostPolicy expirationTime conds++  where+    keyEquals k' (PPCStartsWith k _) = k == k'+    keyEquals k' (PPCEquals k _)     = k == k'+    keyEquals _ _                    = False++    isEmptyRangeKey (PPCRange k _ _) = k == ""+    isEmptyRangeKey _                = False++    isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma+    isInvalidRange _                  = False++-- | Convert Post Policy to a string (e.g. for printing).+showPostPolicy :: PostPolicy -> ByteString+showPostPolicy = toS . Json.encode++-- | Generate a presigned URL and POST policy to upload files via a+-- browser. On success, this function returns a URL and POST+-- form-data.+presignedPostPolicy :: PostPolicy+                    -> Minio (ByteString, Map.Map Text ByteString)+presignedPostPolicy p = do+  ci <- asks mcConnInfo+  signTime <- liftIO $ Time.getCurrentTime++  let+    extraConditions =+      [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)+      , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"+      , PPCEquals "x-amz-credential"+        (T.intercalate "/" [connectAccessKey ci,+                            decodeUtf8 $ mkScope signTime region])+      ]+    ppWithCreds = p {+      conditions = conditions p ++ extraConditions+      }+    signData = signV4PostPolicy (showPostPolicy ppWithCreds)+               signTime ci++    -- compute form-data+    mkPair (PPCStartsWith k v) = Just (k, v)+    mkPair (PPCEquals k v)     = Just (k, v)+    mkPair _                   = Nothing+    formFromPolicy = Map.map toS $ Map.fromList $ catMaybes $+                     mkPair <$> conditions ppWithCreds+    formData = formFromPolicy `Map.union` signData++    -- compute POST upload URL+    bucket = Map.findWithDefault "" "bucket" formData+    scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci+    host = formatBS "{}:{}" (connectHost ci, connectPort ci)+    region = connectRegion ci++    url = toS $ toLazyByteString $ scheme <> byteString host <>+          byteString "/" <> byteString (toS bucket) <> byteString "/"++  return (url, formData)
src/Network/Minio/PutObject.hs view
@@ -25,21 +25,16 @@   ) where  -import qualified Data.ByteString as B import qualified Data.Conduit as C-import           Data.Conduit.Binary (sourceHandleRange) import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Combinators as CC import qualified Data.Conduit.List as CL import qualified Data.List as List-import qualified Data.Map.Strict as Map  import           Lib.Prelude  import           Network.Minio.Data-import           Network.Minio.Data.Crypto import           Network.Minio.Errors-import           Network.Minio.ListOps import           Network.Minio.S3API import           Network.Minio.Utils @@ -117,35 +112,17 @@       | st + m >= sz = [(st, sz - st)]       | otherwise = (st, m) : loop (st + m) sz --- returns partinfo if part is already uploaded.-checkUploadNeeded :: Payload -> PartNumber-                  -> Map.Map PartNumber ObjectPartInfo-                  -> Minio (Maybe PartTuple)-checkUploadNeeded payload n pmap = do-  (md5hash, pSize) <- case payload of-    PayloadBS bs -> return (hashMD5 bs, fromIntegral $ B.length bs)-    PayloadH h off size -> fmap (, size) $-      hashMD5FromSource $ sourceHandleRange h (Just $ fromIntegral off)-      (Just $ fromIntegral size)-  case Map.lookup n pmap of-    Nothing -> return Nothing-    Just (ObjectPartInfo _ etag size _) -> return $-      bool Nothing (Just (n, etag)) $-      md5hash == encodeUtf8 etag && size == pSize- parallelMultipartUpload :: Bucket -> Object -> FilePath -> Int64                         -> Minio ETag parallelMultipartUpload b o filePath size = do-  (uidMay, pmap) <- getExistingUpload b o--  -- get a new upload id if needed.-  uploadId <- maybe (newMultipartUpload b o []) return uidMay+  -- get a new upload id.+  uploadId <- newMultipartUpload b o []    let partSizeInfo = selectPartSizes size    -- perform upload with 10 threads   uploadedPartsE <- limitedMapConcurrently 10-                    (uploadPart pmap uploadId) partSizeInfo+                    (uploadPart uploadId) partSizeInfo    -- if there were any errors, rethrow exception.   mapM_ throwM $ lefts uploadedPartsE@@ -153,22 +130,17 @@   -- if we get here, all parts were successfully uploaded.   completeMultipartUpload b o uploadId $ rights uploadedPartsE   where-    uploadPart pmap uploadId (partNum, offset, sz) =+    uploadPart uploadId (partNum, offset, sz) =       withNewHandle filePath $ \h -> do         let payload = PayloadH h offset sz-        pInfoMay <- checkUploadNeeded payload partNum pmap-        maybe-          (putObjectPart b o uploadId partNum [] payload)-          return pInfoMay+        putObjectPart b o uploadId partNum [] payload  -- | Upload multipart object from conduit source sequentially sequentialMultipartUpload :: Bucket -> Object -> Maybe Int64                           -> C.Producer Minio ByteString -> Minio ETag sequentialMultipartUpload b o sizeMay src = do-  (uidMay, pmap) <- getExistingUpload b o--  -- get a new upload id if needed.-  uploadId <- maybe (newMultipartUpload b o []) return uidMay+  -- get a new upload id.+  uploadId <- newMultipartUpload b o []    -- upload parts in loop   let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay@@ -176,35 +148,21 @@   uploadedParts <- src               C..| chunkBSConduit sizes               C..| CL.map PayloadBS-              C..| checkAndUpload uploadId pmap pnums+              C..| uploadPart' uploadId pnums               C.$$ CC.sinkList    -- complete multipart upload   completeMultipartUpload b o uploadId uploadedParts    where-    checkAndUpload _ _ [] = return ()-    checkAndUpload uid pmap (pn:pns) = do+    uploadPart' _ [] = return ()+    uploadPart' uid (pn:pns) = do       payloadMay <- C.await       case payloadMay of         Nothing -> return ()-        Just payload -> do partMay <- lift $ checkUploadNeeded payload pn pmap-                           pinfo <- maybe-                                    (lift $ putObjectPart b o uid pn [] payload)-                                    return partMay+        Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload                            C.yield pinfo-                           checkAndUpload uid pmap pns---- | Looks for incomplete uploads for an object. Returns the first one--- if there are many.-getExistingUpload :: Bucket -> Object-                  -> Minio (Maybe UploadId, Map.Map PartNumber ObjectPartInfo)-getExistingUpload b o = do-  uidMay <- (fmap . fmap) uiUploadId $-            listIncompleteUploads b (Just o) False C.$$ CC.head-  parts <- maybe (return [])-    (\uid -> listIncompleteParts b o uid C.$$ CC.sinkList) uidMay-  return (uidMay, Map.fromList $ map (\p -> (opiNumber p, p)) parts)+                           uploadPart' uid pns  -- | Copy an object using single or multipart copy strategy. copyObjectInternal :: Bucket -> Object -> CopyPartSource
src/Network/Minio/S3API.hs view
@@ -26,10 +26,13 @@   -- * Listing objects   --------------------   , ListObjectsResult(..)+  , ListObjectsV1Result(..)   , listObjects'+  , listObjectsV1'    -- * Retrieving buckets   , headBucket+   -- * Retrieving objects   -----------------------   , getObject'@@ -64,6 +67,23 @@   , deleteBucket   , deleteObject +  -- * Presigned Operations+  -----------------------------+  , module Network.Minio.PresignedOperations++  -- * Bucket Notifications+  -------------------------+  , Notification(..)+  , NotificationConfig(..)+  , Arn+  , Event(..)+  , Filter(..)+  , FilterKey(..)+  , FilterRules(..)+  , FilterRule(..)+  , getBucketNotification+  , putBucketNotification+  , removeAllBucketNotification   ) where  import           Control.Monad.Catch (catches, Handler(..))@@ -81,6 +101,7 @@ import           Network.Minio.Utils import           Network.Minio.XmlGenerator import           Network.Minio.XmlParser+import           Network.Minio.PresignedOperations   -- | Fetch all buckets from the service.@@ -144,6 +165,24 @@     return etag  -- | List objects in a bucket matching prefix up to delimiter,+-- starting from nextMarker.+listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int+            -> Minio ListObjectsV1Result+listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do+  resp <- executeRequest $ def { riMethod = HT.methodGet+                               , riBucket = Just bucket+                               , riQueryParams = mkOptionalParams params+                               }+  parseListObjectsV1Response $ NC.responseBody resp+  where+    params = [+        ("marker", nextMarker)+      , ("prefix", prefix)+      , ("delimiter", delimiter)+      , ("max-keys", show <$> maxKeys)+      ]++-- | List objects in a bucket matching prefix up to delimiter, -- starting from nextToken. listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int             -> Minio ListObjectsResult@@ -358,3 +397,26 @@                                    , riBucket = Just bucket                                    }       return $ NC.responseStatus resp == HT.ok200++-- | Set the notification configuration on a bucket.+putBucketNotification :: Bucket -> Notification -> Minio ()+putBucketNotification bucket ncfg =+  void $ executeRequest $ def { riMethod = HT.methodPut+                              , riBucket = Just bucket+                              , riQueryParams = [("notification", Nothing)]+                              , riPayload = PayloadBS $+                                            mkPutNotificationRequest ncfg+                              }++-- | Retrieve the notification configuration on a bucket.+getBucketNotification :: Bucket -> Minio Notification+getBucketNotification bucket = do+  resp <- executeRequest $ def { riMethod = HT.methodGet+                               , riBucket = Just bucket+                               , riQueryParams = [("notification", Nothing)]+                               }+  parseNotification $ NC.responseBody resp++-- | Remove all notifications configured on a bucket.+removeAllBucketNotification :: Bucket -> Minio ()+removeAllBucketNotification = flip putBucketNotification def
src/Network/Minio/Sign/V4.hs view
@@ -18,21 +18,27 @@   (     signV4   , signV4AtTime-  , getScope+  , signV4PostPolicy+  , mkScope   , getHeadersToSign-  , getCanonicalRequest+  , mkCanonicalRequest+  , mkStringToSign+  , mkSigningKey+  , computeSignature   , SignV4Data(..)   , debugPrintSignV4Data   ) where  import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as B8-import           Data.ByteString.Char8 (pack) import           Data.CaseInsensitive (mk) import qualified Data.CaseInsensitive as CI import qualified Data.Set as Set import qualified Data.Time as Time+import qualified Data.ByteString.Base64 as Base64+import qualified Data.Map.Strict as Map import           Network.HTTP.Types (Header)+import qualified Network.HTTP.Types.Header as H  import           Lib.Prelude import           Network.Minio.Data@@ -40,35 +46,35 @@ import           Network.Minio.Data.Crypto import           Network.Minio.Data.Time +-- these headers are not included in the string to sign when signing a+-- request ignoredHeaders :: Set ByteString-ignoredHeaders = Set.fromList $ map CI.foldedCase [-  mk "Authorization",-  mk "Content-Type",-  mk "Content-Length",-  mk "User-Agent"-  ]+ignoredHeaders = Set.fromList $ map CI.foldedCase+                 [ H.hAuthorization+                 , H.hContentType+                 , H.hContentLength+                 , H.hUserAgent+                 ]  data SignV4Data = SignV4Data {     sv4SignTime :: UTCTime   , sv4Scope :: ByteString   , sv4CanonicalRequest :: ByteString   , sv4HeadersToSign :: [(ByteString, ByteString)]-  , sv4InputHeaders :: [Header]-  , sv4OutputHeaders :: [Header]+  , sv4Output :: [(ByteString, ByteString)]   , sv4StringToSign :: ByteString   , sv4SigningKey :: ByteString   } deriving (Show)  debugPrintSignV4Data :: SignV4Data -> IO ()-debugPrintSignV4Data (SignV4Data t s cr h2s ih oh sts sk) = do+debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do   B8.putStrLn "SignV4Data:"   B8.putStr "Timestamp: " >> print t   B8.putStr "Scope: " >> B8.putStrLn s   B8.putStrLn "Canonical Request:"   B8.putStrLn cr   B8.putStr "Headers to Sign: " >> print h2s-  B8.putStr "Input headers: " >> print ih-  B8.putStr "Output headers: " >> print oh+  B8.putStr "Output: " >> print o   B8.putStr "StringToSign: " >> B8.putStrLn sts   B8.putStr "SigningKey: " >> printBytes sk   B8.putStrLn "END of SignV4Data ========="@@ -81,94 +87,161 @@ -- request path, headers, query params and payload hash, generates an -- updated set of headers, including the x-amz-date header and the -- Authorization header, which includes the signature.-signV4 :: ConnectInfo -> RequestInfo-       -> IO [Header]-signV4 ci ri = do+signV4 :: ConnectInfo -> RequestInfo -> Maybe Int+       -> IO [(ByteString, ByteString)]+signV4 !ci !ri !expiry = do   timestamp <- Time.getCurrentTime-  let signData = signV4AtTime timestamp ci ri+  let signData = signV4AtTime timestamp ci ri expiry   -- debugPrintSignV4Data signData-  return $ sv4OutputHeaders signData+  return $ sv4Output signData  -- | Takes a timestamp, server params and request params and generates--- an updated list of headers.-signV4AtTime :: UTCTime -> ConnectInfo -> RequestInfo -> SignV4Data-signV4AtTime ts ci ri =-  SignV4Data ts scope canonicalRequest headersToSign (riHeaders ri) outHeaders stringToSign signingKey-  where-    outHeaders = authHeader : headersWithDate-    timeBS = awsTimeFormatBS ts-    dateHeader = (mk "X-Amz-Date", timeBS)-    hostHeader = (mk "host", encodeUtf8 $ format "{}:{}"-                   [connectHost ci, show $ connectPort ci])--    headersWithDate = dateHeader : hostHeader : riHeaders ri--    authHeader = (mk "Authorization", authHeaderValue)-+-- AWS Sign V4 data. For normal requests (i.e. without an expiry+-- time), the output is the list of headers to add to authenticate the+-- request.+--+-- If `expiry` is not Nothing, it is assumed that a presigned request+-- is being created. The expiry is interpreted as an integer number of+-- seconds. The output will be the list of query-parameters to add to+-- the request.+signV4AtTime :: UTCTime -> ConnectInfo -> RequestInfo -> Maybe Int+             -> SignV4Data+signV4AtTime ts ci ri expiry =+  let     region = maybe (connectRegion ci) identity $ riRegion ri+    scope = mkScope ts region+    accessKey = toS $ connectAccessKey ci+    secretKey = toS $ connectSecretKey ci -    scope = getScope ts region+    -- headers to be added to the request+    datePair = ("X-Amz-Date", awsTimeFormatBS ts)+    computedHeaders = riHeaders ri +++                      if isJust expiry+                      then []+                      else [(\(x, y) -> (mk x, y)) datePair]+    headersToSign = getHeadersToSign computedHeaders+    signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign -    authHeaderValue = B.concat [-      "AWS4-HMAC-SHA256 Credential=",-      encodeUtf8 (connectAccessKey ci), "/", scope,-      ", SignedHeaders=", signedHeaders,-      ", Signature=", signature-      ]+    -- query-parameters to be added before signing for presigned URLs+    -- (i.e. when `isJust expiry`)+    authQP = [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256")+             , ("X-Amz-Credential", B.concat [accessKey, "/", scope])+             , datePair+             , ("X-Amz-Expires", maybe "" show expiry)+             , ("X-Amz-SignedHeaders", signedHeaderKeys)+             ]+    finalQP = riQueryParams ri +++              if isJust expiry+              then (fmap . fmap) Just authQP+              else [] -    headersToSign = getHeadersToSign headersWithDate+    -- 1. compute canonical request+    canonicalRequest = mkCanonicalRequest (ri {riQueryParams = finalQP})+                       headersToSign -    signedHeaders = B.intercalate ";" $ map fst headersToSign+    -- 2. compute string to sign+    stringToSign = mkStringToSign ts scope canonicalRequest -    signature = digestToBase16 $ hmacSHA256 stringToSign signingKey+    -- 3.1 compute signing key+    signingKey = mkSigningKey ts region secretKey -    signingKey = hmacSHA256RawBS "aws4_request"-               . hmacSHA256RawBS "s3"-               . hmacSHA256RawBS (encodeUtf8 region)-               . hmacSHA256RawBS (awsDateFormatBS ts)-               $ B.concat ["AWS4", encodeUtf8 $ connectSecretKey ci]+    -- 3.2 compute signature+    signature = computeSignature stringToSign signingKey -    stringToSign  = B.intercalate "\n"-      [ "AWS4-HMAC-SHA256"-      , timeBS+    -- 4. compute auth header+    authValue = B.concat+      [ "AWS4-HMAC-SHA256 Credential="+      , accessKey+      , "/"       , scope-      , hashSHA256 canonicalRequest+      , ", SignedHeaders="+      , signedHeaderKeys+      , ", Signature="+      , signature       ]+    authHeader = (H.hAuthorization, authValue) -    canonicalRequest = getCanonicalRequest ri headersToSign+    -- finally compute output pairs+    output = if isJust expiry+             then ("X-Amz-Signature", signature) : authQP+             else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,+                   datePair] +  in+    SignV4Data ts scope canonicalRequest headersToSign output+    stringToSign signingKey -getScope :: UTCTime -> Region -> ByteString-getScope ts region = B.intercalate "/" [-  pack $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,-  encodeUtf8 region, "s3", "aws4_request"++mkScope :: UTCTime -> Region -> ByteString+mkScope ts region = B.intercalate "/"+  [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts+  , toS region+  , "s3"+  , "aws4_request"   ]  getHeadersToSign :: [Header] -> [(ByteString, ByteString)]-getHeadersToSign h =-  sort $+getHeadersToSign !h =   filter (flip Set.notMember ignoredHeaders . fst) $   map (\(x, y) -> (CI.foldedCase x, stripBS y)) h -getCanonicalRequest :: RequestInfo -> [(ByteString, ByteString)] -> ByteString-getCanonicalRequest ri headersForSign = B.intercalate "\n" [-  riMethod ri,-  uriEncode False path,-  canonicalQueryString,-  canonicalHeaders,-  signedHeaders,-  riPayloadHash ri-  ]-  where-    path = getPathFromRI ri-+mkCanonicalRequest :: RequestInfo -> [(ByteString, ByteString)]+                    -> ByteString+mkCanonicalRequest !ri !headersForSign =+  let     canonicalQueryString = B.intercalate "&" $       map (\(x, y) -> B.concat [x, "=", y]) $       sort $ map (\(x, y) ->                     (uriEncode True x, maybe "" (uriEncode True) y)) $       riQueryParams ri +    sortedHeaders = sort headersForSign+     canonicalHeaders = B.concat $-      map (\(x, y) -> B.concat [x, ":", y, "\n"]) headersForSign+      map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders -    signedHeaders = B.intercalate ";" $ map fst headersForSign+    signedHeaders = B.intercalate ";" $ map fst sortedHeaders++  in+    B.intercalate "\n"+    [ riMethod ri+    , uriEncode False $ getPathFromRI ri+    , canonicalQueryString+    , canonicalHeaders+    , signedHeaders+    , maybe "UNSIGNED-PAYLOAD" identity $ riPayloadHash ri+    ]++mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString+mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"+                                             [ "AWS4-HMAC-SHA256"+                                             , awsTimeFormatBS ts+                                             , scope+                                             , hashSHA256 canonicalRequest+                                             ]++mkSigningKey :: UTCTime -> Region -> ByteString -> ByteString+mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"+                                  . hmacSHA256RawBS "s3"+                                  . hmacSHA256RawBS (toS region)+                                  . hmacSHA256RawBS (awsDateFormatBS ts)+                                  $ B.concat ["AWS4", secretKey]++computeSignature :: ByteString -> ByteString -> ByteString+computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key++-- | Takes a validated Post Policy JSON bytestring, the signing time,+-- and ConnInfo and returns form-data for the POST upload containing+-- just the signature and the encoded post-policy.+signV4PostPolicy :: ByteString -> UTCTime -> ConnectInfo+                 -> Map.Map Text ByteString+signV4PostPolicy !postPolicyJSON !signTime !ci =+  let+    stringToSign = Base64.encode postPolicyJSON+    region = connectRegion ci+    signingKey = mkSigningKey signTime region $ toS $ connectSecretKey ci+    signature = computeSignature stringToSign signingKey+  in+    Map.fromList [ ("x-amz-signature", signature)+                 , ("policy", stringToSign)+                 ]
src/Network/Minio/Utils.hs view
@@ -35,7 +35,9 @@ import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types.Header as Hdr import qualified System.IO as IO+import           Data.CaseInsensitive (mk) + import           Lib.Prelude  import           Network.Minio.XmlParser (parseErrResponse)@@ -87,6 +89,8 @@       R.release rkey       return resE +mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]+mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))  lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
src/Network/Minio/XmlGenerator.hs view
@@ -17,12 +17,13 @@ module Network.Minio.XmlGenerator   ( mkCreateBucketConfig   , mkCompleteMultipartUploadRequest+  , mkPutNotificationRequest   ) where   import qualified Data.ByteString.Lazy as LBS-import qualified Data.Map as M-import qualified Data.Text as T+import qualified Data.Map             as M+import qualified Data.Text            as T import           Text.XML  import           Lib.Prelude@@ -55,3 +56,47 @@                                  [NodeContent etag]                                ]     cmur = Document (Prologue [] Nothing []) root []++-- Simplified XML representation without element attributes.+data XNode = XNode Text [XNode]+           | XLeaf Text Text+  deriving (Eq, Show)++toXML :: XNode -> ByteString+toXML node = LBS.toStrict $ renderLBS def $+  Document (Prologue [] Nothing []) (xmlNode node) []+  where+    xmlNode :: XNode -> Element+    xmlNode (XNode name nodes)   = Element (s3Name name) M.empty $+                                   map (NodeElement . xmlNode) nodes+    xmlNode (XLeaf name content) = Element (s3Name name) M.empty+                                   [NodeContent content]++class ToXNode a where+  toXNode :: a -> XNode++instance ToXNode Event where+  toXNode = XLeaf "Event" . show++instance ToXNode Notification where+  toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $+    map (toXNodesWithArnName "QueueConfiguration" "Queue") qc +++    map (toXNodesWithArnName "TopicConfiguration" "Topic") tc +++    map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc++toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode+toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =+  XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events +++  [toXNode fRule]++instance ToXNode Filter where+  toXNode (Filter (FilterKey (FilterRules rules))) =+    XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]++getFRXNode :: FilterRule -> XNode+getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n+                                                 , XLeaf "Value" v+                                                 ]++mkPutNotificationRequest :: Notification -> ByteString+mkPutNotificationRequest = toXML . toXNode
src/Network/Minio/XmlParser.hs view
@@ -21,18 +21,20 @@   , parseCompleteMultipartUploadResponse   , parseCopyObjectResponse   , parseListObjectsResponse+  , parseListObjectsV1Response   , parseListUploadsResponse   , parseListPartsResponse   , parseErrResponse+  , parseNotification   ) where  import           Control.Monad.Trans.Resource-import           Data.List (zip3, zip4)-import qualified Data.Text as T-import           Data.Text.Read (decimal)+import           Data.List                    (zip3, zip4)+import qualified Data.Text                    as T+import           Data.Text.Read               (decimal) import           Data.Time import           Text.XML-import           Text.XML.Cursor hiding (bool)+import           Text.XML.Cursor              hiding (bool)  import           Lib.Prelude @@ -55,7 +57,8 @@                . T.unpack  parseDecimal :: (MonadThrow m, Integral a) => Text -> m a-parseDecimal numStr = either (throwM . MErrVXmlParse . show) return $ fst <$> decimal numStr+parseDecimal numStr = either (throwM . MErrVXmlParse . show) return $+                      fst <$> decimal numStr  parseDecimals :: (MonadThrow m, Integral a) => [Text] -> m [a] parseDecimals numStr = forM numStr parseDecimal@@ -86,15 +89,13 @@   return $ bool "us-east-1" region $ region /= ""  -- | Parse the response XML of an newMultipartUpload call.-parseNewMultipartUpload :: (MonadThrow m)-                        => LByteString -> m UploadId+parseNewMultipartUpload :: (MonadThrow m) => LByteString -> m UploadId parseNewMultipartUpload xmldata = do   r <- parseRoot xmldata   return $ T.concat $ r $// s3Elem "UploadId" &/ content  -- | Parse the response XML of completeMultipartUpload call.-parseCompleteMultipartUploadResponse :: (MonadThrow m)-                                     => LByteString -> m ETag+parseCompleteMultipartUploadResponse :: (MonadThrow m) => LByteString -> m ETag parseCompleteMultipartUploadResponse xmldata = do   r <- parseRoot xmldata   return $ T.concat $ r $// s3Elem "ETag" &/ content@@ -109,9 +110,36 @@   mtime <- parseS3XMLTime mtimeStr   return (T.concat $ r $// s3Elem "ETag" &/ content, mtime) +-- | Parse the response XML of a list objects v1 call.+parseListObjectsV1Response :: (MonadThrow m)+                         => LByteString -> m ListObjectsV1Result+parseListObjectsV1Response xmldata = do+  r <- parseRoot xmldata+  let+    hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)++    nextMarker = headMay $ r $/ s3Elem "NextMarker" &/ content++    prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content++    keys = r $/ s3Elem "Contents" &/ s3Elem "Key" &/ content+    modTimeStr = r $/ s3Elem "Contents" &/ s3Elem "LastModified" &/ content+    etagsList = r $/ s3Elem "Contents" &/ s3Elem "ETag" &/ content+    -- if response xml contains empty etag response fill them with as+    -- many empty Text for the zip4 below to work as intended.+    etags = etagsList ++ repeat ""+    sizeStr = r $/ s3Elem "Contents" &/ s3Elem "Size" &/ content++  modTimes <- mapM parseS3XMLTime modTimeStr+  sizes <- parseDecimals sizeStr++  let+    objects = map (uncurry4 ObjectInfo) $ zip4 keys modTimes etags sizes++  return $ ListObjectsV1Result hasMore nextMarker objects prefixes+ -- | Parse the response XML of a list objects call.-parseListObjectsResponse :: (MonadThrow m)-                         => LByteString -> m ListObjectsResult+parseListObjectsResponse :: (MonadThrow m) => LByteString -> m ListObjectsResult parseListObjectsResponse xmldata = do   r <- parseRoot xmldata   let@@ -138,8 +166,7 @@   return $ ListObjectsResult hasMore nextToken objects prefixes  -- | Parse the response XML of a list incomplete multipart upload call.-parseListUploadsResponse :: (MonadThrow m)-                         => LByteString -> m ListUploadsResult+parseListUploadsResponse :: (MonadThrow m) => LByteString -> m ListUploadsResult parseListUploadsResponse xmldata = do   r <- parseRoot xmldata   let@@ -158,8 +185,7 @@    return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes -parseListPartsResponse :: (MonadThrow m)-                       => LByteString -> m ListPartsResult+parseListPartsResponse :: (MonadThrow m) => LByteString -> m ListPartsResult parseListPartsResponse xmldata = do   r <- parseRoot xmldata   let@@ -182,10 +208,35 @@   return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos  -parseErrResponse :: (MonadThrow m)-                 => LByteString -> m ServiceErr+parseErrResponse :: (MonadThrow m) => LByteString -> m ServiceErr parseErrResponse xmldata = do   r <- parseRoot xmldata   let code = T.concat $ r $/ element "Code" &/ content       message = T.concat $ r $/ element "Message" &/ content   return $ toServiceErr code message++parseNotification :: (MonadThrow m) => LByteString -> m Notification+parseNotification xmldata = do+  r <- parseRoot xmldata+  let qcfg = map node $ r $/ s3Elem "QueueConfiguration"+      tcfg = map node $ r $/ s3Elem "TopicConfiguration"+      lcfg = map node $ r $/ s3Elem "CloudFunctionConfiguration"+  Notification <$> (mapM (parseNode "Queue") qcfg)+    <*> (mapM (parseNode "Topic") tcfg)+    <*> (mapM (parseNode "CloudFunction") lcfg)+  where++    getFilterRule c =+      let name = T.concat $ c $/ s3Elem "Name" &/ content+          value = T.concat $ c $/ s3Elem "Value" &/ content+      in FilterRule name value++    parseNode arnName nodeData = do+      let c = fromNode nodeData+          id = T.concat $ c $/ s3Elem "Id" &/ content+          arn = T.concat $ c $/ s3Elem arnName &/ content+          events = catMaybes $ map textToEvent $ c $/ s3Elem "Event" &/ content+          rules = c $/ s3Elem "Filter" &/ s3Elem "S3Key" &/+                  s3Elem "FilterRule" &| getFilterRule+      return $ NotificationConfig id arn events+        (Filter $ FilterKey $ FilterRules rules)
test/LiveServer.hs view
@@ -14,30 +14,34 @@ -- limitations under the License. -- -import qualified Test.QuickCheck as Q+import qualified Test.QuickCheck                       as Q import           Test.Tasty import           Test.Tasty.HUnit-import           Test.Tasty.QuickCheck as QC+import           Test.Tasty.QuickCheck                 as QC  import           Lib.Prelude -import           System.Directory (getTemporaryDirectory)-import qualified System.IO as SIO+import           System.Directory                      (getTemporaryDirectory)+import qualified System.IO                             as SIO -import qualified Control.Monad.Catch as MC-import qualified Control.Monad.Trans.Resource as R-import qualified Data.ByteString as BS-import           Data.Conduit (($$), yield)-import qualified Data.Conduit as C-import qualified Data.Conduit.Binary as CB-import           Data.Conduit.Combinators (sinkList)-import           Data.Default (Default(..))-import qualified Data.Text as T-import           System.Environment (lookupEnv)+import qualified Control.Monad.Catch                   as MC+import qualified Control.Monad.Trans.Resource          as R+import qualified Data.ByteString                       as BS+import           Data.Conduit                          (yield, ($$))+import qualified Data.Conduit                          as C+import qualified Data.Conduit.Binary                   as CB+import           Data.Conduit.Combinators              (sinkList)+import           Data.Default                          (Default (..))+import qualified Data.Map.Strict                       as Map+import qualified Data.Text                             as T+import qualified Data.Time                             as Time+import qualified Network.HTTP.Client.MultipartFormData as Form+import qualified Network.HTTP.Conduit                  as NC+import qualified Network.HTTP.Types                    as HT+import           System.Environment                    (lookupEnv)  import           Network.Minio import           Network.Minio.Data-import           Network.Minio.Errors import           Network.Minio.ListOps import           Network.Minio.PutObject import           Network.Minio.S3API@@ -92,76 +96,8 @@     deleteBucket b   isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret) -liveServerUnitTests :: TestTree-liveServerUnitTests = testGroup "Unit tests against a live server"-  [ funTestWithBucket "Basic tests" $ \step bucket -> do-      step "getService works and contains the test bucket."-      buckets <- getService-      unless (length (filter (== bucket) $ map biName buckets) == 1) $-        liftIO $-        assertFailure ("The bucket " ++ show bucket ++-                       " was expected to exist.")--      step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."-      mbE <- MC.try $ makeBucket bucket Nothing-      case mbE of-        Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou-        _ -> return ()--      step "makeBucket with an invalid bucket name and check for appropriate exception."-      invalidMBE <- MC.try $ makeBucket "invalidBucketName" Nothing-      case invalidMBE of-        Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"-        _ -> return ()--      step "getLocation works"-      region <- getLocation bucket-      liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)--      step "singlepart putObject works"-      fPutObject bucket "lsb-release" "/etc/lsb-release"--      step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"-      fpE <- MC.try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release"-      case fpE of-        Left exn -> liftIO $ exn @?= NoSuchBucket-        _ -> return ()--      outFile <- mkRandFile 0-      step "simple fGetObject works"-      fGetObject bucket "lsb-release" outFile--      step "fGetObject a non-existent object and check for NoSuchKey exception"-      resE <- MC.try $ fGetObject bucket "noSuchKey" outFile-      case resE of-        Left exn -> liftIO $ exn @?= NoSuchKey-        _ -> return ()---      step "create new multipart upload works"-      uid <- newMultipartUpload bucket "newmpupload" []-      liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")--      step "abort a new multipart upload works"-      abortMultipartUpload bucket "newmpupload" uid--      step "delete object works"-      deleteObject bucket "lsb-release"--      step "statObject test"-      let object = "sample"-      step "create an object"-      inputFile <- mkRandFile 0-      fPutObject bucket object inputFile--      step "get metadata of the object"-      res <- statObject bucket object-      liftIO $ (oiSize res) @?= 0--      step "delete object"-      deleteObject bucket object--  , funTestWithBucket "Multipart Tests" $+lowLevelMultipartTest :: TestTree+lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $     \step bucket -> do       -- low-level multipart operation tests.       let object = "newmpupload"@@ -182,21 +118,24 @@       void $ completeMultipartUpload bucket object uid [partInfo]        destFile <- mkRandFile 0-      step $ "Retrieve the created object and check size"+      step  "Retrieve the created object and check size"       fGetObject bucket object destFile       gotSize <- withNewHandle destFile getFileSize       liftIO $ gotSize == Right (Just mb15) @?         "Wrong file size of put file after getting" -      step $ "Cleanup actions"+      step  "Cleanup actions"       removeObject bucket object +putObjectNoSizeTest :: TestTree+putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $+  \step bucket -> do       -- putObject test (conduit source, no size specified)       let obj = "mpart"-          mb100 = 100 * 1024 * 1024+          mb70 = 70 * 1024 * 1024        step "Prepare for putObject with from source without providing size."-      rFile <- mkRandFile mb100+      rFile <- mkRandFile mb70        step "Upload multipart file."       putObject bucket obj (CB.sourceFile rFile) Nothing@@ -205,57 +144,103 @@       destFile <- mkRandFile 0       fGetObject bucket obj destFile       gotSize <- withNewHandle destFile getFileSize-      liftIO $ gotSize == Right (Just mb100) @?+      liftIO $ gotSize == Right (Just mb70) @?         "Wrong file size of put file after getting" -      step $ "Cleanup actions"+      step "Cleanup actions"       deleteObject bucket obj -      step "Prepare for putObjectInternal with non-seekable file, with size."-      step "Upload multipart file."-      void $ putObjectInternal bucket obj $ ODFile "/dev/zero" (Just mb100)+highLevelListingTest :: TestTree+highLevelListingTest = funTestWithBucket "High-level listObjects Test" $+  \step bucket -> do+      step "High-level listObjects Test"+      step "put 3 objects"+      let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3"]+      forM_ expectedObjects $+        \obj -> fPutObject bucket obj "/etc/lsb-release" -      step "Retrieve and verify file size"-      destFile <- mkRandFile 0-      fGetObject bucket obj destFile-      gotSize <- withNewHandle destFile getFileSize-      liftIO $ gotSize == Right (Just mb100) @?-        "Wrong file size of put file after getting"+      step "High-level listing of objects"+      objects <- listObjects bucket Nothing True $$ sinkList -      step $ "Cleanup actions"-      removeObject bucket obj+      liftIO $ assertEqual "Objects match failed!" (sort expectedObjects)+        (map oiObject objects) -      step "Prepare for putObjectInternal with large file as source."-      step "upload large object"-      void $ putObjectInternal bucket "big" (ODFile "/dev/zero" $-                                             Just $ 1024*1024*100)+      step "High-level listing of objects (version 1)"+      objects <- listObjectsV1 bucket Nothing True $$ sinkList +      step "Cleanup actions"+      forM_ expectedObjects $+        \obj -> removeObject bucket obj++      step "High-level listIncompleteUploads Test"+      let object = "newmpupload"+      step "create 10 multipart uploads"+      forM_ [1..10::Int] $ \_ -> do+        uid <- newMultipartUpload bucket object []+        liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")++      step "High-level listing of incomplete multipart uploads"+      uploads <- listIncompleteUploads bucket Nothing True $$ sinkList+      -- Minio server behaviour changed to list no incomplete uploads,+      -- so the check below reflects this; this test is expected to+      -- fail on AWS S3.+      liftIO $ length uploads @?= 0+       step "cleanup"-      removeObject bucket "big"+      forM_ uploads $ \(UploadInfo _ uid _ _) ->+                        abortMultipartUpload bucket object uid +      step "High-level listIncompleteParts Test"+      let mb5 = 5 * 1024 * 1024 -  , funTestWithBucket "Listing Test" $ \step bucket -> do+      step "create a multipart upload"+      uid <- newMultipartUpload bucket object []+      liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."++      step "put object parts 1..10"+      inputFile <- mkRandFile mb5+      h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode+      forM_ [1..10] $ \pnum ->+        putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5++      step "fetch list parts"+      incompleteParts <- listIncompleteParts bucket object uid $$ sinkList+      liftIO $ length incompleteParts @?= 10++      step "cleanup"+      abortMultipartUpload bucket object uid++listingTest :: TestTree+listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do       step "listObjects' test"       step "put 10 objects"+      let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]+       forM_ [1..10::Int] $ \s ->         fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release"        step "Simple list"       res <- listObjects' bucket Nothing Nothing Nothing Nothing+      let expectedObjects = sort objects+      liftIO $ assertEqual "Objects match failed!" expectedObjects+        (map oiObject $ lorObjects res)++      step "Simple list version 1"+      res <- listObjectsV1' bucket Nothing Nothing Nothing Nothing       let expected = sort $ map (T.concat .                           ("lsb-release":) .                           (\x -> [x]) .                           T.pack .                           show) [1..10::Int]       liftIO $ assertEqual "Objects match failed!" expected-        (map oiObject $ lorObjects res)+        (map oiObject $ lorObjects' res)        step "Cleanup actions"-      forM_ [1..10::Int] $ \s -> deleteObject bucket (T.concat ["lsb-release", T.pack (show s)])+      forM_ objects $ \obj -> deleteObject bucket obj        step "listIncompleteUploads' test"-      let object = "newmpupload"       step "create 10 multipart uploads"+      let object = "newmpupload"       forM_ [1..10::Int] $ \_ -> do         uid <- newMultipartUpload bucket object []         liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")@@ -273,87 +258,75 @@         \(_, uid, _) -> abortMultipartUpload bucket object uid        step "Basic listIncompleteParts Test"-      let-        object = "newmpupload"-        mb15 = 15 * 1024 * 1024+      let mb5 = 5 * 1024 * 1024        step "create a multipart upload"       uid <- newMultipartUpload bucket object []       liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")        step "put object parts 1..10"-      inputFile <- mkRandFile mb15+      inputFile <- mkRandFile mb5       h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode       forM_ [1..10] $ \pnum ->-        putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb15+        putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5        step "fetch list parts"       listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing-      -- Minio server behaviour changed to list no incomplete uploads,-      -- so the check below reflects this; this test is expected to-      -- fail on AWS S3.-      liftIO $ (length $ lprParts listPartsResult) @?= 0+      liftIO $ (length $ lprParts listPartsResult) @?= 10       abortMultipartUpload bucket object uid -      step "High-level listObjects Test"-      step "put 3 objects"-      let expected = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3"]-      forM_ expected $-        \obj -> fPutObject bucket obj "/etc/lsb-release" -      step "High-level listing of objects"-      objects <- (listObjects bucket Nothing True) $$ sinkList--      liftIO $ assertEqual "Objects match failed!" (sort expected)-        (map oiObject objects)+liveServerUnitTests :: TestTree+liveServerUnitTests = testGroup "Unit tests against a live server"+  [ basicTests+  , listingTest+  , highLevelListingTest+  , lowLevelMultipartTest+  , putObjectNoSizeTest+  , funTestWithBucket "Multipart Tests" $+    \step bucket -> do+      step "Prepare for putObjectInternal with non-seekable file, with size."+      step "Upload multipart file."+      let mb80 = 80 * 1024 * 1024+          obj = "mpart" -      step "Cleanup actions"-      forM_ expected $-        \obj -> removeObject bucket obj+      void $ putObjectInternal bucket obj $ ODFile "/dev/zero" (Just mb80) -      step "High-level listIncompleteUploads Test"-      let object = "newmpupload"-      step "create 10 multipart uploads"-      forM_ [1..10::Int] $ \_ -> do-        uid <- newMultipartUpload bucket object []-        liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")+      step "Retrieve and verify file size"+      destFile <- mkRandFile 0+      fGetObject bucket obj destFile+      gotSize <- withNewHandle destFile getFileSize+      liftIO $ gotSize == Right (Just mb80) @?+        "Wrong file size of put file after getting" -      step "High-level listing of incomplete multipart uploads"-      uploads <- (listIncompleteUploads bucket Nothing True) $$ sinkList-      -- Minio server behaviour changed to list no incomplete uploads,-      -- so the check below reflects this; this test is expected to-      -- fail on AWS S3.-      liftIO $ (length uploads) @?= 0+      step "Cleanup actions"+      removeObject bucket obj        step "cleanup"-      forM_ uploads $ \(UploadInfo _ uid _ _) ->-                        abortMultipartUpload bucket object uid+      removeObject bucket "big" -      step "High-level listIncompleteParts Test"-      let-        object = "newmpupload"-        mb15 = 15 * 1024 * 1024+      step "Prepare for removeIncompleteUpload"+      -- low-level multipart operation tests.+      let object = "newmpupload"+          kb5 = 5 * 1024 -      step "create a multipart upload"+      step "create new multipart upload"       uid <- newMultipartUpload bucket object []-      liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")+      liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id." -      step "put object parts 1..10"-      inputFile <- mkRandFile mb15-      h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode-      forM_ [1..10] $ \pnum ->-        putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb15+      randFile <- mkRandFile kb5 -      step "fetch list parts"-      incompleteParts <- (listIncompleteParts bucket object uid) $$ sinkList-      -- Minio server behaviour changed to list no incomplete uploads,-      -- so the check below reflects this; this test is expected to-      -- fail on AWS S3.-      liftIO $ (length incompleteParts) @?= 0+      step "upload 2 parts"+      forM_ [1,2] $ \partNum -> do+        h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode+        void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5 -      step "cleanup"-      abortMultipartUpload bucket object uid+      step "remove ongoing upload"+      removeIncompleteUpload bucket object+      uploads <- listIncompleteUploads bucket (Just object) False C.$$ sinkList+      liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully" +   , funTestWithBucket "copyObject related tests" $ \step bucket -> do       step "copyObjectSingle basic tests"       let object = "xxx"@@ -392,25 +365,25 @@        step "create new multipart upload"       uid <- newMultipartUpload bucket copyObj []-      liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")+      liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."        step "put object parts 1-3"-      let cps = def {cpSource = format "/{}/{}" [bucket, srcObj]}+      let cps' = def {cpSource = format "/{}/{}" [bucket, srcObj]}       parts <- forM [1..3] $ \p -> do-        (etag, _) <- copyObjectPart bucket copyObj cps{+        (etag', _) <- copyObjectPart bucket copyObj cps'{           cpSourceRange = Just ((p-1)*mb5, (p-1)*mb5 + (mb5 - 1))           } uid (fromIntegral p) []-        return $ (fromIntegral p, etag)+        return (fromIntegral p, etag')        step "complete multipart"       void $ completeMultipartUpload bucket copyObj uid parts        step "verify copied object size"-      (ObjectInfo _ _ _ s) <- headObject bucket copyObj+      (ObjectInfo _ _ _ s') <- headObject bucket copyObj -      liftIO $ (s == mb15) @? "Size failed to match"+      liftIO $ (s' == mb15) @? "Size failed to match" -      step $ "Cleanup actions"+      step "Cleanup actions"       removeObject bucket srcObj       removeObject bucket copyObj @@ -428,15 +401,14 @@         copyObject bucket cp def{cpSource = format "/{}/{}" [bucket, src]}        step "verify uploaded objects"-      uploadedSizes <- fmap (fmap oiSize) $ forM copyObjs (headObject bucket)+      uploadedSizes <- fmap oiSize <$> forM copyObjs (headObject bucket)        liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match" -      forM_ (concat [srcs, copyObjs]) (removeObject bucket)+      forM_ (srcs ++ copyObjs) (removeObject bucket)        step "copyObject with offset test "       let src = "XXX"-          copyObj = "XXXCopy"           size = 15 * 1024 * 1024        step "Prepare"@@ -454,4 +426,201 @@       liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"        forM_ [src, copyObj] (removeObject bucket)++  , presignedUrlFunTest+  , presignedPostPolicyFunTest   ]++basicTests :: TestTree+basicTests = funTestWithBucket "Basic tests" $ \step bucket -> do+      step "getService works and contains the test bucket."+      buckets <- getService+      unless (length (filter (== bucket) $ map biName buckets) == 1) $+        liftIO $+        assertFailure ("The bucket " ++ show bucket +++                       " was expected to exist.")++      step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."+      mbE <- MC.try $ makeBucket bucket Nothing+      case mbE of+        Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou+        _        -> return ()++      step "makeBucket with an invalid bucket name and check for appropriate exception."+      invalidMBE <- MC.try $ makeBucket "invalidBucketName" Nothing+      case invalidMBE of+        Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"+        _ -> return ()++      step "getLocation works"+      region <- getLocation bucket+      liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)++      step "singlepart putObject works"+      fPutObject bucket "lsb-release" "/etc/lsb-release"++      step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"+      fpE <- MC.try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release"+      case fpE of+        Left exn -> liftIO $ exn @?= NoSuchBucket+        _        -> return ()++      outFile <- mkRandFile 0+      step "simple fGetObject works"+      fGetObject bucket "lsb-release" outFile++      step "fGetObject a non-existent object and check for NoSuchKey exception"+      resE <- MC.try $ fGetObject bucket "noSuchKey" outFile+      case resE of+        Left exn -> liftIO $ exn @?= NoSuchKey+        _        -> return ()+++      step "create new multipart upload works"+      uid <- newMultipartUpload bucket "newmpupload" []+      liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")++      step "abort a new multipart upload works"+      abortMultipartUpload bucket "newmpupload" uid++      step "delete object works"+      deleteObject bucket "lsb-release"++      step "statObject test"+      let object = "sample"+      step "create an object"+      inputFile <- mkRandFile 0+      fPutObject bucket object inputFile++      step "get metadata of the object"+      res <- statObject bucket object+      liftIO $ (oiSize res) @?= 0++      step "delete object"+      deleteObject bucket object++presignedUrlFunTest :: TestTree+presignedUrlFunTest = funTestWithBucket "presigned Url tests" $+  \step bucket -> do+    let obj = "mydir/myput"+        obj2 = "mydir1/myfile1"++    -- manager for http requests+    mgr <- liftIO $ NC.newManager NC.tlsManagerSettings++    step "PUT object presigned URL - makePresignedUrl"+    putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)+           (Just obj) (Just "us-east-1") [] []++    let size1 = 1000 :: Int64+    inputFile <- mkRandFile size1++    -- attempt to upload using the presigned URL+    putResp <- putR size1 inputFile mgr putUrl+    liftIO $ (NC.responseStatus putResp == HT.status200) @?+      "presigned PUT failed"++    step "GET object presigned URL - makePresignedUrl"+    getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)+           (Just obj) (Just "us-east-1") [] []++    getResp <- getR mgr getUrl+    liftIO $ (NC.responseStatus getResp == HT.status200) @?+      "presigned GET failed"++    -- read content from file to compare with response above+    bs <- CB.sourceFile inputFile $$ CB.sinkLbs+    liftIO $ (bs == NC.responseBody getResp) @?+      "presigned put and get got mismatched data"++    step "PUT object presigned - presignedPutObjectURL"+    putUrl2 <- presignedPutObjectUrl bucket obj2 3600 []++    let size2 = 1200+    testFile <- mkRandFile size2++    putResp2 <- putR size2 testFile mgr putUrl2+    liftIO $ (NC.responseStatus putResp2 == HT.status200) @?+      "presigned PUT failed (presignedPutObjectUrl)"++    step "HEAD object presigned URL - presignedHeadObjectUrl"+    headUrl <- presignedHeadObjectUrl bucket obj2 3600 []++    headResp <- do req <- NC.parseRequest $ toS headUrl+                   NC.httpLbs (req {NC.method = HT.methodHead}) mgr+    liftIO $ (NC.responseStatus headResp == HT.status200) @?+      "presigned HEAD failed (presignedHeadObjectUrl)"++    -- check that header info is accurate+    let h = Map.fromList $ NC.responseHeaders headResp+        cLen = Map.findWithDefault "0" HT.hContentLength h+    liftIO $ (cLen == show size2) @? "Head req returned bad content length"++    step "GET object presigned URL - presignedGetObjectUrl"+    getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []++    getResp2 <- getR mgr getUrl2+    liftIO $ (NC.responseStatus getResp2 == HT.status200) @?+      "presigned GET failed (presignedGetObjectUrl)"++    -- read content from file to compare with response above+    bs2 <- CB.sourceFile testFile $$ CB.sinkLbs+    liftIO $ (bs2 == NC.responseBody getResp2) @?+      "presigned put and get got mismatched data (presigned*Url)"+++    mapM_ (removeObject bucket) [obj, obj2]+  where+    putR size filePath mgr url = do+      req <- NC.parseRequest $ toS url+      let req' = req { NC.method = HT.methodPut+                     , NC.requestBody = NC.requestBodySource size $+                                        CB.sourceFile filePath}+      NC.httpLbs req' mgr++    getR mgr url = do+      req <- NC.parseRequest $ toS url+      NC.httpLbs req mgr++presignedPostPolicyFunTest :: TestTree+presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $+  \step bucket -> do++    step "presignedPostPolicy basic test"+    now <- liftIO $ Time.getCurrentTime++    let key = "presignedPostPolicyTest/myfile"+        policyConds = [ ppCondBucket bucket+                      , ppCondKey key+                      , ppCondContentLengthRange 1 1000+                      , ppCondContentType "application/octet-stream"+                      , ppCondSuccessActionStatus 200+                      ]++        expirationTime = Time.addUTCTime 3600 now+        postPolicyE = newPostPolicy expirationTime policyConds++        size = 1000 :: Int64++    inputFile <- mkRandFile size++    case postPolicyE of+      Left err -> liftIO $ assertFailure $ show err+      Right postPolicy -> do+        (url, formData) <- presignedPostPolicy postPolicy+        -- liftIO (print url) >> liftIO (print formData)+        result <- liftIO $ postForm url formData inputFile+        liftIO $ (NC.responseStatus result == HT.status200) @?+          "presigned POST failed"++    mapM_ (removeObject bucket) [key]+    where++      postForm url formData inputFile = do+        req <- NC.parseRequest $ toS url+        let parts = map (\(x, y) -> Form.partBS x y) $+                    Map.toList formData+            parts' = parts ++ [Form.partFile "file" inputFile]+        req' <- Form.formDataBody parts' req+        mgr <- NC.newManager NC.tlsManagerSettings+        NC.httpLbs req' mgr
test/Network/Minio/XmlGenerator/Test.hs view
@@ -18,17 +18,22 @@   ( xmlGeneratorTests   ) where -import Test.Tasty-import Test.Tasty.HUnit+import           Test.Tasty+import           Test.Tasty.HUnit -import Lib.Prelude+import           Lib.Prelude -import Network.Minio.XmlGenerator+import           Data.Default               (def) +import           Network.Minio.Data+import           Network.Minio.XmlGenerator+import           Network.Minio.XmlParser    (parseNotification)+ xmlGeneratorTests :: TestTree xmlGeneratorTests = testGroup "XML Generator Tests"   [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig   , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest+  , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest   ]  testMkCreateBucketConfig :: Assertion@@ -52,3 +57,39 @@     \<PartNumber>1</PartNumber><ETag>abc</ETag>\     \</Part>\     \</CompleteMultipartUpload>"++testMkPutNotificationRequest :: Assertion+testMkPutNotificationRequest =+  forM_ cases $ \val -> do+    let result = toS $ mkPutNotificationRequest val+    ntf <- runExceptT $ parseNotification result+    either (\_ -> assertFailure "XML Parse Error!")+      (@?= val) ntf+  where+    cases = [ Notification []+              [ NotificationConfig+                "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"+                "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"+                [ReducedRedundancyLostObject, ObjectCreated] def+              ]+              []+            , Notification+              [ NotificationConfig+                "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"+                [ObjectCreatedPut]+                (Filter $ FilterKey $ FilterRules+                  [ FilterRule "prefix" "images/"+                  , FilterRule "suffix" ".jpg"])+              , NotificationConfig+                "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"+                [ObjectCreated] def+              ]+              [ NotificationConfig+                "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"+                [ReducedRedundancyLostObject] def+              ]+              [ NotificationConfig+                "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"+                [ObjectCreated] def+              ]+            ]
test/Network/Minio/XmlParser/Test.hs view
@@ -19,13 +19,15 @@     xmlParserTests   ) where -import qualified Control.Monad.Catch as MC-import           Data.Time (fromGregorian)+import qualified Control.Monad.Catch     as MC+import           Data.Time               (fromGregorian) import           Test.Tasty import           Test.Tasty.HUnit  import           Lib.Prelude +import           Data.Default            (def)+ import           Network.Minio.Data import           Network.Minio.Errors import           Network.Minio.XmlParser@@ -35,10 +37,12 @@   [ testCase "Test parseLocation" testParseLocation   , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload   , testCase "Test parseListObjectsResponse" testParseListObjectsResult+  , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result   , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads   , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse   , testCase "Test parseListPartsResponse" testParseListPartsResponse   , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse+  , testCase "Test parseNotification" testParseNotification   ]  tryValidationErr :: (MC.MonadCatch m) => m a -> m (Either MErrV a)@@ -48,7 +52,7 @@ assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e  eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion-eitherValidationErr (Left e) _ = assertValidtionErr e+eitherValidationErr (Left e) _  = assertValidtionErr e eitherValidationErr (Right a) f = f a  testParseLocation :: Assertion@@ -108,9 +112,10 @@               \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\               \<Name>bucket</Name>\               \<Prefix/>\-              \<KeyCount>205</KeyCount>\+              \<NextContinuationToken>opaque</NextContinuationToken>\+              \<KeyCount>1000</KeyCount>\               \<MaxKeys>1000</MaxKeys>\-              \<IsTruncated>false</IsTruncated>\+              \<IsTruncated>true</IsTruncated>\               \<Contents>\               \<Key>my-image.jpg</Key>\               \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\@@ -120,13 +125,40 @@               \</Contents>\               \</ListBucketResult>" -    expectedListResult = ListObjectsResult False Nothing [object1] []+    expectedListResult = ListObjectsResult True (Just "opaque") [object1] []     object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234     modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12    parsedListObjectsResult <- tryValidationErr $ parseListObjectsResponse xmldata   eitherValidationErr parsedListObjectsResult (@?= expectedListResult) +testParseListObjectsV1Result :: Assertion+testParseListObjectsV1Result = do+  let+    xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+              \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+              \<Name>bucket</Name>\+              \<Prefix/>\+              \<NextMarker>my-image1.jpg</NextMarker>\+              \<KeyCount>1000</KeyCount>\+              \<MaxKeys>1000</MaxKeys>\+              \<IsTruncated>true</IsTruncated>\+              \<Contents>\+              \<Key>my-image.jpg</Key>\+              \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\+              \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\+              \<Size>434234</Size>\+              \<StorageClass>STANDARD</StorageClass>\+              \</Contents>\+              \</ListBucketResult>"++    expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []+    object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234+    modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12++  parsedListObjectsV1Result <- tryValidationErr $ parseListObjectsV1Response xmldata+  eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)+ testParseListIncompleteUploads :: Assertion testParseListIncompleteUploads = do   let@@ -250,3 +282,77 @@   forM_ cases $ \(xmldata, (etag, modTime)) -> do     parseResult <- runExceptT $ parseCopyObjectResponse xmldata     eitherValidationErr parseResult (@?= (etag, modTime))++testParseNotification :: Assertion+testParseNotification = do+  let+    cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+\  <TopicConfiguration>\+\    <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\+\    <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\+\    <Event>s3:ReducedRedundancyLostObject</Event>\+\    <Event>s3:ObjectCreated:*</Event>\+\  </TopicConfiguration>\+\</NotificationConfiguration>",+               Notification []+                [ NotificationConfig+                  "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"+                  "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"+                  [ReducedRedundancyLostObject, ObjectCreated] def+                ]+                [])+            , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+\  <CloudFunctionConfiguration>\+\    <Id>ObjectCreatedEvents</Id>\+\    <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\+\    <Event>s3:ObjectCreated:*</Event>\+\  </CloudFunctionConfiguration>\+\  <QueueConfiguration>\+\      <Id>1</Id>\+\      <Filter>\+\          <S3Key>\+\              <FilterRule>\+\                  <Name>prefix</Name>\+\                  <Value>images/</Value>\+\              </FilterRule>\+\              <FilterRule>\+\                  <Name>suffix</Name>\+\                  <Value>.jpg</Value>\+\              </FilterRule>\+\          </S3Key>\+\     </Filter>\+\     <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\+\     <Event>s3:ObjectCreated:Put</Event>\+\  </QueueConfiguration>\+\  <TopicConfiguration>\+\    <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\+\    <Event>s3:ReducedRedundancyLostObject</Event>\+\  </TopicConfiguration>\+\  <QueueConfiguration>\+\    <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\+\    <Event>s3:ObjectCreated:*</Event>\+\  </QueueConfiguration>)\+\</NotificationConfiguration>",+               Notification [ NotificationConfig+                              "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"+                              [ObjectCreatedPut]+                              (Filter $ FilterKey $ FilterRules+                               [FilterRule "prefix" "images/",+                                FilterRule "suffix" ".jpg"])+                            , NotificationConfig+                              "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"+                              [ObjectCreated] def+                            ]+                            [ NotificationConfig+                              "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"+                              [ReducedRedundancyLostObject] def+                            ]+                            [ NotificationConfig+                              "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"+                              [ObjectCreated] def+                            ])+            ]++  forM_ cases $ \(xmldata, val) -> do+    result <- runExceptT $ parseNotification xmldata+    eitherValidationErr result (@?= val)