minio-hs 1.5.2 → 1.5.3
raw patch · 57 files changed
+5977/−5155 lines, 57 filesdep ~protoludedep ~unliftiodep ~unliftio-coresetup-changed
Dependency ranges changed: protolude, unliftio, unliftio-core
Files
- CHANGELOG.md +7/−0
- Setup.hs +1/−0
- examples/BucketExists.hs +4/−7
- examples/CopyObject.hs +12/−14
- examples/FileUploader.hs +24/−24
- examples/GetConfig.hs +5/−5
- examples/GetObject.hs +5/−10
- examples/HeadObject.hs +8/−11
- examples/Heal.hs +11/−7
- examples/ListBuckets.hs +3/−5
- examples/ListIncompleteUploads.hs +17/−19
- examples/ListObjects.hs +13/−15
- examples/MakeBucket.hs +6/−9
- examples/PresignedGetObject.hs +30/−27
- examples/PresignedPostPolicy.hs +35/−34
- examples/PresignedPutObject.hs +18/−20
- examples/PutObject.hs +11/−14
- examples/RemoveBucket.hs +3/−8
- examples/RemoveIncompleteUpload.hs +7/−10
- examples/RemoveObject.hs +7/−8
- examples/SelectObject.hs +21/−23
- examples/ServerInfo.hs +6/−6
- examples/ServiceSendRestart.hs +6/−6
- examples/ServiceSendStop.hs +6/−6
- examples/ServiceStatus.hs +6/−6
- examples/SetConfig.hs +3/−4
- minio-hs.cabal +4/−4
- src/Lib/Prelude.hs +37/−11
- src/Network/Minio.hs +208/−191
- src/Network/Minio/API.hs +171/−144
- src/Network/Minio/APICommon.hs +27/−23
- src/Network/Minio/AdminAPI.hs +461/−375
- src/Network/Minio/CopyObject.hs +48/−32
- src/Network/Minio/Data.hs +1110/−1019
- src/Network/Minio/Data/ByteString.hs +17/−17
- src/Network/Minio/Data/Crypto.hs +24/−24
- src/Network/Minio/Data/Time.hs +10/−12
- src/Network/Minio/Errors.hs +48/−47
- src/Network/Minio/JsonParser.hs +23/−16
- src/Network/Minio/ListOps.hs +63/−38
- src/Network/Minio/PresignedOperations.hs +200/−144
- src/Network/Minio/PutObject.hs +82/−66
- src/Network/Minio/S3API.hs +452/−312
- src/Network/Minio/SelectAPI.hs +213/−218
- src/Network/Minio/Sign/V4.hs +311/−293
- src/Network/Minio/Utils.hs +124/−95
- src/Network/Minio/XmlGenerator.hs +158/−93
- src/Network/Minio/XmlParser.hs +106/−114
- stack.yaml +4/−2
- test/LiveServer.hs +1181/−1008
- test/Network/Minio/API/Test.hs +72/−60
- test/Network/Minio/JsonParser/Test.hs +26/−27
- test/Network/Minio/TestHelpers.hs +9/−8
- test/Network/Minio/Utils/Test.hs +16/−17
- test/Network/Minio/XmlGenerator/Test.hs +124/−94
- test/Network/Minio/XmlParser/Test.hs +295/−274
- test/Spec.hs +78/−79
CHANGELOG.md view
@@ -1,6 +1,13 @@ Changelog ========== +## Version 1.5.3++* Fix windows build+* Fix support for Yandex Storage (#147)+* Fix for HEAD requests to S3/Minio (#155)+* Bump up some dependencies, new code formatting, Github CI, example fixes and other minor improvements.+ ## Version 1.5.2 * Fix region `us-west-2` for AWS S3 (#139)
Setup.hs view
@@ -15,4 +15,5 @@ -- import Distribution.Simple+ main = defaultMain
examples/BucketExists.hs view
@@ -16,20 +16,17 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Control.Monad.IO.Class (liftIO)-import Prelude+import Control.Monad.IO.Class (liftIO)+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do let bucket = "missingbucket"@@ -39,5 +36,5 @@ liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket case res1 of- Left e -> putStrLn $ "bucketExists failed." ++ show e+ Left e -> putStrLn $ "bucketExists failed." ++ show e Right () -> return ()
examples/CopyObject.hs view
@@ -16,42 +16,40 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import UnliftIO.Exception (catch, throwIO)+import Network.Minio+import UnliftIO.Exception (catch, throwIO) -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "test"+ let bucket = "test" object = "obj" objectCopy = "obj-copy" localFile = "/etc/lsb-release" res1 <- runMinio minioPlayCI $ do -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.- catch (makeBucket bucket Nothing) (- \e -> case e of- BucketAlreadyOwnedByYou -> return ()- _ -> throwIO e+ catch+ (makeBucket bucket Nothing)+ ( \e -> case e of+ BucketAlreadyOwnedByYou -> return ()+ _ -> throwIO e ) -- 2. Upload a file to bucket/object. fPutObject bucket object localFile defaultPutObjectOptions -- 3. Copy bucket/object to bucket/objectCopy.- copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}- defaultSourceInfo { srcBucket = bucket , srcObject = object }+ copyObject+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}+ defaultSourceInfo {srcBucket = bucket, srcObject = object} case res1 of- Left e -> putStrLn $ "copyObject failed." ++ show e+ Left e -> putStrLn $ "copyObject failed." ++ show e Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs view
@@ -16,40 +16,40 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -----{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}-import Network.Minio -import Data.Monoid ((<>))-import Data.Text (pack)-import Options.Applicative-import System.FilePath.Posix-import UnliftIO (throwIO, try)--import Prelude+import Data.Monoid ((<>))+import Data.Text (pack)+import Network.Minio+import Options.Applicative+import System.FilePath.Posix+import UnliftIO (throwIO, try)+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo--- -- optparse-applicative package based command-line parsing. fileNameArgs :: Parser FilePath-fileNameArgs = strArgument- (metavar "FILENAME"- <> help "Name of file to upload to AWS S3 or a MinIO server")+fileNameArgs =+ strArgument+ ( metavar "FILENAME"+ <> help "Name of file to upload to AWS S3 or a MinIO server"+ ) cmdParser :: ParserInfo FilePath-cmdParser = info- (helper <*> fileNameArgs)- (fullDesc- <> progDesc "FileUploader"- <> header- "FileUploader - a simple file-uploader program using minio-hs")+cmdParser =+ info+ (helper <*> fileNameArgs)+ ( fullDesc+ <> progDesc "FileUploader"+ <> header+ "FileUploader - a simple file-uploader program using minio-hs"+ ) main :: IO () main = do@@ -64,12 +64,12 @@ bErr <- try $ makeBucket bucket Nothing case bErr of Left BucketAlreadyOwnedByYou -> return ()- Left e -> throwIO e- Right _ -> return ()+ Left e -> throwIO e+ Right _ -> return () -- Upload filepath to bucket; object is derived from filepath. fPutObject bucket object filepath defaultPutObjectOptions case res of- Left e -> putStrLn $ "file upload failed due to " ++ (show e)+ Left e -> putStrLn $ "file upload failed due to " ++ (show e) Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs view
@@ -16,15 +16,15 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.AdminAPI -import Prelude+import Network.Minio+import Network.Minio.AdminAPI+import Prelude main :: IO () main = do- res <- runMinio minioPlayCI $+ res <-+ runMinio minioPlayCI $ getConfig print res
examples/GetObject.hs view
@@ -16,31 +16,26 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import qualified Data.Conduit as C+import qualified Data.Conduit as C import qualified Data.Conduit.Binary as CB--import Prelude+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "my-bucket"+ let bucket = "my-bucket" object = "my-object" res <- runMinio minioPlayCI $ do src <- getObject bucket object defaultGetObjectOptions C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object" case res of- Left e -> putStrLn $ "getObject failed." ++ (show e)+ Left e -> putStrLn $ "getObject failed." ++ (show e) Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs view
@@ -16,28 +16,25 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.S3API -import Prelude+import Network.Minio+import Network.Minio.S3API+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "test"+ let bucket = "test" object = "passwd"- res <- runMinio minioPlayCI $- headObject bucket object []+ res <-+ runMinio minioPlayCI $+ headObject bucket object [] case res of- Left e -> putStrLn $ "headObject failed." ++ show e+ Left e -> putStrLn $ "headObject failed." ++ show e Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hs view
@@ -16,19 +16,23 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.AdminAPI -import Prelude+import Network.Minio+import Network.Minio.AdminAPI+import Prelude main :: IO () main = do res <- runMinio minioPlayCI $ do- hsr <- startHeal Nothing Nothing HealOpts { hoRecursive = True- , hoDryRun = False- }+ hsr <-+ startHeal+ Nothing+ Nothing+ HealOpts+ { hoRecursive = True,+ hoDryRun = False+ } getHealStatus Nothing Nothing (hsrClientToken hsr) print res
examples/ListBuckets.hs view
@@ -16,19 +16,17 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Control.Monad.IO.Class (liftIO)-import Prelude+import Control.Monad.IO.Class (liftIO)+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo--- -- This example list buckets that belongs to the user and returns -- region of the first bucket returned.
examples/ListIncompleteUploads.hs view
@@ -16,38 +16,36 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Conduit-import Prelude+import Conduit+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "test"+ let bucket = "test" -- Performs a recursive listing of incomplete uploads under bucket "test" -- on a local minio server.- res <- runMinio minioPlayCI $- runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))+ res <-+ runMinio minioPlayCI+ $ runConduit+ $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v)) print res - {-- Following is the output of the above program on a local MinIO server.+{-+ Following is the output of the above program on a local MinIO server. - Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"- , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"- , uiInitTime = 2017-03-01 10:16:25.698 UTC- , uiSize = 17731794- }- ]- -}+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"+ , uiInitTime = 2017-03-01 10:16:25.698 UTC+ , uiSize = 17731794+ }+ ]+-}
examples/ListObjects.hs view
@@ -16,33 +16,31 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Conduit-import Prelude-+import Conduit+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "test"+ let bucket = "test" -- Performs a recursive listing of all objects under bucket "test" -- on play.min.io.- res <- runMinio minioPlayCI $- runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))+ res <-+ runMinio minioPlayCI+ $ runConduit+ $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v)) print res- {-- Following is the output of the above program on a local MinIO server. - Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]- -}+{-+ Following is the output of the above program on a local MinIO server.++ Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]+-}
examples/MakeBucket.hs view
@@ -16,24 +16,21 @@ -- See the License for the specific language governing permissions and -- limitations under the License. ---- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Prelude+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do let bucket = "my-bucket"- res <- runMinio minioPlayCI $- -- N B the region provided for makeBucket is optional.- makeBucket bucket (Just "us-east-1")+ res <-+ runMinio minioPlayCI $+ -- N B the region provided for makeBucket is optional.+ makeBucket bucket (Just "us-east-1") print res
examples/PresignedGetObject.hs view
@@ -16,34 +16,32 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Control.Monad.IO.Class (liftIO)-import qualified Data.ByteString.Char8 as B-import Data.CaseInsensitive (original)+import Control.Monad.IO.Class (liftIO)+import qualified Data.ByteString.Char8 as B+import Data.CaseInsensitive (original) import qualified Data.Conduit.Combinators as CC-import qualified Data.Text.Encoding as E+import qualified Data.Text.Encoding as E+import Network.Minio -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "my-bucket"- object = "my-object"- kb15 = 15*1024-- -- Set query parameter to modify content disposition response- -- header- queryParam = [("response-content-disposition",- Just "attachment; filename=\"your-filename.txt\"")]+ let bucket = "my-bucket"+ object = "my-object"+ kb15 = 15 * 1024+ -- Set query parameter to modify content disposition response+ -- header+ queryParam =+ [ ( "response-content-disposition",+ Just "attachment; filename=\"your-filename.txt\""+ )+ ] res <- runMinio minioPlayCI $ do liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."@@ -61,23 +59,28 @@ -- Generate a URL with 7 days expiry time - note that the headers -- used above must be added to the request with the signed URL -- generated.- url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)- queryParam headers+ url <-+ presignedGetObjectUrl+ "my-bucket"+ "my-object"+ (7 * 24 * 3600)+ queryParam+ headers return (headers, etag, url) case res of Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e Right (headers, _, url) -> do- -- We generate a curl command to demonstrate usage of the signed -- URL.- let- hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]- curlCmd = B.intercalate " " $- ["curl --fail"] ++ map hdrOpt headers ++- ["-o /tmp/myfile", B.concat ["'", url, "'"]]+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]+ curlCmd =+ B.intercalate " " $+ ["curl --fail"] ++ map hdrOpt headers+ ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]] - putStrLn $ "The following curl command would use the presigned " ++- "URL to fetch the object and write it to \"/tmp/myfile\":"+ putStrLn $+ "The following curl command would use the presigned "+ ++ "URL to fetch the object and write it to \"/tmp/myfile\":" B.putStrLn curlCmd
examples/PresignedPostPolicy.hs view
@@ -16,47 +16,43 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import qualified Data.ByteString as B+import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as Char8-import qualified Data.HashMap.Strict as H-import qualified Data.Text.Encoding as Enc-import qualified Data.Time as Time+import qualified Data.HashMap.Strict as H+import qualified Data.Text.Encoding as Enc+import qualified Data.Time as Time+import Network.Minio -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do now <- Time.getCurrentTime- let- bucket = "my-bucket"- object = "photos/my-object"-- -- set an expiration time of 10 days- expireTime = Time.addUTCTime (3600 * 24 * 10) now-- -- create a policy with expiration time and conditions - since the- -- conditions are validated, newPostPolicy returns an Either value- policyE = newPostPolicy expireTime- [ -- set the object name condition- ppCondKey object- -- set the bucket name condition- , ppCondBucket bucket- -- set the size range of object as 1B to 10MiB- , ppCondContentLengthRange 1 (10*1024*1024)- -- set content type as jpg image- , ppCondContentType "image/jpeg"- -- on success set the server response code to 200- , ppCondSuccessActionStatus 200- ]+ let bucket = "my-bucket"+ object = "photos/my-object"+ -- set an expiration time of 10 days+ expireTime = Time.addUTCTime (3600 * 24 * 10) now+ -- create a policy with expiration time and conditions - since the+ -- conditions are validated, newPostPolicy returns an Either value+ policyE =+ newPostPolicy+ expireTime+ [ -- set the object name condition+ ppCondKey object,+ -- set the bucket name condition+ ppCondBucket bucket,+ -- set the size range of object as 1B to 10MiB+ ppCondContentLengthRange 1 (10 * 1024 * 1024),+ -- set content type as jpg image+ ppCondContentType "image/jpeg",+ -- on success set the server response code to 200+ ppCondSuccessActionStatus 200+ ] case policyE of Left err -> putStrLn $ show err@@ -66,11 +62,16 @@ -- a curl command is output to demonstrate using the generated -- URL and form-data- let- formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",- "'", v, "'"]- formOptions = B.intercalate " " $ map formFn $ H.toList formData-+ let formFn (k, v) =+ B.concat+ [ "-F ",+ Enc.encodeUtf8 k,+ "=",+ "'",+ v,+ "'"+ ]+ formOptions = B.intercalate " " $ map formFn $ H.toList formData return $ B.intercalate " " $ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
examples/PresignedPutObject.hs view
@@ -16,44 +16,42 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio import qualified Data.ByteString.Char8 as B-import Data.CaseInsensitive (original)+import Data.CaseInsensitive (original)+import Network.Minio -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- -- Use headers to set user-metadata - note that this header will- -- need to be set when the URL is used to make an upload.- headers = [("x-amz-meta-url-creator",- "minio-hs-presigned-put-example")]+ let -- Use headers to set user-metadata - note that this header will+ -- need to be set when the URL is used to make an upload.+ headers =+ [ ( "x-amz-meta-url-creator",+ "minio-hs-presigned-put-example"+ )+ ] res <- runMinio minioPlayCI $ do- -- generate a URL with 7 days expiry time- presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers+ presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers case res of Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e Right url -> do- -- We generate a curl command to demonstrate usage of the signed -- URL.- let- hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]- curlCmd = B.intercalate " " $- ["curl "] ++ map hdrOpt headers ++- ["-T /tmp/myfile", B.concat ["'", url, "'"]]+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]+ curlCmd =+ B.intercalate " " $+ ["curl "] ++ map hdrOpt headers+ ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]] - putStrLn $ "The following curl command would use the presigned " ++- "URL to upload the file at \"/tmp/myfile\":"+ putStrLn $+ "The following curl command would use the presigned "+ ++ "URL to upload the file at \"/tmp/myfile\":" B.putStrLn curlCmd
examples/PutObject.hs view
@@ -16,39 +16,36 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio import qualified Data.Conduit.Combinators as CC--import Prelude+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "test"+ let bucket = "test" object = "obj" localFile = "/etc/lsb-release" kb15 = 15 * 1024 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.- res1 <- runMinio minioPlayCI $- putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions+ res1 <-+ runMinio minioPlayCI $+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions case res1 of- Left e -> putStrLn $ "putObject failed." ++ show e+ Left e -> putStrLn $ "putObject failed." ++ show e Right () -> putStrLn "putObject succeeded." -- Eg 2. Upload a file using fPutObject with default options.- res2 <- runMinio minioPlayCI $- fPutObject bucket object localFile defaultPutObjectOptions+ res2 <-+ runMinio minioPlayCI $+ fPutObject bucket object localFile defaultPutObjectOptions case res2 of- Left e -> putStrLn $ "fPutObject failed." ++ show e+ Left e -> putStrLn $ "fPutObject failed." ++ show e Right () -> putStrLn "fPutObject succeeded."
examples/RemoveBucket.hs view
@@ -16,23 +16,18 @@ -- See the License for the specific language governing permissions and -- limitations under the License. ---- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Prelude+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "my-bucket"+ let bucket = "my-bucket" res <- runMinio minioPlayCI $ removeBucket bucket print res
examples/RemoveIncompleteUpload.hs view
@@ -16,27 +16,24 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import Prelude+import Network.Minio+import Prelude -- | The following example uses minio's play server at -- https://play.min.io. The endpoint and associated -- credentials are provided via the libary constant, -- -- > minioPlayCI :: ConnectInfo---- main :: IO () main = do- let- bucket = "mybucket"- object = "myobject"+ let bucket = "mybucket"+ object = "myobject" - res <- runMinio minioPlayCI $- removeIncompleteUpload bucket object+ res <-+ runMinio minioPlayCI $+ removeIncompleteUpload bucket object case res of Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs view
@@ -16,20 +16,19 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-} -import Network.Minio-import Prelude+import Network.Minio+import Prelude main :: IO () main = do- let- bucket = "mybucket"- object = "myobject"+ let bucket = "mybucket"+ object = "myobject" - res <- runMinio minioPlayCI $- removeObject bucket object+ res <-+ runMinio minioPlayCI $+ removeObject bucket object case res of Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hs view
@@ -16,34 +16,32 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio -import qualified Conduit as C-import Control.Monad (when)--import Prelude+import qualified Conduit as C+import Control.Monad (when)+import Network.Minio+import Prelude main :: IO () main = do- let bucket = "selectbucket"- object = "1.csv"- content = "Name,Place,Temperature\n"- <> "James,San Jose,76\n"- <> "Alicia,San Leandro,88\n"- <> "Mark,San Carlos,90\n"-- res <- runMinio minioPlayCI $ do+ let bucket = "selectbucket"+ object = "1.csv"+ content =+ "Name,Place,Temperature\n"+ <> "James,San Jose,76\n"+ <> "Alicia,San Leandro,88\n"+ <> "Mark,San Carlos,90\n" - exists <- bucketExists bucket- when (not exists) $- makeBucket bucket Nothing+ res <- runMinio minioPlayCI $ do+ exists <- bucketExists bucket+ when (not exists) $+ makeBucket bucket Nothing - C.liftIO $ putStrLn "Uploading csv object"- putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions+ C.liftIO $ putStrLn "Uploading csv object"+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions - let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput- res <- selectObjectContent bucket object sr- C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC- print res+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput+ res <- selectObjectContent bucket object sr+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC+ print res
examples/ServerInfo.hs view
@@ -16,15 +16,15 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.AdminAPI -import Prelude+import Network.Minio+import Network.Minio.AdminAPI+import Prelude main :: IO () main = do- res <- runMinio minioPlayCI $- getServerInfo+ res <-+ runMinio minioPlayCI $+ getServerInfo print res
examples/ServiceSendRestart.hs view
@@ -16,15 +16,15 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.AdminAPI -import Prelude+import Network.Minio+import Network.Minio.AdminAPI+import Prelude main :: IO () main = do- res <- runMinio minioPlayCI $- serviceSendAction ServiceActionRestart+ res <-+ runMinio minioPlayCI $+ serviceSendAction ServiceActionRestart print res
examples/ServiceSendStop.hs view
@@ -16,15 +16,15 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.AdminAPI -import Prelude+import Network.Minio+import Network.Minio.AdminAPI+import Prelude main :: IO () main = do- res <- runMinio minioPlayCI $- serviceSendAction ServiceActionStop+ res <-+ runMinio minioPlayCI $+ serviceSendAction ServiceActionStop print res
examples/ServiceStatus.hs view
@@ -16,15 +16,15 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.AdminAPI -import Prelude+import Network.Minio+import Network.Minio.AdminAPI+import Prelude main :: IO () main = do- res <- runMinio minioPlayCI $- serviceStatus+ res <-+ runMinio minioPlayCI $+ serviceStatus print res
examples/SetConfig.hs view
@@ -16,12 +16,11 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE OverloadedStrings #-}-import Network.Minio-import Network.Minio.AdminAPI -import Prelude+import Network.Minio+import Network.Minio.AdminAPI+import Prelude main :: IO () main = do
minio-hs.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: minio-hs-version: 1.5.2+version: 1.5.3 synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud storage. description: The MinIO Haskell client library provides simple APIs to@@ -56,7 +56,7 @@ , Network.Minio.XmlParser , Network.Minio.JsonParser build-depends: base >= 4.7 && < 5- , protolude >= 0.2 && < 0.3+ , protolude >= 0.3 && < 0.4 , aeson >= 1.2 , base64-bytestring >= 1.0 , binary >= 0.8.5.0@@ -83,8 +83,8 @@ , text >= 1.2 , time >= 1.8 , transformers >= 0.5- , unliftio >= 0.2- , unliftio-core >= 0.1+ , unliftio >= 0.2 && < 0.3+ , unliftio-core >= 0.2 && < 0.3 , unordered-containers >= 0.2 , xml-conduit >= 1.8
src/Lib/Prelude.hs view
@@ -15,19 +15,45 @@ -- module Lib.Prelude- ( module Exports- , both- ) where--import Protolude as Exports hiding (catch, catches,- throwIO, try)+ ( module Exports,+ both,+ showBS,+ toStrictBS,+ fromStrictBS,+ )+where -import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)-import Data.Time as Exports (UTCTime (..),- diffUTCTime)-import UnliftIO as Exports (catch, catches, throwIO,- try)+import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)+import qualified Data.ByteString.Lazy as LB+import Data.Time as Exports+ ( UTCTime (..),+ diffUTCTime,+ )+import Protolude as Exports hiding+ ( Handler,+ catch,+ catches,+ throwIO,+ try,+ yield,+ )+import UnliftIO as Exports+ ( Handler,+ catch,+ catches,+ throwIO,+ try,+ ) -- | Apply a function on both elements of a pair both :: (a -> b) -> (a, a) -> (b, b) both f (a, b) = (f a, f b)++showBS :: Show a => a -> ByteString+showBS a = toUtf8 (show a :: Text)++toStrictBS :: LByteString -> ByteString+toStrictBS = LB.toStrict++fromStrictBS :: ByteString -> LByteString+fromStrictBS = LB.fromStrict
src/Network/Minio.hs view
@@ -22,218 +22,217 @@ -- -- Types and functions to conveniently access S3 compatible object -- storage servers like MinIO.- module Network.Minio- (- -- * Credentials- Credentials (..)+ ( -- * Credentials+ Credentials (..), - -- ** Credential providers- -- | Run actions that retrieve 'Credentials' from the environment or- -- files or other custom sources.- , Provider- , fromAWSConfigFile- , fromAWSEnv- , fromMinioEnv- , findFirst+ -- ** Credential providers - -- * Connecting to object storage- , ConnectInfo- , setRegion- , setCreds- , setCredsFrom- , isConnectInfoSecure- , disableTLSCertValidation- , MinioConn- , mkMinioConn+ -- | Run actions that retrieve 'Credentials' from the environment or+ -- files or other custom sources.+ Provider,+ fromAWSConfigFile,+ fromAWSEnv,+ fromMinioEnv,+ findFirst, - -- ** Connection helpers- -- | These are helpers to construct 'ConnectInfo' values for common- -- cases.- , minioPlayCI- , awsCI- , gcsCI+ -- * Connecting to object storage+ ConnectInfo,+ setRegion,+ setCreds,+ setCredsFrom,+ isConnectInfoSecure,+ disableTLSCertValidation,+ MinioConn,+ mkMinioConn, - -- * 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- , runMinioWith- , runMinio- , runMinioResWith- , runMinioRes+ -- ** Connection helpers - -- * Bucket Operations+ -- | These are helpers to construct 'ConnectInfo' values for common+ -- cases.+ minioPlayCI,+ awsCI,+ gcsCI, - -- ** Creation, removal and querying- , Bucket- , makeBucket- , removeBucket- , bucketExists- , Region- , getLocation+ -- * Minio Monad+ ---------------- - -- ** Listing buckets- , BucketInfo(..)- , listBuckets+ -- | 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,+ runMinioWith,+ runMinio,+ runMinioResWith,+ runMinioRes, - -- ** Listing objects- , listObjects- , listObjectsV1- , ListItem(..)+ -- * Bucket Operations - , ObjectInfo- , oiObject- , oiModTime- , oiETag- , oiSize- , oiUserMetadata- , oiMetadata+ -- ** Creation, removal and querying+ Bucket,+ makeBucket,+ removeBucket,+ bucketExists,+ Region,+ getLocation, - -- ** Listing incomplete uploads- , listIncompleteUploads- , UploadId- , UploadInfo(..)- , listIncompleteParts- , ObjectPartInfo(..)+ -- ** Listing buckets+ BucketInfo (..),+ listBuckets, - -- ** Bucket Notifications- , getBucketNotification- , putBucketNotification- , removeAllBucketNotification- , Notification(..)- , defaultNotification- , NotificationConfig(..)- , Arn- , Event(..)- , Filter(..)- , defaultFilter- , FilterKey(..)- , defaultFilterKey- , FilterRules(..)- , defaultFilterRules- , FilterRule(..)+ -- ** Listing objects+ listObjects,+ listObjectsV1,+ ListItem (..),+ ObjectInfo,+ oiObject,+ oiModTime,+ oiETag,+ oiSize,+ oiUserMetadata,+ oiMetadata, - -- * Object Operations- , Object+ -- ** Listing incomplete uploads+ listIncompleteUploads,+ UploadId,+ UploadInfo (..),+ listIncompleteParts,+ ObjectPartInfo (..), - -- ** File-based operations- , fGetObject- , fPutObject+ -- ** Bucket Notifications+ getBucketNotification,+ putBucketNotification,+ removeAllBucketNotification,+ Notification (..),+ defaultNotification,+ NotificationConfig (..),+ Arn,+ Event (..),+ Filter (..),+ defaultFilter,+ FilterKey (..),+ defaultFilterKey,+ FilterRules (..),+ defaultFilterRules,+ FilterRule (..), - -- ** Conduit-based streaming operations- , putObject- , PutObjectOptions- , defaultPutObjectOptions- , pooContentType- , pooContentEncoding- , pooContentDisposition- , pooContentLanguage- , pooCacheControl- , pooStorageClass- , pooUserMetadata- , pooNumThreads- , pooSSE+ -- * Object Operations+ Object, - , getObject- , GetObjectOptions- , defaultGetObjectOptions- , gooRange- , gooIfMatch- , gooIfNoneMatch- , gooIfModifiedSince- , gooIfUnmodifiedSince- , gooSSECKey- , GetObjectResponse- , gorObjectInfo- , gorObjectStream+ -- ** File-based operations+ fGetObject,+ fPutObject, - -- ** Server-side object copying- , copyObject- , SourceInfo- , defaultSourceInfo- , srcBucket- , srcObject- , srcRange- , srcIfMatch- , srcIfNoneMatch- , srcIfModifiedSince- , srcIfUnmodifiedSince- , DestinationInfo- , defaultDestinationInfo- , dstBucket- , dstObject+ -- ** Conduit-based streaming operations+ putObject,+ PutObjectOptions,+ defaultPutObjectOptions,+ pooContentType,+ pooContentEncoding,+ pooContentDisposition,+ pooContentLanguage,+ pooCacheControl,+ pooStorageClass,+ pooUserMetadata,+ pooNumThreads,+ pooSSE,+ getObject,+ GetObjectOptions,+ defaultGetObjectOptions,+ gooRange,+ gooIfMatch,+ gooIfNoneMatch,+ gooIfModifiedSince,+ gooIfUnmodifiedSince,+ gooSSECKey,+ GetObjectResponse,+ gorObjectInfo,+ gorObjectStream, - -- ** Querying object info- , statObject+ -- ** Server-side object copying+ copyObject,+ SourceInfo,+ defaultSourceInfo,+ srcBucket,+ srcObject,+ srcRange,+ srcIfMatch,+ srcIfNoneMatch,+ srcIfModifiedSince,+ srcIfUnmodifiedSince,+ DestinationInfo,+ defaultDestinationInfo,+ dstBucket,+ dstObject, - -- ** Object removal operations- , removeObject- , removeIncompleteUpload+ -- ** Querying object info+ statObject, - -- ** Select Object Content with SQL- , module Network.Minio.SelectAPI+ -- ** Object removal operations+ removeObject,+ removeIncompleteUpload, - -- * Server-Side Encryption Helpers- , mkSSECKey- , SSECKey- , SSE(..)+ -- ** Select Object Content with SQL+ module Network.Minio.SelectAPI, - -- * Presigned Operations- , presignedPutObjectUrl- , presignedGetObjectUrl- , presignedHeadObjectUrl- , UrlExpiry+ -- * Server-Side Encryption Helpers+ mkSSECKey,+ SSECKey,+ SSE (..), - -- ** POST (browser) upload helpers- -- | Please see- -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html- -- for detailed information.- , newPostPolicy- , presignedPostPolicy- , showPostPolicy- , PostPolicy- , PostPolicyError(..)+ -- * Presigned Operations+ presignedPutObjectUrl,+ presignedGetObjectUrl,+ presignedHeadObjectUrl,+ UrlExpiry, - -- *** Post Policy condition helpers- , PostPolicyCondition- , ppCondBucket- , ppCondContentLengthRange- , ppCondContentType- , ppCondKey- , ppCondKeyStartsWith- , ppCondSuccessActionStatus+ -- ** POST (browser) upload helpers - -- * Error handling- -- | Data types representing various errors that may occur while- -- working with an object storage service.- , MinioErr(..)- , MErrV(..)- , ServiceErr(..)+ -- | Please see+ -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html+ -- for detailed information.+ newPostPolicy,+ presignedPostPolicy,+ showPostPolicy,+ PostPolicy,+ PostPolicyError (..), -) where+ -- *** Post Policy condition helpers+ 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 qualified Data.Conduit as C+import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Combinators as CC--import Lib.Prelude--import Network.Minio.CopyObject-import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.ListOps-import Network.Minio.PutObject-import Network.Minio.S3API-import Network.Minio.SelectAPI-import Network.Minio.Utils+import Lib.Prelude+import Network.Minio.CopyObject+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.ListOps+import Network.Minio.PutObject+import Network.Minio.S3API+import Network.Minio.SelectAPI+import Network.Minio.Utils -- | Lists buckets. listBuckets :: Minio [BucketInfo]@@ -248,8 +247,12 @@ C.connect (gorObjectStream src) $ CB.sinkFileCautious fp -- | Upload the given file to the given object.-fPutObject :: Bucket -> Object -> FilePath- -> PutObjectOptions -> Minio ()+fPutObject ::+ Bucket ->+ Object ->+ FilePath ->+ PutObjectOptions ->+ Minio () fPutObject bucket object f opts = void $ putObjectInternal bucket object opts $ ODFile f Nothing @@ -257,8 +260,13 @@ -- known; this helps the library select optimal part sizes to perform -- a multipart upload. If not specified, it is assumed that the object -- can be potentially 5TiB and selects multipart sizes appropriately.-putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()- -> Maybe Int64 -> PutObjectOptions -> Minio ()+putObject ::+ Bucket ->+ Object ->+ C.ConduitM () ByteString Minio () ->+ Maybe Int64 ->+ PutObjectOptions ->+ Minio () putObject bucket object src sizeMay opts = void $ putObjectInternal bucket object opts $ ODStream src sizeMay @@ -268,18 +276,25 @@ -- copy operation if the new object is to be greater than 5GiB in -- size. copyObject :: DestinationInfo -> SourceInfo -> Minio ()-copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)- (dstObject dstInfo) srcInfo+copyObject dstInfo srcInfo =+ void $+ copyObjectInternal+ (dstBucket dstInfo)+ (dstObject dstInfo)+ srcInfo -- | Remove an object from the object store. removeObject :: Bucket -> Object -> Minio () removeObject = deleteObject -- | Get an object from the object store.-getObject :: Bucket -> Object -> GetObjectOptions- -> Minio GetObjectResponse+getObject ::+ Bucket ->+ Object ->+ GetObjectOptions ->+ Minio GetObjectResponse getObject bucket object opts =- getObject' bucket object [] $ gooToHeaders opts+ getObject' bucket object [] $ gooToHeaders opts -- | Get an object's metadata from the object store. It accepts the -- same options as GetObject.@@ -309,6 +324,8 @@ -- | Removes an ongoing multipart upload of an object. removeIncompleteUpload :: Bucket -> Object -> Minio () removeIncompleteUpload bucket object = do- uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False- C..| CC.sinkList+ uploads <-+ C.runConduit $+ listIncompleteUploads bucket (Just object) False+ C..| CC.sinkList mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs view
@@ -15,169 +15,187 @@ -- module Network.Minio.API- ( connect- , S3ReqInfo(..)- , runMinio- , executeRequest- , mkStreamRequest- , getLocation-- , isValidBucketName- , checkBucketNameValidity- , isValidObjectName- , checkObjectNameValidity- ) where--import Control.Retry (fullJitterBackoff,- limitRetriesByCumulativeDelay,- retrying)-import qualified Data.ByteString as B-import qualified Data.Char as C-import qualified Data.Conduit as C-import qualified Data.HashMap.Strict as H-import qualified Data.Text as T-import qualified Data.Time.Clock as Time-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+ ( connect,+ S3ReqInfo (..),+ runMinio,+ executeRequest,+ mkStreamRequest,+ getLocation,+ isValidBucketName,+ checkBucketNameValidity,+ isValidObjectName,+ checkObjectNameValidity,+ )+where -import Network.Minio.APICommon-import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.Sign.V4-import Network.Minio.Utils-import Network.Minio.XmlParser+import Control.Retry+ ( fullJitterBackoff,+ limitRetriesByCumulativeDelay,+ retrying,+ )+import qualified Data.ByteString as B+import qualified Data.Char as C+import qualified Data.Conduit as C+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import qualified Data.Time.Clock as Time+import Lib.Prelude+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 Network.Minio.APICommon+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.Sign.V4+import Network.Minio.Utils+import Network.Minio.XmlParser -- | Fetch bucket location (region) getLocation :: Bucket -> Minio Region getLocation bucket = do- resp <- executeRequest $ defaultS3ReqInfo {- riBucket = Just bucket- , riQueryParams = [("location", Nothing)]- , riNeedsLocation = False- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riBucket = Just bucket,+ riQueryParams = [("location", Nothing)],+ riNeedsLocation = False+ } parseLocation $ NC.responseBody resp - -- | Looks for region in RegionMap and updates it using getLocation if -- absent. discoverRegion :: S3ReqInfo -> Minio (Maybe Region) discoverRegion ri = runMaybeT $ do bucket <- MaybeT $ return $ riBucket ri regionMay <- lift $ lookupRegionCache bucket- maybe (do- l <- lift $ getLocation bucket- lift $ addToRegionCache bucket l- return l- ) return regionMay+ maybe+ ( do+ l <- lift $ getLocation bucket+ lift $ addToRegionCache bucket l+ return l+ )+ return+ regionMay getRegion :: S3ReqInfo -> Minio (Maybe Region) getRegion ri = do- ci <- asks mcConnInfo-- -- getService/makeBucket/getLocation -- don't need location- if | not $ riNeedsLocation ri ->- return $ Just $ connectRegion ci-- -- if autodiscovery of location is disabled by user- | not $ connectAutoDiscoverRegion ci ->- return $ Just $ connectRegion ci+ ci <- asks mcConnInfo - -- discover the region for the request- | otherwise -> discoverRegion ri+ -- getService/makeBucket/getLocation -- don't need location+ if+ | not $ riNeedsLocation ri ->+ return $ Just $ connectRegion ci+ -- if autodiscovery of location is disabled by user+ | not $ connectAutoDiscoverRegion ci ->+ return $ Just $ connectRegion ci+ -- discover the region for the request+ | otherwise -> discoverRegion ri getRegionHost :: Region -> Minio Text getRegionHost r = do- ci <- asks mcConnInfo+ ci <- asks mcConnInfo - if "amazonaws.com" `T.isSuffixOf` connectHost ci- then maybe (throwIO $ MErrVRegionNotSupported r)- return (H.lookup r awsRegionMap)+ if "amazonaws.com" `T.isSuffixOf` connectHost ci+ then+ maybe+ (throwIO $ MErrVRegionNotSupported r)+ return+ (H.lookup r awsRegionMap) else return $ connectHost ci buildRequest :: S3ReqInfo -> Minio NC.Request buildRequest ri = do- maybe (return ()) checkBucketNameValidity $ riBucket ri- maybe (return ()) checkObjectNameValidity $ riObject ri-- ci <- asks mcConnInfo-- regionMay <- getRegion ri-- regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay+ maybe (return ()) checkBucketNameValidity $ riBucket ri+ maybe (return ()) checkObjectNameValidity $ riObject ri - let ri' = ri { riHeaders = hostHeader : riHeaders ri- , riRegion = regionMay- }- ci' = ci { connectHost = regionHost }- hostHeader = (hHost, getHostAddr ci')+ ci <- asks mcConnInfo - -- Does not contain body and auth info.- baseRequest = NC.defaultRequest- { NC.method = riMethod ri'- , NC.secure = connectIsSecure ci'- , NC.host = encodeUtf8 $ connectHost ci'- , NC.port = connectPort ci'- , NC.path = getS3Path (riBucket ri') (riObject ri')- , NC.requestHeaders = riHeaders ri'- , NC.queryString = HT.renderQuery False $ riQueryParams ri'- }+ regionMay <- getRegion ri - timeStamp <- liftIO Time.getCurrentTime+ regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay - let sp = SignParams (connectAccessKey ci') (connectSecretKey ci')- timeStamp (riRegion ri') Nothing Nothing+ let ri' =+ ri+ { riHeaders = hostHeader : riHeaders ri,+ riRegion = regionMay+ }+ ci' = ci {connectHost = regionHost}+ hostHeader = (hHost, getHostAddr ci')+ -- Does not contain body and auth info.+ baseRequest =+ NC.defaultRequest+ { NC.method = riMethod ri',+ NC.secure = connectIsSecure ci',+ NC.host = encodeUtf8 $ connectHost ci',+ NC.port = connectPort ci',+ NC.path = getS3Path (riBucket ri') (riObject ri'),+ NC.requestHeaders = riHeaders ri',+ NC.queryString = HT.renderQuery False $ riQueryParams ri'+ } - -- Cases to handle:- --- -- 1. Connection is secure: use unsigned payload- --- -- 2. Insecure connection, streaming signature is enabled via use of- -- conduit payload: use streaming signature for request.- --- -- 3. Insecure connection, non-conduit payload: compute payload- -- sha256hash, buffer request in memory and perform request.+ timeStamp <- liftIO Time.getCurrentTime - -- case 2 from above.- if | isStreamingPayload (riPayload ri') &&- (not $ connectIsSecure ci') -> do- (pLen, pSrc) <- case riPayload ri of- PayloadC l src -> return (l, src)- _ -> throwIO MErrVUnexpectedPayload- let reqFn = signV4Stream pLen sp baseRequest- return $ reqFn pSrc+ let sp =+ SignParams+ (connectAccessKey ci')+ (connectSecretKey ci')+ timeStamp+ (riRegion ri')+ Nothing+ Nothing - | otherwise -> do- -- case 1 described above.- sp' <- if | connectIsSecure ci' -> return sp- -- case 3 described above.- | otherwise -> do- pHash <- getPayloadSHA256Hash $ riPayload ri'- return $ sp { spPayloadHash = Just pHash }+ -- Cases to handle:+ --+ -- 1. Connection is secure: use unsigned payload+ --+ -- 2. Insecure connection, streaming signature is enabled via use of+ -- conduit payload: use streaming signature for request.+ --+ -- 3. Insecure connection, non-conduit payload: compute payload+ -- sha256hash, buffer request in memory and perform request. - let signHeaders = signV4 sp' baseRequest- return $ baseRequest- { NC.requestHeaders =- NC.requestHeaders baseRequest ++- mkHeaderFromPairs signHeaders- , NC.requestBody = getRequestBody (riPayload ri')- }+ -- case 2 from above.+ if+ | isStreamingPayload (riPayload ri')+ && (not $ connectIsSecure ci') -> do+ (pLen, pSrc) <- case riPayload ri of+ PayloadC l src -> return (l, src)+ _ -> throwIO MErrVUnexpectedPayload+ let reqFn = signV4Stream pLen sp baseRequest+ return $ reqFn pSrc+ | otherwise -> do+ -- case 1 described above.+ sp' <-+ if+ | connectIsSecure ci' -> return sp+ -- case 3 described above.+ | otherwise -> do+ pHash <- getPayloadSHA256Hash $ riPayload ri'+ return $ sp {spPayloadHash = Just pHash} + let signHeaders = signV4 sp' baseRequest+ return $+ baseRequest+ { NC.requestHeaders =+ NC.requestHeaders baseRequest+ ++ mkHeaderFromPairs signHeaders,+ NC.requestBody = getRequestBody (riPayload ri')+ } retryAPIRequest :: Minio a -> Minio a retryAPIRequest apiCall = do- resE <- retrying retryPolicy (const shouldRetry) $- const $ try apiCall+ resE <-+ retrying retryPolicy (const shouldRetry)+ $ const+ $ try apiCall either throwIO return resE where -- Retry using the full-jitter backoff method for up to 10 mins -- total- retryPolicy = limitRetriesByCumulativeDelay tenMins- $ fullJitterBackoff oneMilliSecond-+ retryPolicy =+ limitRetriesByCumulativeDelay tenMins $+ fullJitterBackoff oneMilliSecond oneMilliSecond = 1000 -- in microseconds tenMins = 10 * 60 * 1000000 -- in microseconds -- retry on connection related failure@@ -189,23 +207,23 @@ -- API request failed with a retryable exception Left httpExn@(NC.HttpExceptionRequest _ exn) -> case (exn :: NC.HttpExceptionContent) of- NC.ResponseTimeout -> return True- NC.ConnectionTimeout -> return True+ NC.ResponseTimeout -> return True+ NC.ConnectionTimeout -> return True NC.ConnectionFailure _ -> return True -- We received an unexpected exception- _ -> throwIO httpExn+ _ -> throwIO httpExn -- We received an unexpected exception Left someOtherExn -> throwIO someOtherExn - executeRequest :: S3ReqInfo -> Minio (Response LByteString) executeRequest ri = do req <- buildRequest ri mgr <- asks mcConnManager retryAPIRequest $ httpLbs req mgr -mkStreamRequest :: S3ReqInfo- -> Minio (Response (C.ConduitM () ByteString Minio ()))+mkStreamRequest ::+ S3ReqInfo ->+ Minio (Response (C.ConduitM () ByteString Minio ())) mkStreamRequest ri = do req <- buildRequest ri mgr <- asks mcConnManager@@ -214,35 +232,43 @@ -- Bucket name validity check according to AWS rules. isValidBucketName :: Bucket -> Bool isValidBucketName bucket =- not (or [ len < 3 || len > 63- , or (map labelCheck labels)- , or (map labelCharsCheck labels)- , isIPCheck- ])+ not+ ( or+ [ len < 3 || len > 63,+ or (map labelCheck labels),+ or (map labelCharsCheck labels),+ isIPCheck+ ]+ ) where len = T.length bucket labels = T.splitOn "." bucket- -- does label `l` fail basic checks of length and start/end? labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'- -- does label `l` have non-allowed characters?- labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||- x == '-' ||- C.isDigit x)) l-+ labelCharsCheck l =+ isJust $+ T.find+ ( \x ->+ not+ ( C.isAsciiLower x+ || x == '-'+ || C.isDigit x+ )+ )+ l -- does label `l` have non-digit characters? labelNonDigits l = isJust $ T.find (not . C.isDigit) l labelAsNums = map (not . labelNonDigits) labels- -- check if bucket name looks like an IP isIPCheck = and labelAsNums && length labelAsNums == 4 -- Throws exception iff bucket name is invalid according to AWS rules. checkBucketNameValidity :: MonadIO m => Bucket -> m () checkBucketNameValidity bucket =- when (not $ isValidBucketName bucket) $- throwIO $ MErrVInvalidBucketName bucket+ when (not $ isValidBucketName bucket)+ $ throwIO+ $ MErrVInvalidBucketName bucket isValidObjectName :: Object -> Bool isValidObjectName object =@@ -250,5 +276,6 @@ checkObjectNameValidity :: MonadIO m => Object -> m () checkObjectNameValidity object =- when (not $ isValidObjectName object) $- throwIO $ MErrVInvalidObjectName object+ when (not $ isValidObjectName object)+ $ throwIO+ $ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs view
@@ -16,37 +16,38 @@ module Network.Minio.APICommon where -import qualified Conduit as C-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as LB-import Data.Conduit.Binary (sourceHandleRange)-import qualified Network.HTTP.Conduit as NC-import qualified Network.HTTP.Types as HT--import Lib.Prelude--import Network.Minio.Data-import Network.Minio.Data.Crypto-import Network.Minio.Errors+import qualified Conduit as C+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LB+import Data.Conduit.Binary (sourceHandleRange)+import Lib.Prelude+import qualified Network.HTTP.Conduit as NC+import qualified Network.HTTP.Types as HT+import Network.Minio.Data+import Network.Minio.Data.Crypto+import Network.Minio.Errors sha256Header :: ByteString -> HT.Header-sha256Header = ("x-amz-content-sha256", )+sha256Header = ("x-amz-content-sha256",) -- | This function throws an error if the payload is a conduit (as it -- will not be possible to re-read the conduit after it is consumed). getPayloadSHA256Hash :: Payload -> Minio ByteString getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs-getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $- sourceHandleRange h- (return . fromIntegral $ off)- (return . fromIntegral $ size)+getPayloadSHA256Hash (PayloadH h off size) =+ hashSHA256FromSource $+ sourceHandleRange+ h+ (return . fromIntegral $ off)+ (return . fromIntegral $ size) getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload getRequestBody :: Payload -> NC.RequestBody getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs getRequestBody (PayloadH h off size) = NC.requestBodySource (fromIntegral size) $- sourceHandleRange h+ sourceHandleRange+ h (return . fromIntegral $ off) (return . fromIntegral $ size) getRequestBody (PayloadC n src) = NC.requestBodySource n src@@ -55,14 +56,17 @@ mkStreamingPayload payload = case payload of PayloadBS bs ->- PayloadC (fromIntegral $ BS.length bs)+ PayloadC+ (fromIntegral $ BS.length bs) (C.sourceLazy $ LB.fromStrict bs) PayloadH h off len ->- PayloadC len $ sourceHandleRange h- (return . fromIntegral $ off)- (return . fromIntegral $ len)+ PayloadC len $+ sourceHandleRange+ h+ (return . fromIntegral $ off)+ (return . fromIntegral $ len) _ -> payload isStreamingPayload :: Payload -> Bool isStreamingPayload (PayloadC _ _) = True-isStreamingPayload _ = False+isStreamingPayload _ = False
src/Network/Minio/AdminAPI.hs view
@@ -16,270 +16,304 @@ module Network.Minio.AdminAPI ( -- * MinIO Admin API- --------------------+ --------------------+ -- | Provides MinIO admin API and related types. It is in -- experimental state.- DriveInfo(..)- , ErasureInfo(..)- , Backend(..)- , ConnStats(..)- , HttpStats(..)- , ServerProps(..)- , CountNAvgTime(..)- , StorageClass(..)- , StorageInfo(..)- , SIData(..)- , ServerInfo(..)- , getServerInfo-- , HealOpts(..)- , HealResultItem(..)- , HealStatus(..)- , HealStartResp(..)- , startHeal- , forceStartHeal- , getHealStatus-- , SetConfigResult(..)- , NodeSummary(..)- , setConfig- , getConfig-- , ServerVersion(..)- , ServiceStatus(..)- , serviceStatus-- , ServiceAction(..)- , serviceSendAction- ) where--import Data.Aeson (FromJSON, ToJSON, Value (Object),- eitherDecode, object, pairs,- parseJSON, toEncoding, toJSON,- withObject, withText, (.:), (.:?),- (.=))-import qualified Data.Aeson as A-import Data.Aeson.Types (typeMismatch)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as LBS-import qualified Data.Text as T-import Data.Time (NominalDiffTime, getCurrentTime)-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+ DriveInfo (..),+ ErasureInfo (..),+ Backend (..),+ ConnStats (..),+ HttpStats (..),+ ServerProps (..),+ CountNAvgTime (..),+ StorageClass (..),+ StorageInfo (..),+ SIData (..),+ ServerInfo (..),+ getServerInfo,+ HealOpts (..),+ HealResultItem (..),+ HealStatus (..),+ HealStartResp (..),+ startHeal,+ forceStartHeal,+ getHealStatus,+ SetConfigResult (..),+ NodeSummary (..),+ setConfig,+ getConfig,+ ServerVersion (..),+ ServiceStatus (..),+ serviceStatus,+ ServiceAction (..),+ serviceSendAction,+ )+where -import Network.Minio.APICommon-import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.Sign.V4-import Network.Minio.Utils+import Data.Aeson+ ( (.:),+ (.:?),+ (.=),+ FromJSON,+ ToJSON,+ Value (Object),+ eitherDecode,+ object,+ pairs,+ parseJSON,+ toEncoding,+ toJSON,+ withObject,+ withText,+ )+import qualified Data.Aeson as A+import Data.Aeson.Types (typeMismatch)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as LBS+import qualified Data.Text as T+import Data.Time (NominalDiffTime, getCurrentTime)+import Lib.Prelude+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 Network.Minio.APICommon+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.Sign.V4+import Network.Minio.Utils data DriveInfo = DriveInfo- { diUuid :: Text- , diEndpoint :: Text- , diState :: Text- } deriving (Eq, Show)+ { diUuid :: Text,+ diEndpoint :: Text,+ diState :: Text+ }+ deriving (Eq, Show) instance FromJSON DriveInfo where- parseJSON = withObject "DriveInfo" $ \v -> DriveInfo- <$> v .: "uuid"- <*> v .: "endpoint"- <*> v .: "state"+ parseJSON = withObject "DriveInfo" $ \v ->+ DriveInfo+ <$> v .: "uuid"+ <*> v .: "endpoint"+ <*> v .: "state" data StorageClass = StorageClass- { scParity :: Int- , scData :: Int- } deriving (Eq, Show)+ { scParity :: Int,+ scData :: Int+ }+ deriving (Eq, Show) data ErasureInfo = ErasureInfo- { eiOnlineDisks :: Int- , eiOfflineDisks :: Int- , eiStandard :: StorageClass- , eiReducedRedundancy :: StorageClass- , eiSets :: [[DriveInfo]]- } deriving (Eq, Show)+ { eiOnlineDisks :: Int,+ eiOfflineDisks :: Int,+ eiStandard :: StorageClass,+ eiReducedRedundancy :: StorageClass,+ eiSets :: [[DriveInfo]]+ }+ deriving (Eq, Show) instance FromJSON ErasureInfo where- parseJSON = withObject "ErasureInfo" $ \v -> do- onlineDisks <- v .: "OnlineDisks"- offlineDisks <- v .: "OfflineDisks"- stdClass <- StorageClass- <$> v .: "StandardSCData"- <*> v .: "StandardSCParity"- rrClass <- StorageClass- <$> v .: "RRSCData"- <*> v .: "RRSCParity"- sets <- v .: "Sets"- return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets+ parseJSON = withObject "ErasureInfo" $ \v -> do+ onlineDisks <- v .: "OnlineDisks"+ offlineDisks <- v .: "OfflineDisks"+ stdClass <-+ StorageClass+ <$> v .: "StandardSCData"+ <*> v .: "StandardSCParity"+ rrClass <-+ StorageClass+ <$> v .: "RRSCData"+ <*> v .: "RRSCParity"+ sets <- v .: "Sets"+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets -data Backend = BackendFS- | BackendErasure ErasureInfo- deriving (Eq, Show)+data Backend+ = BackendFS+ | BackendErasure ErasureInfo+ deriving (Eq, Show) instance FromJSON Backend where- parseJSON = withObject "Backend" $ \v -> do- typ <- v .: "Type"- case typ :: Int of- 1 -> return BackendFS- 2 -> BackendErasure <$> parseJSON (Object v)- _ -> typeMismatch "BackendType" (Object v)+ parseJSON = withObject "Backend" $ \v -> do+ typ <- v .: "Type"+ case typ :: Int of+ 1 -> return BackendFS+ 2 -> BackendErasure <$> parseJSON (Object v)+ _ -> typeMismatch "BackendType" (Object v) data ConnStats = ConnStats- { csTransferred :: Int64- , csReceived :: Int64- } deriving (Eq, Show)+ { csTransferred :: Int64,+ csReceived :: Int64+ }+ deriving (Eq, Show) instance FromJSON ConnStats where- parseJSON = withObject "ConnStats" $ \v -> ConnStats- <$> v .: "transferred"- <*> v .: "received"+ parseJSON = withObject "ConnStats" $ \v ->+ ConnStats+ <$> v .: "transferred"+ <*> v .: "received" data ServerProps = ServerProps- { spUptime :: NominalDiffTime- , spVersion :: Text- , spCommitId :: Text- , spRegion :: Text- , spSqsArns :: [Text]- } deriving (Eq, Show)+ { spUptime :: NominalDiffTime,+ spVersion :: Text,+ spCommitId :: Text,+ spRegion :: Text,+ spSqsArns :: [Text]+ }+ deriving (Eq, Show) instance FromJSON ServerProps where- parseJSON = withObject "SIServer" $ \v -> do- uptimeNs <- v .: "uptime"- let uptime = uptimeNs / 1e9- ver <- v .: "version"- commitId <- v .: "commitID"- region <- v .: "region"- arn <- v .: "sqsARN"- return $ ServerProps uptime ver commitId region arn+ parseJSON = withObject "SIServer" $ \v -> do+ uptimeNs <- v .: "uptime"+ let uptime = uptimeNs / 1e9+ ver <- v .: "version"+ commitId <- v .: "commitID"+ region <- v .: "region"+ arn <- v .: "sqsARN"+ return $ ServerProps uptime ver commitId region arn data StorageInfo = StorageInfo- { siUsed :: Int64- , siBackend :: Backend- } deriving (Eq, Show)+ { siUsed :: Int64,+ siBackend :: Backend+ }+ deriving (Eq, Show) instance FromJSON StorageInfo where- parseJSON = withObject "StorageInfo" $ \v -> StorageInfo- <$> v .: "Used"- <*> v .: "Backend"+ parseJSON = withObject "StorageInfo" $ \v ->+ StorageInfo+ <$> v .: "Used"+ <*> v .: "Backend" data CountNAvgTime = CountNAvgTime- { caCount :: Int64- , caAvgDuration :: Text- } deriving (Eq, Show)+ { caCount :: Int64,+ caAvgDuration :: Text+ }+ deriving (Eq, Show) instance FromJSON CountNAvgTime where- parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime- <$> v .: "count"- <*> v .: "avgDuration"+ parseJSON = withObject "CountNAvgTime" $ \v ->+ CountNAvgTime+ <$> v .: "count"+ <*> v .: "avgDuration" data HttpStats = HttpStats- { hsTotalHeads :: CountNAvgTime- , hsSuccessHeads :: CountNAvgTime- , hsTotalGets :: CountNAvgTime- , hsSuccessGets :: CountNAvgTime- , hsTotalPuts :: CountNAvgTime- , hsSuccessPuts :: CountNAvgTime- , hsTotalPosts :: CountNAvgTime- , hsSuccessPosts :: CountNAvgTime- , hsTotalDeletes :: CountNAvgTime- , hsSuccessDeletes :: CountNAvgTime- } deriving (Eq, Show)+ { hsTotalHeads :: CountNAvgTime,+ hsSuccessHeads :: CountNAvgTime,+ hsTotalGets :: CountNAvgTime,+ hsSuccessGets :: CountNAvgTime,+ hsTotalPuts :: CountNAvgTime,+ hsSuccessPuts :: CountNAvgTime,+ hsTotalPosts :: CountNAvgTime,+ hsSuccessPosts :: CountNAvgTime,+ hsTotalDeletes :: CountNAvgTime,+ hsSuccessDeletes :: CountNAvgTime+ }+ deriving (Eq, Show) instance FromJSON HttpStats where- parseJSON = withObject "HttpStats" $ \v -> HttpStats- <$> v .: "totalHEADs"- <*> v .: "successHEADs"- <*> v .: "totalGETs"- <*> v .: "successGETs"- <*> v .: "totalPUTs"- <*> v .: "successPUTs"- <*> v .: "totalPOSTs"- <*> v .: "successPOSTs"- <*> v .: "totalDELETEs"- <*> v .: "successDELETEs"+ parseJSON = withObject "HttpStats" $ \v ->+ HttpStats+ <$> v .: "totalHEADs"+ <*> v .: "successHEADs"+ <*> v .: "totalGETs"+ <*> v .: "successGETs"+ <*> v .: "totalPUTs"+ <*> v .: "successPUTs"+ <*> v .: "totalPOSTs"+ <*> v .: "successPOSTs"+ <*> v .: "totalDELETEs"+ <*> v .: "successDELETEs" data SIData = SIData- { sdStorage :: StorageInfo- , sdConnStats :: ConnStats- , sdHttpStats :: HttpStats- , sdProps :: ServerProps- } deriving (Eq, Show)+ { sdStorage :: StorageInfo,+ sdConnStats :: ConnStats,+ sdHttpStats :: HttpStats,+ sdProps :: ServerProps+ }+ deriving (Eq, Show) instance FromJSON SIData where- parseJSON = withObject "SIData" $ \v -> SIData- <$> v .: "storage"- <*> v .: "network"- <*> v .: "http"- <*> v .: "server"+ parseJSON = withObject "SIData" $ \v ->+ SIData+ <$> v .: "storage"+ <*> v .: "network"+ <*> v .: "http"+ <*> v .: "server" data ServerInfo = ServerInfo- { siError :: Text- , siAddr :: Text- , siData :: SIData- } deriving (Eq, Show)+ { siError :: Text,+ siAddr :: Text,+ siData :: SIData+ }+ deriving (Eq, Show) instance FromJSON ServerInfo where- parseJSON = withObject "ServerInfo" $ \v -> ServerInfo- <$> v .: "error"- <*> v .: "addr"- <*> v .: "data"+ parseJSON = withObject "ServerInfo" $ \v ->+ ServerInfo+ <$> v .: "error"+ <*> v .: "addr"+ <*> v .: "data" data ServerVersion = ServerVersion- { svVersion :: Text- , svCommitId :: Text- } deriving (Eq, Show)+ { svVersion :: Text,+ svCommitId :: Text+ }+ deriving (Eq, Show) instance FromJSON ServerVersion where- parseJSON = withObject "ServerVersion" $ \v -> ServerVersion+ parseJSON = withObject "ServerVersion" $ \v ->+ ServerVersion <$> v .: "version" <*> v .: "commitID" data ServiceStatus = ServiceStatus- { ssVersion :: ServerVersion- , ssUptime :: NominalDiffTime- } deriving (Eq, Show)+ { ssVersion :: ServerVersion,+ ssUptime :: NominalDiffTime+ }+ deriving (Eq, Show) instance FromJSON ServiceStatus where- parseJSON = withObject "ServiceStatus" $ \v -> do- serverVersion <- v .: "serverVersion"- uptimeNs <- v .: "uptime"- let uptime = uptimeNs / 1e9- return $ ServiceStatus serverVersion uptime+ parseJSON = withObject "ServiceStatus" $ \v -> do+ serverVersion <- v .: "serverVersion"+ uptimeNs <- v .: "uptime"+ let uptime = uptimeNs / 1e9+ return $ ServiceStatus serverVersion uptime -data ServiceAction = ServiceActionRestart- | ServiceActionStop- deriving (Eq, Show)+data ServiceAction+ = ServiceActionRestart+ | ServiceActionStop+ deriving (Eq, Show) instance ToJSON ServiceAction where- toJSON a = object [ "action" .= serviceActionToText a ]+ toJSON a = object ["action" .= serviceActionToText a] serviceActionToText :: ServiceAction -> Text serviceActionToText a = case a of ServiceActionRestart -> "restart"- ServiceActionStop -> "stop"+ ServiceActionStop -> "stop" adminPath :: ByteString adminPath = "/minio/admin" data HealStartResp = HealStartResp- { hsrClientToken :: Text- , hsrClientAddr :: Text- , hsrStartTime :: UTCTime- } deriving (Eq, Show)+ { hsrClientToken :: Text,+ hsrClientAddr :: Text,+ hsrStartTime :: UTCTime+ }+ deriving (Eq, Show) instance FromJSON HealStartResp where- parseJSON = withObject "HealStartResp" $ \v -> HealStartResp- <$> v .: "clientToken"- <*> v .: "clientAddress"- <*> v .: "startTime"+ parseJSON = withObject "HealStartResp" $ \v ->+ HealStartResp+ <$> v .: "clientToken"+ <*> v .: "clientAddress"+ <*> v .: "startTime" data HealOpts = HealOpts- { hoRecursive :: Bool- , hoDryRun :: Bool- } deriving (Eq, Show)+ { hoRecursive :: Bool,+ hoDryRun :: Bool+ }+ deriving (Eq, Show) instance ToJSON HealOpts where toJSON (HealOpts r d) =@@ -288,197 +322,234 @@ pairs ("recursive" .= r <> "dryRun" .= d) instance FromJSON HealOpts where- parseJSON = withObject "HealOpts" $ \v -> HealOpts+ parseJSON = withObject "HealOpts" $ \v ->+ HealOpts <$> v .: "recursive" <*> v .: "dryRun" -data HealItemType = HealItemMetadata- | HealItemBucket- | HealItemBucketMetadata- | HealItemObject- deriving (Eq, Show)+data HealItemType+ = HealItemMetadata+ | HealItemBucket+ | HealItemBucketMetadata+ | HealItemObject+ deriving (Eq, Show) instance FromJSON HealItemType where- parseJSON = withText "HealItemType" $ \v -> case v of- "metadata" -> return HealItemMetadata- "bucket" -> return HealItemBucket- "object" -> return HealItemObject- "bucket-metadata" -> return HealItemBucketMetadata- _ -> typeMismatch "HealItemType" (A.String v)+ parseJSON = withText "HealItemType" $ \v -> case v of+ "metadata" -> return HealItemMetadata+ "bucket" -> return HealItemBucket+ "object" -> return HealItemObject+ "bucket-metadata" -> return HealItemBucketMetadata+ _ -> typeMismatch "HealItemType" (A.String v) data NodeSummary = NodeSummary- { nsName :: Text- , nsErrSet :: Bool- , nsErrMessage :: Text- } deriving (Eq, Show)+ { nsName :: Text,+ nsErrSet :: Bool,+ nsErrMessage :: Text+ }+ deriving (Eq, Show) instance FromJSON NodeSummary where- parseJSON = withObject "NodeSummary" $ \v -> NodeSummary- <$> v .: "name"- <*> v .: "errSet"- <*> v .: "errMsg"+ parseJSON = withObject "NodeSummary" $ \v ->+ NodeSummary+ <$> v .: "name"+ <*> v .: "errSet"+ <*> v .: "errMsg" data SetConfigResult = SetConfigResult- { scrStatus :: Bool- , scrNodeSummary :: [NodeSummary]- } deriving (Eq, Show)+ { scrStatus :: Bool,+ scrNodeSummary :: [NodeSummary]+ }+ deriving (Eq, Show) instance FromJSON SetConfigResult where- parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult- <$> v .: "status"- <*> v .: "nodeResults"+ parseJSON = withObject "SetConfigResult" $ \v ->+ SetConfigResult+ <$> v .: "status"+ <*> v .: "nodeResults" data HealResultItem = HealResultItem- { hriResultIdx :: Int- , hriType :: HealItemType- , hriBucket :: Bucket- , hriObject :: Object- , hriDetail :: Text- , hriParityBlocks :: Maybe Int- , hriDataBlocks :: Maybe Int- , hriDiskCount :: Int- , hriSetCount :: Int- , hriObjectSize :: Int- , hriBefore :: [DriveInfo]- , hriAfter :: [DriveInfo]- } deriving (Eq, Show)+ { hriResultIdx :: Int,+ hriType :: HealItemType,+ hriBucket :: Bucket,+ hriObject :: Object,+ hriDetail :: Text,+ hriParityBlocks :: Maybe Int,+ hriDataBlocks :: Maybe Int,+ hriDiskCount :: Int,+ hriSetCount :: Int,+ hriObjectSize :: Int,+ hriBefore :: [DriveInfo],+ hriAfter :: [DriveInfo]+ }+ deriving (Eq, Show) instance FromJSON HealResultItem where- parseJSON = withObject "HealResultItem" $ \v -> HealResultItem- <$> v .: "resultId"- <*> v .: "type"- <*> v .: "bucket"- <*> v .: "object"- <*> v .: "detail"- <*> v .:? "parityBlocks"- <*> v .:? "dataBlocks"- <*> v .: "diskCount"- <*> v .: "setCount"- <*> v .: "objectSize"- <*> (do before <- v .: "before"- before .: "drives")- <*> (do after <- v .: "after"- after .: "drives")+ parseJSON = withObject "HealResultItem" $ \v ->+ HealResultItem+ <$> v .: "resultId"+ <*> v .: "type"+ <*> v .: "bucket"+ <*> v .: "object"+ <*> v .: "detail"+ <*> v .:? "parityBlocks"+ <*> v .:? "dataBlocks"+ <*> v .: "diskCount"+ <*> v .: "setCount"+ <*> v .: "objectSize"+ <*> ( do+ before <- v .: "before"+ before .: "drives"+ )+ <*> ( do+ after <- v .: "after"+ after .: "drives"+ ) data HealStatus = HealStatus- { hsSummary :: Text- , hsStartTime :: UTCTime- , hsSettings :: HealOpts- , hsNumDisks :: Int- , hsFailureDetail :: Maybe Text- , hsItems :: Maybe [HealResultItem]- } deriving (Eq, Show)+ { hsSummary :: Text,+ hsStartTime :: UTCTime,+ hsSettings :: HealOpts,+ hsNumDisks :: Int,+ hsFailureDetail :: Maybe Text,+ hsItems :: Maybe [HealResultItem]+ }+ deriving (Eq, Show) instance FromJSON HealStatus where- parseJSON = withObject "HealStatus" $ \v -> HealStatus- <$> v .: "Summary"- <*> v .: "StartTime"- <*> v .: "Settings"- <*> v .: "NumDisks"- <*> v .:? "Detail"- <*> v .: "Items"+ parseJSON = withObject "HealStatus" $ \v ->+ HealStatus+ <$> v .: "Summary"+ <*> v .: "StartTime"+ <*> v .: "Settings"+ <*> v .: "NumDisks"+ <*> v .:? "Detail"+ <*> v .: "Items" healPath :: Maybe Bucket -> Maybe Text -> ByteString healPath bucket prefix = do if (isJust bucket)- then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"- <> fromMaybe "" prefix+ then+ encodeUtf8 $+ "v1/heal/" <> fromMaybe "" bucket <> "/"+ <> fromMaybe "" prefix else encodeUtf8 $ "v1/heal/" -- | Get server version and uptime. serviceStatus :: Minio ServiceStatus serviceStatus = do- rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet- , ariPayload = PayloadBS B.empty- , ariPayloadHash = Nothing- , ariPath = "v1/service"- , ariHeaders = []- , ariQueryParams = []- }+ rsp <-+ executeAdminRequest+ AdminReqInfo+ { ariMethod = HT.methodGet,+ ariPayload = PayloadBS B.empty,+ ariPayloadHash = Nothing,+ ariPath = "v1/service",+ ariHeaders = [],+ ariQueryParams = []+ } - let rspBS = NC.responseBody rsp- case eitherDecode rspBS of- Right ss -> return ss- Left err -> throwIO $ MErrVJsonParse $ T.pack err+ let rspBS = NC.responseBody rsp+ case eitherDecode rspBS of+ Right ss -> return ss+ Left err -> throwIO $ MErrVJsonParse $ T.pack err -- | Send service restart or stop action to MinIO server. serviceSendAction :: ServiceAction -> Minio () serviceSendAction action = do- let payload = PayloadBS $ LBS.toStrict $ A.encode action- void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost- , ariPayload = payload- , ariPayloadHash = Nothing- , ariPath = "v1/service"- , ariHeaders = []- , ariQueryParams = []- }+ let payload = PayloadBS $ LBS.toStrict $ A.encode action+ void $+ executeAdminRequest+ AdminReqInfo+ { ariMethod = HT.methodPost,+ ariPayload = payload,+ ariPayloadHash = Nothing,+ ariPath = "v1/service",+ ariHeaders = [],+ ariQueryParams = []+ } -- | Get the current config file from server. getConfig :: Minio ByteString getConfig = do- rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet- , ariPayload = PayloadBS B.empty- , ariPayloadHash = Nothing- , ariPath = "v1/config"- , ariHeaders = []- , ariQueryParams = []- }- return $ LBS.toStrict $ NC.responseBody rsp+ rsp <-+ executeAdminRequest+ AdminReqInfo+ { ariMethod = HT.methodGet,+ ariPayload = PayloadBS B.empty,+ ariPayloadHash = Nothing,+ ariPath = "v1/config",+ ariHeaders = [],+ ariQueryParams = []+ }+ return $ LBS.toStrict $ NC.responseBody rsp -- | Set a new config to the server. setConfig :: ByteString -> Minio SetConfigResult setConfig config = do- rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut- , ariPayload = PayloadBS config- , ariPayloadHash = Nothing- , ariPath = "v1/config"- , ariHeaders = []- , ariQueryParams = []- }+ rsp <-+ executeAdminRequest+ AdminReqInfo+ { ariMethod = HT.methodPut,+ ariPayload = PayloadBS config,+ ariPayloadHash = Nothing,+ ariPath = "v1/config",+ ariHeaders = [],+ ariQueryParams = []+ } - let rspBS = NC.responseBody rsp- case eitherDecode rspBS of- Right scr -> return scr- Left err -> throwIO $ MErrVJsonParse $ T.pack err+ let rspBS = NC.responseBody rsp+ case eitherDecode rspBS of+ Right scr -> return scr+ Left err -> throwIO $ MErrVJsonParse $ T.pack err -- | Get the progress of currently running heal task, this API should be -- invoked right after `startHeal`. `token` is obtained after `startHeal` -- which should be used to get the heal status. getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus getHealStatus bucket prefix token = do- when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath- let qparams = HT.queryTextToQuery [("clientToken", Just token)]- rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost- , ariPayload = PayloadBS B.empty- , ariPayloadHash = Nothing- , ariPath = healPath bucket prefix- , ariHeaders = []- , ariQueryParams = qparams- }- let rspBS = NC.responseBody rsp- case eitherDecode rspBS of- Right hs -> return hs- Left err -> throwIO $ MErrVJsonParse $ T.pack err+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]+ rsp <-+ executeAdminRequest+ AdminReqInfo+ { ariMethod = HT.methodPost,+ ariPayload = PayloadBS B.empty,+ ariPayloadHash = Nothing,+ ariPath = healPath bucket prefix,+ ariHeaders = [],+ ariQueryParams = qparams+ }+ let rspBS = NC.responseBody rsp+ case eitherDecode rspBS of+ Right hs -> return hs+ Left err -> throwIO $ MErrVJsonParse $ T.pack err doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp doHeal bucket prefix opts forceStart = do- when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath- let payload = PayloadBS $ LBS.toStrict $ A.encode opts- let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])- forceStart+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts+ let qparams =+ bool+ []+ (HT.queryTextToQuery [("forceStart", Just "true")])+ forceStart - rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost- , ariPayload = payload- , ariPayloadHash = Nothing- , ariPath = healPath bucket prefix- , ariHeaders = []- , ariQueryParams = qparams- }+ rsp <-+ executeAdminRequest+ AdminReqInfo+ { ariMethod = HT.methodPost,+ ariPayload = payload,+ ariPayloadHash = Nothing,+ ariPath = healPath bucket prefix,+ ariHeaders = [],+ ariQueryParams = qparams+ } - let rspBS = NC.responseBody rsp- case eitherDecode rspBS of- Right hsr -> return hsr- Left err -> throwIO $ MErrVJsonParse $ T.pack err+ let rspBS = NC.responseBody rsp+ case eitherDecode rspBS of+ Right hsr -> return hsr+ Left err -> throwIO $ MErrVJsonParse $ T.pack err -- | Start a heal sequence that scans data under given (possible empty) -- `bucket` and `prefix`. The `recursive` bool turns on recursive@@ -500,60 +571,75 @@ -- properties, storage information, network statistics, etc. getServerInfo :: Minio [ServerInfo] getServerInfo = do- rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet- , ariPayload = PayloadBS B.empty- , ariPayloadHash = Nothing- , ariPath = "v1/info"- , ariHeaders = []- , ariQueryParams = []- }- let rspBS = NC.responseBody rsp- case eitherDecode rspBS of- Right si -> return si- Left err -> throwIO $ MErrVJsonParse $ T.pack err+ rsp <-+ executeAdminRequest+ AdminReqInfo+ { ariMethod = HT.methodGet,+ ariPayload = PayloadBS B.empty,+ ariPayloadHash = Nothing,+ ariPath = "v1/info",+ ariHeaders = [],+ ariQueryParams = []+ }+ let rspBS = NC.responseBody rsp+ case eitherDecode rspBS of+ Right si -> return si+ Left err -> throwIO $ MErrVJsonParse $ T.pack err executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString) executeAdminRequest ari = do- req <- buildAdminRequest ari- mgr <- asks mcConnManager- httpLbs req mgr+ req <- buildAdminRequest ari+ mgr <- asks mcConnManager+ httpLbs req mgr buildAdminRequest :: AdminReqInfo -> Minio NC.Request buildAdminRequest areq = do- ci <- asks mcConnInfo- sha256Hash <- if | connectIsSecure ci ->- -- if secure connection- return "UNSIGNED-PAYLOAD"-- -- otherwise compute sha256- | otherwise -> getPayloadSHA256Hash (ariPayload areq)+ ci <- asks mcConnInfo+ sha256Hash <-+ if+ | connectIsSecure ci ->+ -- if secure connection+ return "UNSIGNED-PAYLOAD"+ -- otherwise compute sha256+ | otherwise -> getPayloadSHA256Hash (ariPayload areq) - timeStamp <- liftIO getCurrentTime+ timeStamp <- liftIO getCurrentTime - let hostHeader = (hHost, getHostAddr ci)- newAreq = areq { ariPayloadHash = Just sha256Hash- , ariHeaders = hostHeader- : sha256Header sha256Hash- : ariHeaders areq- }- signReq = toRequest ci newAreq- sp = SignParams (connectAccessKey ci) (connectSecretKey ci)- timeStamp Nothing Nothing (ariPayloadHash newAreq)- signHeaders = signV4 sp signReq+ let hostHeader = (hHost, getHostAddr ci)+ newAreq =+ areq+ { ariPayloadHash = Just sha256Hash,+ ariHeaders =+ hostHeader+ : sha256Header sha256Hash+ : ariHeaders areq+ }+ signReq = toRequest ci newAreq+ sp =+ SignParams+ (connectAccessKey ci)+ (connectSecretKey ci)+ timeStamp+ Nothing+ Nothing+ (ariPayloadHash newAreq)+ signHeaders = signV4 sp signReq - -- Update signReq with Authorization header containing v4 signature- return signReq {- NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders- }+ -- Update signReq with Authorization header containing v4 signature+ return+ signReq+ { NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders+ } where toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request- toRequest ci aReq = NC.defaultRequest- { NC.method = ariMethod aReq- , NC.secure = connectIsSecure ci- , NC.host = encodeUtf8 $ connectHost ci- , NC.port = connectPort ci- , NC.path = B.intercalate "/" [adminPath, ariPath aReq]- , NC.requestHeaders = ariHeaders aReq- , NC.queryString = HT.renderQuery False $ ariQueryParams aReq- , NC.requestBody = getRequestBody (ariPayload aReq)+ toRequest ci aReq =+ NC.defaultRequest+ { NC.method = ariMethod aReq,+ NC.secure = connectIsSecure ci,+ NC.host = encodeUtf8 $ connectHost ci,+ NC.port = connectPort ci,+ NC.path = B.intercalate "/" [adminPath, ariPath aReq],+ NC.requestHeaders = ariHeaders aReq,+ NC.queryString = HT.renderQuery False $ ariQueryParams aReq,+ NC.requestBody = getRequestBody (ariPayload aReq) }
src/Network/Minio/CopyObject.hs view
@@ -16,19 +16,19 @@ module Network.Minio.CopyObject where -import qualified Data.List as List--import Lib.Prelude--import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.S3API-import Network.Minio.Utils-+import qualified Data.List as List+import Lib.Prelude+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.S3API+import Network.Minio.Utils -- | Copy an object using single or multipart copy strategy.-copyObjectInternal :: Bucket -> Object -> SourceInfo- -> Minio ETag+copyObjectInternal ::+ Bucket ->+ Object ->+ SourceInfo ->+ Minio ETag copyObjectInternal b' o srcInfo = do let sBucket = srcBucket srcInfo sObject = srcObject srcInfo@@ -43,27 +43,35 @@ startOffset = fst range endOffset = snd range - when (isJust rangeMay &&- or [startOffset < 0, endOffset < startOffset,- endOffset >= fromIntegral srcSize]) $- throwIO $ MErrVInvalidSrcObjByteRange range+ when+ ( isJust rangeMay+ && or+ [ startOffset < 0,+ endOffset < startOffset,+ endOffset >= fromIntegral srcSize+ ]+ )+ $ throwIO+ $ MErrVInvalidSrcObjByteRange range -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR -- 2. If startOffset /= 0 use multipart copy- let destSize = (\(a, b) -> b - a + 1 ) $- maybe (0, srcSize - 1) identity rangeMay+ let destSize =+ (\(a, b) -> b - a + 1) $+ maybe (0, srcSize - 1) identity rangeMay if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize) then multiPartCopyObject b' o srcInfo srcSize-- else fst <$> copyObjectSingle b' o srcInfo{srcRange = Nothing} []+ else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} [] -- | Given the input byte range of the source object, compute the -- splits for a multipart copy object procedure. Minimum part size -- used is minPartSize. selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]-selectCopyRanges (st, end) = zip pns $- map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes+selectCopyRanges (st, end) =+ zip pns+ $ map (\(x, y) -> (st + x, st + x + y - 1))+ $ zip startOffsets partSizes where size = end - st + 1 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size@@ -71,22 +79,30 @@ -- | Perform a multipart copy object action. Since we cannot verify -- existing parts based on the source object, there is no resuming -- copy action support.-multiPartCopyObject :: Bucket -> Object -> SourceInfo -> Int64- -> Minio ETag+multiPartCopyObject ::+ Bucket ->+ Object ->+ SourceInfo ->+ Int64 ->+ Minio ETag multiPartCopyObject b o cps srcSize = do uid <- newMultipartUpload b o [] let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps partRanges = selectCopyRanges byteRange- partSources = map (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end) }))- partRanges- dstInfo = defaultDestinationInfo { dstBucket = b, dstObject = o}+ partSources =+ map+ (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))+ partRanges+ dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o} - copiedParts <- limitedMapConcurrently 10- (\(pn, cps') -> do- (etag, _) <- copyObjectPart dstInfo cps' uid pn []- return (pn, etag)- )- partSources+ copiedParts <-+ limitedMapConcurrently+ 10+ ( \(pn, cps') -> do+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []+ return (pn, etag)+ )+ partSources completeMultipartUpload b o uid copiedParts
src/Network/Minio/Data.hs view
@@ -13,1025 +13,1116 @@ -- See the License for the specific language governing permissions and -- limitations under the License. ----{-# LANGUAGE CPP #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE TypeFamilies #-}-module Network.Minio.Data where--import qualified Conduit as C-import qualified Control.Concurrent.MVar as M-import Control.Monad.IO.Unlift (UnliftIO (..), askUnliftIO,- withUnliftIO)-import Control.Monad.Trans.Resource-import qualified Data.Aeson as A-import qualified Data.ByteArray as BA-import qualified Data.ByteString as B-import Data.CaseInsensitive (mk)-import qualified Data.HashMap.Strict as H-import qualified Data.Ini as Ini-import Data.String (IsString (..))-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE-import Data.Time (defaultTimeLocale, formatTime)-import GHC.Show (Show (show))-import qualified Network.Connection as Conn-import Network.HTTP.Client (defaultManagerSettings)-import qualified Network.HTTP.Client.TLS as TLS-import qualified Network.HTTP.Conduit as NC-import Network.HTTP.Types (ByteRange, Header, Method, Query,- hRange)-import qualified Network.HTTP.Types as HT-import Network.Minio.Errors-import System.Directory (doesFileExist, getHomeDirectory)-import qualified System.Environment as Env-import System.FilePath.Posix (combine)-import Text.XML-import qualified UnliftIO as U--import Lib.Prelude-import Network.Minio.Data.Crypto---- | max obj size is 5TiB-maxObjectSize :: Int64-maxObjectSize = 5 * 1024 * 1024 * oneMiB---- | minimum size of parts used in multipart operations.-minPartSize :: Int64-minPartSize = 64 * oneMiB--oneMiB :: Int64-oneMiB = 1024 * 1024---- | maximum number of parts that can be uploaded for a single object.-maxMultipartParts :: Int64-maxMultipartParts = 10000---- TODO: Add a type which provides typed constants for region. this--- type should have a IsString instance to infer the appropriate--- constant.--- | awsRegionMap - library constant-awsRegionMap :: H.HashMap Text Text-awsRegionMap = H.fromList [- ("us-east-1", "s3.amazonaws.com")- , ("us-east-2", "s3-us-east-2.amazonaws.com")- , ("us-west-1", "s3-us-west-1.amazonaws.com")- , ("us-west-2", "s3-us-west-2.amazonaws.com")- , ("ca-central-1", "s3-ca-central-1.amazonaws.com")- , ("ap-south-1", "s3-ap-south-1.amazonaws.com")- , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")- , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")- , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")- , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")- , ("eu-west-1", "s3-eu-west-1.amazonaws.com")- , ("eu-west-2", "s3-eu-west-2.amazonaws.com")- , ("eu-central-1", "s3-eu-central-1.amazonaws.com")- , ("sa-east-1", "s3-sa-east-1.amazonaws.com")- ]---- | Connection Info data type. To create a 'ConnectInfo' value,--- enable the @OverloadedStrings@ language extension and use the--- `IsString` instance to provide a URL, for example:------ > let c :: ConnectInfo = "https://play.min.io"-data ConnectInfo =- ConnectInfo { connectHost :: Text- , connectPort :: Int- , connectAccessKey :: Text- , connectSecretKey :: Text- , connectIsSecure :: Bool- , connectRegion :: Region- , connectAutoDiscoverRegion :: Bool- , connectDisableTLSCertValidation :: Bool- } deriving (Eq, Show)--instance IsString ConnectInfo where- fromString str =- let req = NC.parseRequest_ str- in ConnectInfo- { connectHost = TE.decodeUtf8 $ NC.host req- , connectPort = NC.port req- , connectAccessKey = ""- , connectSecretKey = ""- , connectIsSecure = NC.secure req- , connectRegion = ""- , connectAutoDiscoverRegion = True- , connectDisableTLSCertValidation = False- }---- | Contains access key and secret key to access object storage.-data Credentials = Credentials { cAccessKey :: Text- , cSecretKey :: Text- } deriving (Eq, Show)---- | A Provider is an action that may return Credentials. Providers--- may be chained together using 'findFirst'.-type Provider = IO (Maybe Credentials)---- | Combines the given list of providers, by calling each one in--- order until Credentials are found.-findFirst :: [Provider] -> Provider-findFirst [] = return Nothing-findFirst (f:fs) = do c <- f- maybe (findFirst fs) (return . Just) c---- | This Provider loads `Credentials` from @~\/.aws\/credentials@-fromAWSConfigFile :: Provider-fromAWSConfigFile = do- credsE <- runExceptT $ do- homeDir <- lift $ getHomeDirectory- let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"- fileExists <- lift $ doesFileExist awsCredsFile- bool (throwE "FileNotFound") (return ()) fileExists- ini <- ExceptT $ Ini.readIniFile awsCredsFile- akey <- ExceptT $ return- $ Ini.lookupValue "default" "aws_access_key_id" ini- skey <- ExceptT $ return- $ Ini.lookupValue "default" "aws_secret_access_key" ini- return $ Credentials akey skey- return $ hush credsE---- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and--- @AWS_SECRET_ACCESS_KEY@ environment variables.-fromAWSEnv :: Provider-fromAWSEnv = runMaybeT $ do- akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"- skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"- return $ Credentials (T.pack akey) (T.pack skey)---- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and--- @MINIO_SECRET_KEY@ environment variables.-fromMinioEnv :: Provider-fromMinioEnv = runMaybeT $ do- akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"- skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"- return $ Credentials (T.pack akey) (T.pack skey)---- | setCredsFrom retrieves access credentials from the first--- `Provider` form the given list that succeeds and sets it in the--- `ConnectInfo`.-setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo-setCredsFrom ps ci = do pMay <- findFirst ps- maybe- (throwIO MErrVMissingCredentials)- (return . (flip setCreds ci))- pMay---- | setCreds sets the given `Credentials` in the `ConnectInfo`.-setCreds :: Credentials -> ConnectInfo -> ConnectInfo-setCreds (Credentials accessKey secretKey) connInfo =- connInfo { connectAccessKey = accessKey- , connectSecretKey = secretKey- }---- | Set the S3 region parameter in the `ConnectInfo`-setRegion :: Region -> ConnectInfo -> ConnectInfo-setRegion r connInfo = connInfo { connectRegion = r- , connectAutoDiscoverRegion = False- }---- | Check if the connection to object storage server is secure--- (i.e. uses TLS)-isConnectInfoSecure :: ConnectInfo -> Bool-isConnectInfoSecure = connectIsSecure---- | Disable TLS certificate validation completely! This makes TLS--- insecure! Use only for testing with self-signed or temporary--- certificates. Note that this option has no effect, if you provide--- your own Manager in `mkMinioConn`.-disableTLSCertValidation :: ConnectInfo -> ConnectInfo-disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }--getHostAddr :: ConnectInfo -> ByteString-getHostAddr ci = if | port == 80 || port == 443 -> toS host- | otherwise -> toS $- T.concat [ host, ":" , Lib.Prelude.show port]- where- port = connectPort ci- host = connectHost ci----- | Default Google Compute Storage ConnectInfo. Works only for--- "Simple Migration" use-case with interoperability mode enabled on--- GCP console. For more information ---- https://cloud.google.com/storage/docs/migrating------ Credentials should be supplied before use.-gcsCI :: ConnectInfo-gcsCI = setRegion "us"- "https://storage.googleapis.com"----- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials--- should be supplied before use.-awsCI :: ConnectInfo-awsCI = "https://s3.amazonaws.com"----- | <https://play.min.io MinIO Play Server>--- ConnectInfo. Credentials are already filled in.-minioPlayCI :: ConnectInfo-minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"- in setCreds playCreds- $ setRegion "us-east-1"- "https://play.min.io"---- |--- Represents a bucket in the object store-type Bucket = Text---- |--- Represents an object name-type Object = Text---- | Represents a region-type Region = Text---- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.-type ETag = Text---- | Data type to represent an object encryption key. Create one using--- the `mkSSECKey` function.-newtype SSECKey = SSECKey BA.ScrubbedBytes- deriving (Eq, Show)---- | Validates that the given ByteString is 32 bytes long and creates--- an encryption key.-mkSSECKey :: MonadThrow m => ByteString -> m SSECKey-mkSSECKey keyBytes | B.length keyBytes /= 32 =- throwM MErrVInvalidEncryptionKeyLength- | otherwise =- return $ SSECKey $ BA.convert keyBytes---- | Data type to represent Server-Side-Encryption settings-data SSE where- -- | Specifies SSE S3 encryption - server manages encryption keys- SSE :: SSE- -- | Specifies that KMS service should be used. The first argument- -- to the constructor is the Key Id to be used by the server (if- -- not specified, the default KMS key id is used). The second- -- argument is the optional KMS context that must have a- -- `A.ToJSON` instance - please refer to the AWS S3 documentation- -- for detailed information.- SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE- -- | Specifies server-side encryption with customer provided- -- key. The argument is the encryption key to be used.- SSEC :: SSECKey -> SSE--toPutObjectHeaders :: SSE -> [HT.Header]-toPutObjectHeaders sseArg =- let sseHeader = "x-amz-server-side-encryption"- sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"- sseKmsContextHeader = sseHeader <> "-context"- ssecAlgo = sseHeader <> "-customer-algorithm"- ssecKey = sseHeader <> "-customer-key"- ssecKeyMD5 = ssecKey <> "-MD5"-- in case sseArg of- SSE -> [(sseHeader, "AES256")]-- SSEKMS keyIdMay ctxMay ->- [(sseHeader, "aws:kms")] ++- maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay ++- maybe [] (\k -> [(sseKmsContextHeader, toS $ A.encode k)]) ctxMay-- SSEC (SSECKey sb) ->- [(ssecAlgo, "AES256"),- (ssecKey, encodeToBase64 sb),- (ssecKeyMD5, hashMD5ToBase64 sb)]---- | Data type for options in PutObject call. Start with the empty--- `defaultPutObjectOptions` and use various the various poo*--- accessors.-data PutObjectOptions = PutObjectOptions {- -- | Set a standard MIME type describing the format of the object.- pooContentType :: Maybe Text- -- | Set what content encodings have been applied to the object and thus- -- what decoding mechanisms must be applied to obtain the media-type- -- referenced by the Content-Type header field.- , pooContentEncoding :: Maybe Text- -- | Set presentational information for the object.- , pooContentDisposition :: Maybe Text- -- | Set to specify caching behavior for the object along the- -- request/reply chain.- , pooCacheControl :: Maybe Text- -- | Set to describe the language(s) intended for the audience.- , pooContentLanguage :: Maybe Text- -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your- -- performance needs, storage class is @STANDARD@ by default (i.e- -- when Nothing is passed).- , pooStorageClass :: Maybe Text- -- | Set user defined metadata to store with the object.- , pooUserMetadata :: [(Text, Text)]- -- | Set number of worker threads used to upload an object.- , pooNumThreads :: Maybe Word- -- | Set object encryption parameters for the request.- , pooSSE :: Maybe SSE- }---- | Provide default `PutObjectOptions`.-defaultPutObjectOptions :: PutObjectOptions-defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing---- | If the given header name has the @X-Amz-Meta-@ prefix, it is--- stripped and a Just is returned.-userMetadataHeaderNameMaybe :: Text -> Maybe Text-userMetadataHeaderNameMaybe k =- let prefix = T.toCaseFold "X-Amz-Meta-"- n = T.length prefix- in if T.toCaseFold (T.take n k) == prefix- then Just (T.drop n k)- else Nothing--addXAmzMetaPrefix :: Text -> Text-addXAmzMetaPrefix s | isJust (userMetadataHeaderNameMaybe s) = s- | otherwise = "X-Amz-Meta-" <> s--mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]-mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ x, encodeUtf8 y))--pooToHeaders :: PutObjectOptions -> [HT.Header]-pooToHeaders poo = userMetadata- ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))- ++ maybe [] toPutObjectHeaders (pooSSE poo)- where- tupToMaybe (k, Just v) = Just (k, v)- tupToMaybe (_, Nothing) = Nothing-- userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo-- names = ["content-type",- "content-encoding",- "content-disposition",- "content-language",- "cache-control",- "x-amz-storage-class"]- values = map (fmap encodeUtf8 . (poo &))- [pooContentType, pooContentEncoding,- pooContentDisposition, pooContentLanguage,- pooCacheControl, pooStorageClass]----- |--- BucketInfo returned for list buckets call-data BucketInfo = BucketInfo {- biName :: Bucket- , biCreationDate :: UTCTime- } deriving (Show, Eq)---- | A type alias to represent a part-number for multipart upload-type PartNumber = Int16---- | A type alias to represent an upload-id for multipart upload-type UploadId = Text---- | A type to represent a part-number and etag.-type PartTuple = (PartNumber, ETag)---- | Represents result from a listing of object parts of an ongoing--- multipart upload.-data ListPartsResult = ListPartsResult {- lprHasMore :: Bool- , lprNextPart :: Maybe Int- , lprParts :: [ObjectPartInfo]- } deriving (Show, Eq)---- | Represents information about an object part in an ongoing--- multipart upload.-data ObjectPartInfo = ObjectPartInfo {- opiNumber :: PartNumber- , opiETag :: ETag- , opiSize :: Int64- , opiModTime :: UTCTime- } deriving (Show, Eq)---- | Represents result from a listing of incomplete uploads to a--- bucket.-data ListUploadsResult = ListUploadsResult {- lurHasMore :: Bool- , lurNextKey :: Maybe Text- , lurNextUpload :: Maybe Text- , lurUploads :: [(Object, UploadId, UTCTime)]- , lurCPrefixes :: [Text]- } deriving (Show, Eq)---- | Represents information about a multipart upload.-data UploadInfo = UploadInfo {- uiKey :: Object- , uiUploadId :: UploadId- , uiInitTime :: UTCTime- , uiSize :: Int64- } deriving (Show, Eq)---- | Represents result from a listing of objects in a bucket.-data ListObjectsResult = ListObjectsResult {- lorHasMore :: Bool- , lorNextToken :: Maybe Text- , lorObjects :: [ObjectInfo]- , 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 -- ^ Object key- , oiModTime :: UTCTime -- ^ Modification time of the object- , oiETag :: ETag -- ^ ETag of the object- , oiSize :: Int64 -- ^ Size of the object in bytes- , oiUserMetadata :: H.HashMap Text Text -- ^ A map of user-metadata- -- pairs stored with an- -- object (keys will not- -- have the @X-Amz-Meta-@- -- prefix).- , oiMetadata :: H.HashMap Text Text -- ^ A map of metadata- -- key-value pairs (not- -- including the- -- user-metadata pairs)- } deriving (Show, Eq)---- | Represents source object in server-side copy object-data SourceInfo = SourceInfo- { srcBucket :: Text -- ^ Bucket containing the source object- , srcObject :: Text -- ^ Source object key- , srcRange :: Maybe (Int64, Int64) -- ^ Source object- -- byte-range- -- (inclusive)- , srcIfMatch :: Maybe Text -- ^ ETag condition on source -- -- object is copied only if the- -- source object's ETag matches- -- this value.- , srcIfNoneMatch :: Maybe Text -- ^ ETag not match condition- -- on source - object is copied- -- if ETag does not match this- -- value.- , srcIfModifiedSince :: Maybe UTCTime -- ^ Copy source object only- -- if the source has been- -- modified since this time.- , srcIfUnmodifiedSince :: Maybe UTCTime -- ^ Copy source object only- -- if the source has been- -- un-modified since this- -- given time.- } deriving (Show, Eq)---- | Provide a default for `SourceInfo`-defaultSourceInfo :: SourceInfo-defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing---- | Represents destination object in server-side copy object-data DestinationInfo = DestinationInfo- { dstBucket :: Text -- ^ Destination bucket- , dstObject :: Text -- ^ Destination object key- } deriving (Show, Eq)---- | Provide a default for `DestinationInfo`-defaultDestinationInfo :: DestinationInfo-defaultDestinationInfo = DestinationInfo "" ""---- | Data type for options when getting an object from the--- service. Start with the empty `defaultGetObjectOptions` and modify--- it using the goo* functions.-data GetObjectOptions = GetObjectOptions {- -- | Set object's data of given offset begin and end,- -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.- gooRange :: Maybe ByteRange- -- | Set matching ETag condition, GetObject which matches the following- -- ETag.- , gooIfMatch :: Maybe ETag- -- | Set matching ETag none condition, GetObject which does not match- -- the following ETag.- , gooIfNoneMatch :: Maybe ETag- -- | Set object unmodified condition, GetObject unmodified since given time.- , gooIfUnmodifiedSince :: Maybe UTCTime- -- | Set object modified condition, GetObject modified since given time.- , gooIfModifiedSince :: Maybe UTCTime- -- | Specify SSE-C key- , gooSSECKey :: Maybe SSECKey- }---- | Provide default `GetObjectOptions`.-defaultGetObjectOptions :: GetObjectOptions-defaultGetObjectOptions =- GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing--gooToHeaders :: GetObjectOptions -> [HT.Header]-gooToHeaders goo = rangeHdr ++ zip names values- ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)- where- names = ["If-Match",- "If-None-Match",- "If-Unmodified-Since",- "If-Modified-Since"]- values = mapMaybe (fmap encodeUtf8 . (goo &))- [gooIfMatch, gooIfNoneMatch,- fmap formatRFC1123 . gooIfUnmodifiedSince,- fmap formatRFC1123 . gooIfModifiedSince]- rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])- $ gooRange goo---- | Data type returned by 'getObject' representing the object being--- retrieved. Use the @gor*@ functions to access its contents.-data GetObjectResponse = GetObjectResponse {- -- | ObjectInfo of the object being retrieved.- gorObjectInfo :: ObjectInfo- -- | A conduit of the bytes of the object.- , gorObjectStream :: C.ConduitM () ByteString Minio ()- }---- | 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----- | Filter data type - part of notification configuration-data Filter = Filter- { fFilter :: FilterKey- } deriving (Show, Eq)---- | defaultFilter is empty, used to create a notification--- configuration.-defaultFilter :: Filter-defaultFilter = Filter defaultFilterKey---- | FilterKey contains FilterRules, and is part of a Filter.-data FilterKey = FilterKey- { fkKey :: FilterRules- } deriving (Show, Eq)---- | defaultFilterKey is empty, used to create notification--- configuration.-defaultFilterKey :: FilterKey-defaultFilterKey = FilterKey defaultFilterRules---- | FilterRules represents a collection of `FilterRule`s.-data FilterRules = FilterRules- { frFilterRules :: [FilterRule]- } deriving (Show, Eq)---- | defaultFilterRules is empty, used to create notification--- configuration.-defaultFilterRules :: FilterRules-defaultFilterRules = 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)---- | Arn is an alias of Text-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)---- | The default notification configuration is empty.-defaultNotification :: Notification-defaultNotification = Notification [] [] []-------------------------------------------------------------------------------- Select API Related Types------------------------------------------------------------------------------- | SelectRequest represents the Select API call. Use the--- `selectRequest` function to create a value of this type.-data SelectRequest = SelectRequest- { srExpression :: Text- , srExpressionType :: ExpressionType- , srInputSerialization :: InputSerialization- , srOutputSerialization :: OutputSerialization- , srRequestProgressEnabled :: Maybe Bool- } deriving (Eq, Show)--data ExpressionType = SQL- deriving (Eq, Show)---- | InputSerialization represents format information of the input--- object being queried. Use one of the smart constructors such as--- `defaultCsvInput` as a starting value, and add compression info--- using `setInputCompressionType`-data InputSerialization = InputSerialization- { isCompressionType :: Maybe CompressionType- , isFormatInfo :: InputFormatInfo- } deriving (Eq, Show)---- | Data type representing the compression setting in a Select--- request.-data CompressionType = CompressionTypeNone- | CompressionTypeGzip- | CompressionTypeBzip2- deriving (Eq, Show)---- | Data type representing input object format information in a--- Select request.-data InputFormatInfo = InputFormatCSV CSVInputProp- | InputFormatJSON JSONInputProp- | InputFormatParquet- deriving (Eq, Show)---- | defaultCsvInput returns InputSerialization with default CSV--- format, and without any compression setting.-defaultCsvInput :: InputSerialization-defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)---- | linesJsonInput returns InputSerialization with JSON line based--- format with no compression setting.-linesJsonInput :: InputSerialization-linesJsonInput = InputSerialization Nothing- (InputFormatJSON $ JSONInputProp JSONTypeLines)---- | documentJsonInput returns InputSerialization with JSON document--- based format with no compression setting.-documentJsonInput :: InputSerialization-documentJsonInput = InputSerialization Nothing- (InputFormatJSON $ JSONInputProp JSONTypeDocument)---- | defaultParquetInput returns InputSerialization with Parquet--- format, and no compression setting.-defaultParquetInput :: InputSerialization-defaultParquetInput = InputSerialization Nothing InputFormatParquet---- | setInputCompressionType sets the compression type for the input--- of the SelectRequest-setInputCompressionType :: CompressionType -> SelectRequest- -> SelectRequest-setInputCompressionType c i =- let is = srInputSerialization i- is' = is { isCompressionType = Just c }- in i { srInputSerialization = is' }---- | defaultCsvOutput returns OutputSerialization with default CSV--- format.-defaultCsvOutput :: OutputSerialization-defaultCsvOutput = OutputSerializationCSV defaultCSVProp---- | defaultJsonInput returns OutputSerialization with default JSON--- format.-defaultJsonOutput :: OutputSerialization-defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)---- | selectRequest is used to build a `SelectRequest`--- value. @selectRequest query inputSer outputSer@ represents a--- SelectRequest with the SQL query text given by @query@, the input--- serialization settings (compression format and format information)--- @inputSer@ and the output serialization settings @outputSer@.-selectRequest :: Text -> InputSerialization -> OutputSerialization- -> SelectRequest-selectRequest sqlQuery inputSer outputSer =- SelectRequest { srExpression = sqlQuery- , srExpressionType = SQL- , srInputSerialization = inputSer- , srOutputSerialization = outputSer- , srRequestProgressEnabled = Nothing- }---- | setRequestProgressEnabled sets the flag for turning on progress--- messages when the Select response is being streamed back to the--- client.-setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest-setRequestProgressEnabled enabled sr =- sr { srRequestProgressEnabled = Just enabled }--type CSVInputProp = CSVProp---- | CSVProp represents CSV format properties. It is built up using--- the Monoid instance.-data CSVProp = CSVProp (H.HashMap Text Text)- deriving (Eq, Show)--#if (__GLASGOW_HASKELL__ >= 804)-instance Semigroup CSVProp where- (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)-#endif--instance Monoid CSVProp where- mempty = CSVProp mempty-#if (__GLASGOW_HASKELL__ < 804)- mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)-#endif--defaultCSVProp :: CSVProp-defaultCSVProp = mempty---- | Specify the CSV record delimiter property.-recordDelimiter :: Text -> CSVProp-recordDelimiter = CSVProp . H.singleton "RecordDelimiter"---- | Specify the CSV field delimiter property.-fieldDelimiter :: Text -> CSVProp-fieldDelimiter = CSVProp . H.singleton "FieldDelimiter"---- | Specify the CSV quote character property.-quoteCharacter :: Text -> CSVProp-quoteCharacter = CSVProp . H.singleton "QuoteCharacter"---- | Specify the CSV quote-escape character property.-quoteEscapeCharacter :: Text -> CSVProp-quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"---- | FileHeaderInfo specifies information about column headers for CSV--- format.-data FileHeaderInfo- = FileHeaderNone -- ^ No column headers are present- | FileHeaderUse -- ^ Headers are present and they should be used- | FileHeaderIgnore -- ^ Header are present, but should be ignored- deriving (Eq, Show)---- | Specify the CSV file header info property.-fileHeaderInfo :: FileHeaderInfo -> CSVProp-fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString- where- toString FileHeaderNone = "NONE"- toString FileHeaderUse = "USE"- toString FileHeaderIgnore = "IGNORE"---- | Specify the CSV comment character property. Lines starting with--- this character are ignored by the server.-commentCharacter :: Text -> CSVProp-commentCharacter = CSVProp . H.singleton "Comments"---- | Allow quoted record delimiters inside a row using this property.-allowQuotedRecordDelimiter :: CSVProp-allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter" "TRUE"---- | Set the CSV format properties in the InputSerialization.-setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization-setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }---- | Set the CSV format properties in the OutputSerialization.-outputCSVFromProps :: CSVProp -> OutputSerialization-outputCSVFromProps p = OutputSerializationCSV p--data JSONInputProp = JSONInputProp { jsonipType :: JSONType }- deriving (Eq, Show)--data JSONType = JSONTypeDocument | JSONTypeLines- deriving (Eq, Show)----- | OutputSerialization represents output serialization settings for--- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as--- a starting point.-data OutputSerialization = OutputSerializationJSON JSONOutputProp- | OutputSerializationCSV CSVOutputProp- deriving (Eq, Show)--type CSVOutputProp = CSVProp---- | quoteFields is an output serialization parameter-quoteFields :: QuoteFields -> CSVProp-quoteFields q = CSVProp $ H.singleton "QuoteFields" $- case q of- QuoteFieldsAsNeeded -> "ASNEEDED"- QuoteFieldsAlways -> "ALWAYS"---- | Represent the QuoteField setting.-data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways- deriving (Eq, Show)--data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }- deriving (Eq, Show)---- | Set the output record delimiter for JSON format-outputJSONFromRecordDelimiter :: Text -> OutputSerialization-outputJSONFromRecordDelimiter t =- OutputSerializationJSON (JSONOutputProp $ Just t)---- Response related types---- | An EventMessage represents each kind of message received from the server.-data EventMessage = ProgressEventMessage { emProgress :: Progress }- | StatsEventMessage { emStats :: Stats }- | RequestLevelErrorMessage { emErrorCode :: Text- , emErrorMessage :: Text- }- | RecordPayloadEventMessage { emPayloadBytes :: ByteString }- deriving (Eq, Show)--data MsgHeaderName = MessageType- | EventType- | ContentType- | ErrorCode- | ErrorMessage- deriving (Eq, Show)--msgHeaderValueType :: Word8-msgHeaderValueType = 7--type MessageHeader = (MsgHeaderName, Text)---- | Represent the progress event returned in the Select response.-data Progress = Progress { pBytesScanned :: Int64- , pBytesProcessed :: Int64- , pBytesReturned :: Int64- }- deriving (Eq, Show)---- | Represent the stats event returned at the end of the Select--- response.-type Stats = Progress------------------------------------------------------------------------------- Select API Related Types End------------------------------------------------------------------------------- | Represents different kinds of payload that are used with S3 API--- requests.-data Payload- = PayloadBS ByteString- | PayloadH Handle Int64 Int64 -- file handle, offset and length- | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source--defaultPayload :: Payload-defaultPayload = PayloadBS ""--data AdminReqInfo = AdminReqInfo {- ariMethod :: Method- , ariPayloadHash :: Maybe ByteString- , ariPayload :: Payload- , ariPath :: ByteString- , ariHeaders :: [Header]- , ariQueryParams :: Query- }--data S3ReqInfo = S3ReqInfo- { riMethod :: Method- , riBucket :: Maybe Bucket- , riObject :: Maybe Object- , riQueryParams :: Query- , riHeaders :: [Header]- , riPayload :: Payload- , riPayloadHash :: Maybe ByteString- , riRegion :: Maybe Region- , riNeedsLocation :: Bool- }--defaultS3ReqInfo :: S3ReqInfo-defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing- [] [] defaultPayload Nothing Nothing True--getS3Path :: Maybe Bucket -> Maybe Object -> ByteString-getS3Path b o =- let 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 = H.HashMap Bucket Region---- | The Minio Monad - all computations accessing object storage--- happens in it.-newtype Minio a = Minio {- unMinio :: ReaderT MinioConn (ResourceT IO) a- }- deriving (- Functor- , Applicative- , Monad- , MonadIO- , MonadReader MinioConn- , MonadResource- )--instance MonadUnliftIO Minio where- askUnliftIO = Minio $ ReaderT $ \r ->- withUnliftIO $ \u ->- return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))---- | MinioConn holds connection info and a connection pool to allow--- for efficient resource re-use.-data MinioConn = MinioConn- { mcConnInfo :: ConnectInfo- , mcConnManager :: NC.Manager- , mcRegionMap :: MVar RegionMap- }--class HasSvcNamespace env where- getSvcNamespace :: env -> Text--instance HasSvcNamespace MinioConn where- getSvcNamespace env = let host = connectHost $ mcConnInfo env- in if | host == "storage.googleapis.com" ->- "http://doc.s3.amazonaws.com/2006-03-01"- | otherwise ->- "http://s3.amazonaws.com/doc/2006-03-01/"---- | Takes connection information and returns a connection object to--- be passed to 'runMinio'. The returned value can be kept in the--- application environment and passed to `runMinioWith` whenever--- object storage is accessed.-connect :: ConnectInfo -> IO MinioConn-connect ci = do- let settings | connectIsSecure ci && connectDisableTLSCertValidation ci =- let badTlsSettings = Conn.TLSSettingsSimple True False False- in TLS.mkManagerSettings badTlsSettings Nothing- | connectIsSecure ci = NC.tlsManagerSettings- | otherwise = defaultManagerSettings- mgr <- NC.newManager settings- mkMinioConn ci mgr---- | Run the computation accessing object storage using the given--- `MinioConn`. This reuses connections, but otherwise it is similar--- to `runMinio`.-runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)-runMinioWith conn m = runResourceT $ runMinioResWith conn m---- | Given `ConnectInfo` and a HTTP connection manager, create a--- `MinioConn`.-mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn-mkMinioConn ci mgr = do- rMapMVar <- M.newMVar H.empty- return $ MinioConn ci mgr rMapMVar---- | Run the Minio action and return the result or an error.-runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)-runMinio ci m = do- conn <- connect ci- runResourceT $ runMinioResWith conn m---- | Similar to 'runMinioWith'. Allows applications to allocate/release--- its resources along side MinIO's internal resources.-runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)-runMinioResWith conn m =- flip runReaderT conn . unMinio $- fmap Right m `U.catches`- [ U.Handler handlerServiceErr- , U.Handler handlerHE- , U.Handler handlerFE- , U.Handler handlerValidation- ]+{-# LANGUAGE CPP #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies #-}++module Network.Minio.Data where++import qualified Conduit as C+import qualified Control.Concurrent.MVar as M+import Control.Monad.Trans.Resource+import qualified Data.Aeson as A+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as LB+import Data.CaseInsensitive (mk)+import qualified Data.HashMap.Strict as H+import qualified Data.Ini as Ini+import Data.String (IsString (..))+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Time (defaultTimeLocale, formatTime)+import GHC.Show (Show (show))+import Lib.Prelude+import qualified Network.Connection as Conn+import Network.HTTP.Client (defaultManagerSettings)+import qualified Network.HTTP.Client.TLS as TLS+import qualified Network.HTTP.Conduit as NC+import Network.HTTP.Types+ ( ByteRange,+ Header,+ Method,+ Query,+ hRange,+ )+import qualified Network.HTTP.Types as HT+import Network.Minio.Data.Crypto+import Network.Minio.Errors+import System.Directory (doesFileExist, getHomeDirectory)+import qualified System.Environment as Env+import System.FilePath.Posix (combine)+import Text.XML+import qualified UnliftIO as U++-- | max obj size is 5TiB+maxObjectSize :: Int64+maxObjectSize = 5 * 1024 * 1024 * oneMiB++-- | minimum size of parts used in multipart operations.+minPartSize :: Int64+minPartSize = 64 * oneMiB++oneMiB :: Int64+oneMiB = 1024 * 1024++-- | maximum number of parts that can be uploaded for a single object.+maxMultipartParts :: Int64+maxMultipartParts = 10000++-- TODO: Add a type which provides typed constants for region. this+-- type should have a IsString instance to infer the appropriate+-- constant.++-- | awsRegionMap - library constant+awsRegionMap :: H.HashMap Text Text+awsRegionMap =+ H.fromList+ [ ("us-east-1", "s3.amazonaws.com"),+ ("us-east-2", "s3-us-east-2.amazonaws.com"),+ ("us-west-1", "s3-us-west-1.amazonaws.com"),+ ("us-west-2", "s3-us-west-2.amazonaws.com"),+ ("ca-central-1", "s3-ca-central-1.amazonaws.com"),+ ("ap-south-1", "s3-ap-south-1.amazonaws.com"),+ ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com"),+ ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com"),+ ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com"),+ ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com"),+ ("eu-west-1", "s3-eu-west-1.amazonaws.com"),+ ("eu-west-2", "s3-eu-west-2.amazonaws.com"),+ ("eu-central-1", "s3-eu-central-1.amazonaws.com"),+ ("sa-east-1", "s3-sa-east-1.amazonaws.com")+ ]++-- | Connection Info data type. To create a 'ConnectInfo' value,+-- enable the @OverloadedStrings@ language extension and use the+-- `IsString` instance to provide a URL, for example:+--+-- > let c :: ConnectInfo = "https://play.min.io"+data ConnectInfo = ConnectInfo+ { connectHost :: Text,+ connectPort :: Int,+ connectAccessKey :: Text,+ connectSecretKey :: Text,+ connectIsSecure :: Bool,+ connectRegion :: Region,+ connectAutoDiscoverRegion :: Bool,+ connectDisableTLSCertValidation :: Bool+ }+ deriving (Eq, Show)++instance IsString ConnectInfo where+ fromString str =+ let req = NC.parseRequest_ str+ in ConnectInfo+ { connectHost = TE.decodeUtf8 $ NC.host req,+ connectPort = NC.port req,+ connectAccessKey = "",+ connectSecretKey = "",+ connectIsSecure = NC.secure req,+ connectRegion = "",+ connectAutoDiscoverRegion = True,+ connectDisableTLSCertValidation = False+ }++-- | Contains access key and secret key to access object storage.+data Credentials = Credentials+ { cAccessKey :: Text,+ cSecretKey :: Text+ }+ deriving (Eq, Show)++-- | A Provider is an action that may return Credentials. Providers+-- may be chained together using 'findFirst'.+type Provider = IO (Maybe Credentials)++-- | Combines the given list of providers, by calling each one in+-- order until Credentials are found.+findFirst :: [Provider] -> Provider+findFirst [] = return Nothing+findFirst (f : fs) = do+ c <- f+ maybe (findFirst fs) (return . Just) c++-- | This Provider loads `Credentials` from @~\/.aws\/credentials@+fromAWSConfigFile :: Provider+fromAWSConfigFile = do+ credsE <- runExceptT $ do+ homeDir <- lift $ getHomeDirectory+ let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"+ fileExists <- lift $ doesFileExist awsCredsFile+ bool (throwE "FileNotFound") (return ()) fileExists+ ini <- ExceptT $ Ini.readIniFile awsCredsFile+ akey <-+ ExceptT $ return $+ Ini.lookupValue "default" "aws_access_key_id" ini+ skey <-+ ExceptT $ return $+ Ini.lookupValue "default" "aws_secret_access_key" ini+ return $ Credentials akey skey+ return $ hush credsE++-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and+-- @AWS_SECRET_ACCESS_KEY@ environment variables.+fromAWSEnv :: Provider+fromAWSEnv = runMaybeT $ do+ akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"+ skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"+ return $ Credentials (T.pack akey) (T.pack skey)++-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and+-- @MINIO_SECRET_KEY@ environment variables.+fromMinioEnv :: Provider+fromMinioEnv = runMaybeT $ do+ akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"+ skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"+ return $ Credentials (T.pack akey) (T.pack skey)++-- | setCredsFrom retrieves access credentials from the first+-- `Provider` form the given list that succeeds and sets it in the+-- `ConnectInfo`.+setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo+setCredsFrom ps ci = do+ pMay <- findFirst ps+ maybe+ (throwIO MErrVMissingCredentials)+ (return . (flip setCreds ci))+ pMay++-- | setCreds sets the given `Credentials` in the `ConnectInfo`.+setCreds :: Credentials -> ConnectInfo -> ConnectInfo+setCreds (Credentials accessKey secretKey) connInfo =+ connInfo+ { connectAccessKey = accessKey,+ connectSecretKey = secretKey+ }++-- | Set the S3 region parameter in the `ConnectInfo`+setRegion :: Region -> ConnectInfo -> ConnectInfo+setRegion r connInfo =+ connInfo+ { connectRegion = r,+ connectAutoDiscoverRegion = False+ }++-- | Check if the connection to object storage server is secure+-- (i.e. uses TLS)+isConnectInfoSecure :: ConnectInfo -> Bool+isConnectInfoSecure = connectIsSecure++-- | Disable TLS certificate validation completely! This makes TLS+-- insecure! Use only for testing with self-signed or temporary+-- certificates. Note that this option has no effect, if you provide+-- your own Manager in `mkMinioConn`.+disableTLSCertValidation :: ConnectInfo -> ConnectInfo+disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}++getHostAddr :: ConnectInfo -> ByteString+getHostAddr ci =+ if+ | port == 80 || port == 443 -> toUtf8 host+ | otherwise ->+ toUtf8 $+ T.concat [host, ":", Lib.Prelude.show port]+ where+ port = connectPort ci+ host = connectHost ci++-- | Default Google Compute Storage ConnectInfo. Works only for+-- "Simple Migration" use-case with interoperability mode enabled on+-- GCP console. For more information -+-- https://cloud.google.com/storage/docs/migrating+--+-- Credentials should be supplied before use.+gcsCI :: ConnectInfo+gcsCI =+ setRegion+ "us"+ "https://storage.googleapis.com"++-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials+-- should be supplied before use.+awsCI :: ConnectInfo+awsCI = "https://s3.amazonaws.com"++-- | <https://play.min.io MinIO Play Server>+-- ConnectInfo. Credentials are already filled in.+minioPlayCI :: ConnectInfo+minioPlayCI =+ let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"+ in setCreds playCreds $+ setRegion+ "us-east-1"+ "https://play.min.io"++-- |+-- Represents a bucket in the object store+type Bucket = Text++-- |+-- Represents an object name+type Object = Text++-- | Represents a region+type Region = Text++-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.+type ETag = Text++-- | Data type to represent an object encryption key. Create one using+-- the `mkSSECKey` function.+newtype SSECKey = SSECKey BA.ScrubbedBytes+ deriving (Eq, Show)++-- | Validates that the given ByteString is 32 bytes long and creates+-- an encryption key.+mkSSECKey :: MonadThrow m => ByteString -> m SSECKey+mkSSECKey keyBytes+ | B.length keyBytes /= 32 =+ throwM MErrVInvalidEncryptionKeyLength+ | otherwise =+ return $ SSECKey $ BA.convert keyBytes++-- | Data type to represent Server-Side-Encryption settings+data SSE where+ -- | Specifies SSE S3 encryption - server manages encryption keys+ SSE :: SSE+ -- | Specifies that KMS service should be used. The first argument+ -- to the constructor is the Key Id to be used by the server (if+ -- not specified, the default KMS key id is used). The second+ -- argument is the optional KMS context that must have a+ -- `A.ToJSON` instance - please refer to the AWS S3 documentation+ -- for detailed information.+ SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE+ -- | Specifies server-side encryption with customer provided+ -- key. The argument is the encryption key to be used.+ SSEC :: SSECKey -> SSE++toPutObjectHeaders :: SSE -> [HT.Header]+toPutObjectHeaders sseArg =+ let sseHeader = "x-amz-server-side-encryption"+ sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"+ sseKmsContextHeader = sseHeader <> "-context"+ ssecAlgo = sseHeader <> "-customer-algorithm"+ ssecKey = sseHeader <> "-customer-key"+ ssecKeyMD5 = ssecKey <> "-MD5"+ in case sseArg of+ SSE -> [(sseHeader, "AES256")]+ SSEKMS keyIdMay ctxMay ->+ [(sseHeader, "aws:kms")]+ ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay+ ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay+ SSEC (SSECKey sb) ->+ [ (ssecAlgo, "AES256"),+ (ssecKey, encodeToBase64 sb),+ (ssecKeyMD5, hashMD5ToBase64 sb)+ ]++-- | Data type for options in PutObject call. Start with the empty+-- `defaultPutObjectOptions` and use various the various poo*+-- accessors.+data PutObjectOptions = PutObjectOptions+ { -- | Set a standard MIME type describing the format of the object.+ pooContentType :: Maybe Text,+ -- | Set what content encodings have been applied to the object and thus+ -- what decoding mechanisms must be applied to obtain the media-type+ -- referenced by the Content-Type header field.+ pooContentEncoding :: Maybe Text,+ -- | Set presentational information for the object.+ pooContentDisposition :: Maybe Text,+ -- | Set to specify caching behavior for the object along the+ -- request/reply chain.+ pooCacheControl :: Maybe Text,+ -- | Set to describe the language(s) intended for the audience.+ pooContentLanguage :: Maybe Text,+ -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your+ -- performance needs, storage class is @STANDARD@ by default (i.e+ -- when Nothing is passed).+ pooStorageClass :: Maybe Text,+ -- | Set user defined metadata to store with the object.+ pooUserMetadata :: [(Text, Text)],+ -- | Set number of worker threads used to upload an object.+ pooNumThreads :: Maybe Word,+ -- | Set object encryption parameters for the request.+ pooSSE :: Maybe SSE+ }++-- | Provide default `PutObjectOptions`.+defaultPutObjectOptions :: PutObjectOptions+defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing++-- | If the given header name has the @X-Amz-Meta-@ prefix, it is+-- stripped and a Just is returned.+userMetadataHeaderNameMaybe :: Text -> Maybe Text+userMetadataHeaderNameMaybe k =+ let prefix = T.toCaseFold "X-Amz-Meta-"+ n = T.length prefix+ in if T.toCaseFold (T.take n k) == prefix+ then Just (T.drop n k)+ else Nothing++addXAmzMetaPrefix :: Text -> Text+addXAmzMetaPrefix s+ | isJust (userMetadataHeaderNameMaybe s) = s+ | otherwise = "X-Amz-Meta-" <> s++mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]+mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ x, encodeUtf8 y))++pooToHeaders :: PutObjectOptions -> [HT.Header]+pooToHeaders poo =+ userMetadata+ ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))+ ++ maybe [] toPutObjectHeaders (pooSSE poo)+ where+ tupToMaybe (k, Just v) = Just (k, v)+ tupToMaybe (_, Nothing) = Nothing+ userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo+ names =+ [ "content-type",+ "content-encoding",+ "content-disposition",+ "content-language",+ "cache-control",+ "x-amz-storage-class"+ ]+ values =+ map+ (fmap encodeUtf8 . (poo &))+ [ pooContentType,+ pooContentEncoding,+ pooContentDisposition,+ pooContentLanguage,+ pooCacheControl,+ pooStorageClass+ ]++-- |+-- BucketInfo returned for list buckets call+data BucketInfo = BucketInfo+ { biName :: Bucket,+ biCreationDate :: UTCTime+ }+ deriving (Show, Eq)++-- | A type alias to represent a part-number for multipart upload+type PartNumber = Int16++-- | A type alias to represent an upload-id for multipart upload+type UploadId = Text++-- | A type to represent a part-number and etag.+type PartTuple = (PartNumber, ETag)++-- | Represents result from a listing of object parts of an ongoing+-- multipart upload.+data ListPartsResult = ListPartsResult+ { lprHasMore :: Bool,+ lprNextPart :: Maybe Int,+ lprParts :: [ObjectPartInfo]+ }+ deriving (Show, Eq)++-- | Represents information about an object part in an ongoing+-- multipart upload.+data ObjectPartInfo = ObjectPartInfo+ { opiNumber :: PartNumber,+ opiETag :: ETag,+ opiSize :: Int64,+ opiModTime :: UTCTime+ }+ deriving (Show, Eq)++-- | Represents result from a listing of incomplete uploads to a+-- bucket.+data ListUploadsResult = ListUploadsResult+ { lurHasMore :: Bool,+ lurNextKey :: Maybe Text,+ lurNextUpload :: Maybe Text,+ lurUploads :: [(Object, UploadId, UTCTime)],+ lurCPrefixes :: [Text]+ }+ deriving (Show, Eq)++-- | Represents information about a multipart upload.+data UploadInfo = UploadInfo+ { uiKey :: Object,+ uiUploadId :: UploadId,+ uiInitTime :: UTCTime,+ uiSize :: Int64+ }+ deriving (Show, Eq)++-- | Represents result from a listing of objects in a bucket.+data ListObjectsResult = ListObjectsResult+ { lorHasMore :: Bool,+ lorNextToken :: Maybe Text,+ lorObjects :: [ObjectInfo],+ 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+ { -- | Object key+ oiObject :: Object,+ -- | Modification time of the object+ oiModTime :: UTCTime,+ -- | ETag of the object+ oiETag :: ETag,+ -- | Size of the object in bytes+ oiSize :: Int64,+ -- | A map of user-metadata+ -- pairs stored with an+ -- object (keys will not+ -- have the @X-Amz-Meta-@+ -- prefix).+ oiUserMetadata :: H.HashMap Text Text,+ -- | A map of metadata+ -- key-value pairs (not+ -- including the+ -- user-metadata pairs)+ oiMetadata :: H.HashMap Text Text+ }+ deriving (Show, Eq)++-- | Represents source object in server-side copy object+data SourceInfo = SourceInfo+ { -- | Bucket containing the source object+ srcBucket :: Text,+ -- | Source object key+ srcObject :: Text,+ -- | Source object+ -- byte-range+ -- (inclusive)+ srcRange :: Maybe (Int64, Int64),+ -- | ETag condition on source -+ -- object is copied only if the+ -- source object's ETag matches+ -- this value.+ srcIfMatch :: Maybe Text,+ -- | ETag not match condition+ -- on source - object is copied+ -- if ETag does not match this+ -- value.+ srcIfNoneMatch :: Maybe Text,+ -- | Copy source object only+ -- if the source has been+ -- modified since this time.+ srcIfModifiedSince :: Maybe UTCTime,+ -- | Copy source object only+ -- if the source has been+ -- un-modified since this+ -- given time.+ srcIfUnmodifiedSince :: Maybe UTCTime+ }+ deriving (Show, Eq)++-- | Provide a default for `SourceInfo`+defaultSourceInfo :: SourceInfo+defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing++-- | Represents destination object in server-side copy object+data DestinationInfo = DestinationInfo+ { -- | Destination bucket+ dstBucket :: Text,+ -- | Destination object key+ dstObject :: Text+ }+ deriving (Show, Eq)++-- | Provide a default for `DestinationInfo`+defaultDestinationInfo :: DestinationInfo+defaultDestinationInfo = DestinationInfo "" ""++-- | Data type for options when getting an object from the+-- service. Start with the empty `defaultGetObjectOptions` and modify+-- it using the goo* functions.+data GetObjectOptions = GetObjectOptions+ { -- | Set object's data of given offset begin and end,+ -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.+ gooRange :: Maybe ByteRange,+ -- | Set matching ETag condition, GetObject which matches the following+ -- ETag.+ gooIfMatch :: Maybe ETag,+ -- | Set matching ETag none condition, GetObject which does not match+ -- the following ETag.+ gooIfNoneMatch :: Maybe ETag,+ -- | Set object unmodified condition, GetObject unmodified since given time.+ gooIfUnmodifiedSince :: Maybe UTCTime,+ -- | Set object modified condition, GetObject modified since given time.+ gooIfModifiedSince :: Maybe UTCTime,+ -- | Specify SSE-C key+ gooSSECKey :: Maybe SSECKey+ }++-- | Provide default `GetObjectOptions`.+defaultGetObjectOptions :: GetObjectOptions+defaultGetObjectOptions =+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing++gooToHeaders :: GetObjectOptions -> [HT.Header]+gooToHeaders goo =+ rangeHdr ++ zip names values+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)+ where+ names =+ [ "If-Match",+ "If-None-Match",+ "If-Unmodified-Since",+ "If-Modified-Since"+ ]+ values =+ mapMaybe+ (fmap encodeUtf8 . (goo &))+ [ gooIfMatch,+ gooIfNoneMatch,+ fmap formatRFC1123 . gooIfUnmodifiedSince,+ fmap formatRFC1123 . gooIfModifiedSince+ ]+ rangeHdr =+ maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $+ gooRange goo++-- | Data type returned by 'getObject' representing the object being+-- retrieved. Use the @gor*@ functions to access its contents.+data GetObjectResponse = GetObjectResponse+ { -- | ObjectInfo of the object being retrieved.+ gorObjectInfo :: ObjectInfo,+ -- | A conduit of the bytes of the object.+ gorObjectStream :: C.ConduitM () ByteString Minio ()+ }++-- | 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++-- | Filter data type - part of notification configuration+data Filter = Filter+ { fFilter :: FilterKey+ }+ deriving (Show, Eq)++-- | defaultFilter is empty, used to create a notification+-- configuration.+defaultFilter :: Filter+defaultFilter = Filter defaultFilterKey++-- | FilterKey contains FilterRules, and is part of a Filter.+data FilterKey = FilterKey+ { fkKey :: FilterRules+ }+ deriving (Show, Eq)++-- | defaultFilterKey is empty, used to create notification+-- configuration.+defaultFilterKey :: FilterKey+defaultFilterKey = FilterKey defaultFilterRules++-- | FilterRules represents a collection of `FilterRule`s.+data FilterRules = FilterRules+ { frFilterRules :: [FilterRule]+ }+ deriving (Show, Eq)++-- | defaultFilterRules is empty, used to create notification+-- configuration.+defaultFilterRules :: FilterRules+defaultFilterRules = 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)++-- | Arn is an alias of Text+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)++-- | The default notification configuration is empty.+defaultNotification :: Notification+defaultNotification = Notification [] [] []++--------------------------------------------------------------------------+-- Select API Related Types+--------------------------------------------------------------------------++-- | SelectRequest represents the Select API call. Use the+-- `selectRequest` function to create a value of this type.+data SelectRequest = SelectRequest+ { srExpression :: Text,+ srExpressionType :: ExpressionType,+ srInputSerialization :: InputSerialization,+ srOutputSerialization :: OutputSerialization,+ srRequestProgressEnabled :: Maybe Bool+ }+ deriving (Eq, Show)++data ExpressionType = SQL+ deriving (Eq, Show)++-- | InputSerialization represents format information of the input+-- object being queried. Use one of the smart constructors such as+-- `defaultCsvInput` as a starting value, and add compression info+-- using `setInputCompressionType`+data InputSerialization = InputSerialization+ { isCompressionType :: Maybe CompressionType,+ isFormatInfo :: InputFormatInfo+ }+ deriving (Eq, Show)++-- | Data type representing the compression setting in a Select+-- request.+data CompressionType+ = CompressionTypeNone+ | CompressionTypeGzip+ | CompressionTypeBzip2+ deriving (Eq, Show)++-- | Data type representing input object format information in a+-- Select request.+data InputFormatInfo+ = InputFormatCSV CSVInputProp+ | InputFormatJSON JSONInputProp+ | InputFormatParquet+ deriving (Eq, Show)++-- | defaultCsvInput returns InputSerialization with default CSV+-- format, and without any compression setting.+defaultCsvInput :: InputSerialization+defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)++-- | linesJsonInput returns InputSerialization with JSON line based+-- format with no compression setting.+linesJsonInput :: InputSerialization+linesJsonInput =+ InputSerialization+ Nothing+ (InputFormatJSON $ JSONInputProp JSONTypeLines)++-- | documentJsonInput returns InputSerialization with JSON document+-- based format with no compression setting.+documentJsonInput :: InputSerialization+documentJsonInput =+ InputSerialization+ Nothing+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)++-- | defaultParquetInput returns InputSerialization with Parquet+-- format, and no compression setting.+defaultParquetInput :: InputSerialization+defaultParquetInput = InputSerialization Nothing InputFormatParquet++-- | setInputCompressionType sets the compression type for the input+-- of the SelectRequest+setInputCompressionType ::+ CompressionType ->+ SelectRequest ->+ SelectRequest+setInputCompressionType c i =+ let is = srInputSerialization i+ is' = is {isCompressionType = Just c}+ in i {srInputSerialization = is'}++-- | defaultCsvOutput returns OutputSerialization with default CSV+-- format.+defaultCsvOutput :: OutputSerialization+defaultCsvOutput = OutputSerializationCSV defaultCSVProp++-- | defaultJsonInput returns OutputSerialization with default JSON+-- format.+defaultJsonOutput :: OutputSerialization+defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)++-- | selectRequest is used to build a `SelectRequest`+-- value. @selectRequest query inputSer outputSer@ represents a+-- SelectRequest with the SQL query text given by @query@, the input+-- serialization settings (compression format and format information)+-- @inputSer@ and the output serialization settings @outputSer@.+selectRequest ::+ Text ->+ InputSerialization ->+ OutputSerialization ->+ SelectRequest+selectRequest sqlQuery inputSer outputSer =+ SelectRequest+ { srExpression = sqlQuery,+ srExpressionType = SQL,+ srInputSerialization = inputSer,+ srOutputSerialization = outputSer,+ srRequestProgressEnabled = Nothing+ }++-- | setRequestProgressEnabled sets the flag for turning on progress+-- messages when the Select response is being streamed back to the+-- client.+setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest+setRequestProgressEnabled enabled sr =+ sr {srRequestProgressEnabled = Just enabled}++type CSVInputProp = CSVProp++-- | CSVProp represents CSV format properties. It is built up using+-- the Monoid instance.+data CSVProp = CSVProp (H.HashMap Text Text)+ deriving (Eq, Show)++#if (__GLASGOW_HASKELL__ >= 804)+instance Semigroup CSVProp where+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)+#endif++instance Monoid CSVProp where+ mempty = CSVProp mempty++#if (__GLASGOW_HASKELL__ < 804)+ mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)+#endif++defaultCSVProp :: CSVProp+defaultCSVProp = mempty++-- | Specify the CSV record delimiter property.+recordDelimiter :: Text -> CSVProp+recordDelimiter = CSVProp . H.singleton "RecordDelimiter"++-- | Specify the CSV field delimiter property.+fieldDelimiter :: Text -> CSVProp+fieldDelimiter = CSVProp . H.singleton "FieldDelimiter"++-- | Specify the CSV quote character property.+quoteCharacter :: Text -> CSVProp+quoteCharacter = CSVProp . H.singleton "QuoteCharacter"++-- | Specify the CSV quote-escape character property.+quoteEscapeCharacter :: Text -> CSVProp+quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"++-- | FileHeaderInfo specifies information about column headers for CSV+-- format.+data FileHeaderInfo+ = -- | No column headers are present+ FileHeaderNone+ | -- | Headers are present and they should be used+ FileHeaderUse+ | -- | Header are present, but should be ignored+ FileHeaderIgnore+ deriving (Eq, Show)++-- | Specify the CSV file header info property.+fileHeaderInfo :: FileHeaderInfo -> CSVProp+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString+ where+ toString FileHeaderNone = "NONE"+ toString FileHeaderUse = "USE"+ toString FileHeaderIgnore = "IGNORE"++-- | Specify the CSV comment character property. Lines starting with+-- this character are ignored by the server.+commentCharacter :: Text -> CSVProp+commentCharacter = CSVProp . H.singleton "Comments"++-- | Allow quoted record delimiters inside a row using this property.+allowQuotedRecordDelimiter :: CSVProp+allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter" "TRUE"++-- | Set the CSV format properties in the InputSerialization.+setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization+setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}++-- | Set the CSV format properties in the OutputSerialization.+outputCSVFromProps :: CSVProp -> OutputSerialization+outputCSVFromProps p = OutputSerializationCSV p++data JSONInputProp = JSONInputProp {jsonipType :: JSONType}+ deriving (Eq, Show)++data JSONType = JSONTypeDocument | JSONTypeLines+ deriving (Eq, Show)++-- | OutputSerialization represents output serialization settings for+-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as+-- a starting point.+data OutputSerialization+ = OutputSerializationJSON JSONOutputProp+ | OutputSerializationCSV CSVOutputProp+ deriving (Eq, Show)++type CSVOutputProp = CSVProp++-- | quoteFields is an output serialization parameter+quoteFields :: QuoteFields -> CSVProp+quoteFields q = CSVProp $ H.singleton "QuoteFields" $+ case q of+ QuoteFieldsAsNeeded -> "ASNEEDED"+ QuoteFieldsAlways -> "ALWAYS"++-- | Represent the QuoteField setting.+data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways+ deriving (Eq, Show)++data JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}+ deriving (Eq, Show)++-- | Set the output record delimiter for JSON format+outputJSONFromRecordDelimiter :: Text -> OutputSerialization+outputJSONFromRecordDelimiter t =+ OutputSerializationJSON (JSONOutputProp $ Just t)++-- Response related types++-- | An EventMessage represents each kind of message received from the server.+data EventMessage+ = ProgressEventMessage {emProgress :: Progress}+ | StatsEventMessage {emStats :: Stats}+ | RequestLevelErrorMessage+ { emErrorCode :: Text,+ emErrorMessage :: Text+ }+ | RecordPayloadEventMessage {emPayloadBytes :: ByteString}+ deriving (Eq, Show)++data MsgHeaderName+ = MessageType+ | EventType+ | ContentType+ | ErrorCode+ | ErrorMessage+ deriving (Eq, Show)++msgHeaderValueType :: Word8+msgHeaderValueType = 7++type MessageHeader = (MsgHeaderName, Text)++-- | Represent the progress event returned in the Select response.+data Progress = Progress+ { pBytesScanned :: Int64,+ pBytesProcessed :: Int64,+ pBytesReturned :: Int64+ }+ deriving (Eq, Show)++-- | Represent the stats event returned at the end of the Select+-- response.+type Stats = Progress++--------------------------------------------------------------------------+-- Select API Related Types End+--------------------------------------------------------------------------++-- | Represents different kinds of payload that are used with S3 API+-- requests.+data Payload+ = PayloadBS ByteString+ | PayloadH Handle Int64 Int64 -- file handle, offset and length+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source++defaultPayload :: Payload+defaultPayload = PayloadBS ""++data AdminReqInfo = AdminReqInfo+ { ariMethod :: Method,+ ariPayloadHash :: Maybe ByteString,+ ariPayload :: Payload,+ ariPath :: ByteString,+ ariHeaders :: [Header],+ ariQueryParams :: Query+ }++data S3ReqInfo = S3ReqInfo+ { riMethod :: Method,+ riBucket :: Maybe Bucket,+ riObject :: Maybe Object,+ riQueryParams :: Query,+ riHeaders :: [Header],+ riPayload :: Payload,+ riPayloadHash :: Maybe ByteString,+ riRegion :: Maybe Region,+ riNeedsLocation :: Bool+ }++defaultS3ReqInfo :: S3ReqInfo+defaultS3ReqInfo =+ S3ReqInfo+ HT.methodGet+ Nothing+ Nothing+ []+ []+ defaultPayload+ Nothing+ Nothing+ True++getS3Path :: Maybe Bucket -> Maybe Object -> ByteString+getS3Path b o =+ let segments = map toUtf8 $ 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 = H.HashMap Bucket Region++-- | The Minio Monad - all computations accessing object storage+-- happens in it.+newtype Minio a = Minio+ { unMinio :: ReaderT MinioConn (ResourceT IO) a+ }+ deriving+ ( Functor,+ Applicative,+ Monad,+ MonadIO,+ MonadReader MinioConn,+ MonadResource,+ MonadUnliftIO+ )++-- | MinioConn holds connection info and a connection pool to allow+-- for efficient resource re-use.+data MinioConn = MinioConn+ { mcConnInfo :: ConnectInfo,+ mcConnManager :: NC.Manager,+ mcRegionMap :: MVar RegionMap+ }++class HasSvcNamespace env where+ getSvcNamespace :: env -> Text++instance HasSvcNamespace MinioConn where+ getSvcNamespace env =+ let host = connectHost $ mcConnInfo env+ in if+ | host == "storage.googleapis.com" ->+ "http://doc.s3.amazonaws.com/2006-03-01"+ | otherwise ->+ "http://s3.amazonaws.com/doc/2006-03-01/"++-- | Takes connection information and returns a connection object to+-- be passed to 'runMinio'. The returned value can be kept in the+-- application environment and passed to `runMinioWith` whenever+-- object storage is accessed.+connect :: ConnectInfo -> IO MinioConn+connect ci = do+ let settings+ | connectIsSecure ci && connectDisableTLSCertValidation ci =+ let badTlsSettings = Conn.TLSSettingsSimple True False False+ in TLS.mkManagerSettings badTlsSettings Nothing+ | connectIsSecure ci = NC.tlsManagerSettings+ | otherwise = defaultManagerSettings+ mgr <- NC.newManager settings+ mkMinioConn ci mgr++-- | Run the computation accessing object storage using the given+-- `MinioConn`. This reuses connections, but otherwise it is similar+-- to `runMinio`.+runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)+runMinioWith conn m = runResourceT $ runMinioResWith conn m++-- | Given `ConnectInfo` and a HTTP connection manager, create a+-- `MinioConn`.+mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn+mkMinioConn ci mgr = do+ rMapMVar <- M.newMVar H.empty+ return $ MinioConn ci mgr rMapMVar++-- | Run the Minio action and return the result or an error.+runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)+runMinio ci m = do+ conn <- connect ci+ runResourceT $ runMinioResWith conn m++-- | Similar to 'runMinioWith'. Allows applications to allocate/release+-- its resources along side MinIO's internal resources.+runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)+runMinioResWith conn m =+ flip runReaderT conn . unMinio $+ fmap Right m+ `U.catches` [ U.Handler handlerServiceErr,+ U.Handler handlerHE,+ U.Handler handlerFE,+ U.Handler handlerValidation+ ] where handlerServiceErr = return . Left . MErrService handlerHE = return . Left . MErrHTTP
src/Network/Minio/Data/ByteString.hs view
@@ -13,23 +13,22 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE FlexibleInstances #-}+ module Network.Minio.Data.ByteString- (- stripBS- , UriEncodable(..)- ) where+ ( stripBS,+ UriEncodable (..),+ )+where import qualified Data.ByteString as B import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Lazy as LB-import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)+import Data.Char (isAsciiLower, isAsciiUpper) import qualified Data.Text as T-import Numeric (showHex)--import Lib.Prelude+import Lib.Prelude+import Numeric (showHex) stripBS :: ByteString -> ByteString stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace@@ -40,7 +39,7 @@ instance UriEncodable [Char] where uriEncode encodeSlash payload = LB.toStrict $ BB.toLazyByteString $ mconcat $- map (`uriEncodeChar` encodeSlash) payload+ map (`uriEncodeChar` encodeSlash) payload instance UriEncodable ByteString where -- assumes that uriEncode is passed ASCII encoded strings.@@ -59,16 +58,17 @@ uriEncodeChar '/' False = BB.char7 '/' uriEncodeChar ch _ | isAsciiUpper ch- || isAsciiLower ch- || isDigit ch- || (ch == '_')- || (ch == '-')- || (ch == '.')- || (ch == '~') = BB.char7 ch+ || isAsciiLower ch+ || isDigit ch+ || (ch == '_')+ || (ch == '-')+ || (ch == '.')+ || (ch == '~') =+ BB.char7 ch | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch where f :: Word8 -> BB.Builder f n = BB.char7 '%' <> BB.string7 hexStr where hexStr = map toUpper $ showHex q $ showHex r ""- (q, r) = divMod (fromIntegral n) (16::Word8)+ (q, r) = divMod (fromIntegral n) (16 :: Word8)
src/Network/Minio/Data/Crypto.hs view
@@ -15,31 +15,31 @@ -- module Network.Minio.Data.Crypto- (- hashSHA256- , hashSHA256FromSource-- , hashMD5- , hashMD5ToBase64- , hashMD5FromSource-- , hmacSHA256- , hmacSHA256RawBS- , digestToBS- , digestToBase16-- , encodeToBase64- ) where--import Crypto.Hash (Digest, MD5 (..), SHA256 (..),- hashWith)-import Crypto.Hash.Conduit (sinkHash)-import Crypto.MAC.HMAC (HMAC, hmac)-import Data.ByteArray (ByteArrayAccess, convert)-import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)-import qualified Data.Conduit as C+ ( hashSHA256,+ hashSHA256FromSource,+ hashMD5,+ hashMD5ToBase64,+ hashMD5FromSource,+ hmacSHA256,+ hmacSHA256RawBS,+ digestToBS,+ digestToBase16,+ encodeToBase64,+ )+where -import Lib.Prelude+import Crypto.Hash+ ( Digest,+ MD5 (..),+ SHA256 (..),+ hashWith,+ )+import Crypto.Hash.Conduit (sinkHash)+import Crypto.MAC.HMAC (HMAC, hmac)+import Data.ByteArray (ByteArrayAccess, convert)+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)+import qualified Data.Conduit as C+import Lib.Prelude hashSHA256 :: ByteString -> ByteString hashSHA256 = digestToBase16 . hashWith SHA256
src/Network/Minio/Data/Time.hs view
@@ -15,20 +15,18 @@ -- module Network.Minio.Data.Time- (- awsTimeFormat- , awsTimeFormatBS- , awsDateFormat- , awsDateFormatBS- , awsParseTime- , iso8601TimeFormat- ) where-+ ( awsTimeFormat,+ awsTimeFormatBS,+ awsDateFormat,+ awsDateFormatBS,+ awsParseTime,+ iso8601TimeFormat,+ )+where -import Data.ByteString.Char8 (pack)+import Data.ByteString.Char8 (pack) import qualified Data.Time as Time--import Lib.Prelude+import Lib.Prelude awsTimeFormat :: UTCTime -> [Char] awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
src/Network/Minio/Errors.hs view
@@ -16,74 +16,75 @@ module Network.Minio.Errors where -import Control.Exception+import Control.Exception+import Lib.Prelude import qualified Network.HTTP.Conduit as NC -import Lib.Prelude-- --------------------------------- -- Errors ---------------------------------+ -- | Various validation errors-data MErrV = MErrVSinglePUTSizeExceeded Int64- | MErrVPutSizeExceeded Int64- | MErrVETagHeaderNotFound- | MErrVInvalidObjectInfoResponse- | MErrVInvalidSrcObjSpec Text- | MErrVInvalidSrcObjByteRange (Int64, Int64)- | MErrVCopyObjSingleNoRangeAccepted- | MErrVRegionNotSupported Text- | MErrVXmlParse Text- | MErrVInvalidBucketName Text- | MErrVInvalidObjectName Text- | MErrVInvalidUrlExpiry Int- | MErrVJsonParse Text- | MErrVInvalidHealPath- | MErrVMissingCredentials- | MErrVInvalidEncryptionKeyLength- | MErrVStreamingBodyUnexpectedEOF- | MErrVUnexpectedPayload+data MErrV+ = MErrVSinglePUTSizeExceeded Int64+ | MErrVPutSizeExceeded Int64+ | MErrVETagHeaderNotFound+ | MErrVInvalidObjectInfoResponse+ | MErrVInvalidSrcObjSpec Text+ | MErrVInvalidSrcObjByteRange (Int64, Int64)+ | MErrVCopyObjSingleNoRangeAccepted+ | MErrVRegionNotSupported Text+ | MErrVXmlParse Text+ | MErrVInvalidBucketName Text+ | MErrVInvalidObjectName Text+ | MErrVInvalidUrlExpiry Int+ | MErrVJsonParse Text+ | MErrVInvalidHealPath+ | MErrVMissingCredentials+ | MErrVInvalidEncryptionKeyLength+ | MErrVStreamingBodyUnexpectedEOF+ | MErrVUnexpectedPayload deriving (Show, Eq) instance Exception MErrV -- | Errors returned by S3 compatible service-data ServiceErr = BucketAlreadyExists- | BucketAlreadyOwnedByYou- | NoSuchBucket- | InvalidBucketName- | NoSuchKey- | SelectErr Text Text- | ServiceErr Text Text+data ServiceErr+ = BucketAlreadyExists+ | BucketAlreadyOwnedByYou+ | NoSuchBucket+ | InvalidBucketName+ | NoSuchKey+ | SelectErr Text Text+ | ServiceErr Text Text deriving (Show, Eq) instance Exception ServiceErr toServiceErr :: Text -> Text -> ServiceErr-toServiceErr "NoSuchKey" _ = NoSuchKey-toServiceErr "NoSuchBucket" _ = NoSuchBucket-toServiceErr "InvalidBucketName" _ = InvalidBucketName+toServiceErr "NoSuchKey" _ = NoSuchKey+toServiceErr "NoSuchBucket" _ = NoSuchBucket+toServiceErr "InvalidBucketName" _ = InvalidBucketName toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou-toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists-toServiceErr code message = ServiceErr code message-+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists+toServiceErr code message = ServiceErr code message -- | Errors thrown by the library-data MinioErr = MErrHTTP NC.HttpException- | MErrIO IOException- | MErrService ServiceErr- | MErrValidation MErrV+data MinioErr+ = MErrHTTP NC.HttpException+ | MErrIO IOException+ | MErrService ServiceErr+ | MErrValidation MErrV deriving (Show) instance Eq MinioErr where- MErrHTTP _ == MErrHTTP _ = True- MErrHTTP _ == _ = False- MErrIO _ == MErrIO _ = True- MErrIO _ == _ = False- MErrService a == MErrService b = a == b- MErrService _ == _ = False- MErrValidation a == MErrValidation b = a == b- MErrValidation _ == _ = False+ MErrHTTP _ == MErrHTTP _ = True+ MErrHTTP _ == _ = False+ MErrIO _ == MErrIO _ = True+ MErrIO _ == _ = False+ MErrService a == MErrService b = a == b+ MErrService _ == _ = False+ MErrValidation a == MErrValidation b = a == b+ MErrValidation _ == _ = False instance Exception MinioErr
src/Network/Minio/JsonParser.hs view
@@ -15,28 +15,35 @@ -- module Network.Minio.JsonParser- (- parseErrResponseJSON- ) where--import Data.Aeson (FromJSON, eitherDecode, parseJSON,- withObject, (.:))-import qualified Data.Text as T+ ( parseErrResponseJSON,+ )+where -import Lib.Prelude+import Data.Aeson+ ( (.:),+ FromJSON,+ eitherDecode,+ parseJSON,+ withObject,+ )+import qualified Data.Text as T+import Lib.Prelude+import Network.Minio.Errors -import Network.Minio.Errors+data AdminErrJSON = AdminErrJSON+ { aeCode :: Text,+ aeMessage :: Text+ }+ deriving (Eq, Show) -data AdminErrJSON = AdminErrJSON { aeCode :: Text- , aeMessage :: Text- } deriving (Eq, Show) instance FromJSON AdminErrJSON where- parseJSON = withObject "AdminErrJSON" $ \v -> AdminErrJSON- <$> v .: "Code"- <*> v .: "Message"+ parseJSON = withObject "AdminErrJSON" $ \v ->+ AdminErrJSON+ <$> v .: "Code"+ <*> v .: "Message" parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr parseErrResponseJSON jsondata = case eitherDecode jsondata of Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)- Left err -> throwIO $ MErrVJsonParse $ T.pack err+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs view
@@ -16,20 +16,19 @@ module Network.Minio.ListOps where -import qualified Data.Conduit as C+import qualified Data.Conduit as C import qualified Data.Conduit.Combinators as CC-import qualified Data.Conduit.List as CL--import Lib.Prelude--import Network.Minio.Data-import Network.Minio.S3API+import qualified Data.Conduit.List as CL+import Lib.Prelude+import Network.Minio.Data+import Network.Minio.S3API -- | Represents a list output item - either an object or an object -- prefix (i.e. a directory).-data ListItem = ListItemObject ObjectInfo- | ListItemPrefix Text- deriving (Show, Eq)+data ListItem+ = ListItemObject ObjectInfo+ | ListItemPrefix Text+ deriving (Show, Eq) -- | @'listObjects' bucket prefix recurse@ lists objects in a bucket -- similar to a file system tree traversal.@@ -48,73 +47,99 @@ where loop :: Maybe Text -> C.ConduitM () ListItem Minio () loop nextToken = do- let- delimiter = bool (Just "/") Nothing recurse+ let delimiter = bool (Just "/") Nothing recurse res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing CL.sourceList $ map ListItemObject $ lorObjects res- unless recurse $- CL.sourceList $ map ListItemPrefix $ lorCPrefixes res+ unless recurse+ $ CL.sourceList+ $ map ListItemPrefix+ $ lorCPrefixes res when (lorHasMore res) $ loop (lorNextToken res) -- | Lists objects - similar to @listObjects@, however uses the older -- V1 AWS S3 API. Prefer @listObjects@ to this.-listObjectsV1 :: Bucket -> Maybe Text -> Bool- -> C.ConduitM () ListItem Minio ()+listObjectsV1 ::+ Bucket ->+ Maybe Text ->+ Bool ->+ C.ConduitM () ListItem Minio () listObjectsV1 bucket prefix recurse = loop Nothing where loop :: Maybe Text -> C.ConduitM () ListItem Minio () loop nextMarker = do- let- delimiter = bool (Just "/") Nothing recurse+ let delimiter = bool (Just "/") Nothing recurse res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing CL.sourceList $ map ListItemObject $ lorObjects' res- unless recurse $- CL.sourceList $ map ListItemPrefix $ lorCPrefixes' res+ unless recurse+ $ CL.sourceList+ $ map ListItemPrefix+ $ lorCPrefixes' 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.-listIncompleteUploads :: Bucket -> Maybe Text -> Bool- -> C.ConduitM () UploadInfo Minio ()+listIncompleteUploads ::+ Bucket ->+ Maybe Text ->+ Bool ->+ C.ConduitM () UploadInfo Minio () listIncompleteUploads bucket prefix recurse = loop Nothing Nothing where loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio () loop nextKeyMarker nextUploadIdMarker = do- let- delimiter = bool (Just "/") Nothing recurse+ let delimiter = bool (Just "/") Nothing recurse - res <- lift $ listIncompleteUploads' bucket prefix delimiter- nextKeyMarker nextUploadIdMarker Nothing+ res <-+ lift $+ listIncompleteUploads'+ bucket+ prefix+ delimiter+ nextKeyMarker+ nextUploadIdMarker+ Nothing aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do- partInfos <- C.runConduit $ listIncompleteParts bucket uKey uId- C..| CC.sinkList- return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos+ partInfos <-+ C.runConduit $+ listIncompleteParts bucket uKey uId+ C..| CC.sinkList+ return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos - CL.sourceList $- map (\((uKey, uId, uInitTime), size) ->- UploadInfo uKey uId uInitTime size- ) $ zip (lurUploads res) aggrSizes+ CL.sourceList+ $ map+ ( \((uKey, uId, uInitTime), size) ->+ UploadInfo uKey uId uInitTime size+ )+ $ zip (lurUploads res) aggrSizes when (lurHasMore res) $ loop (lurNextKey res) (lurNextUpload res) - -- | List object parts of an ongoing multipart upload for given -- bucket, object and uploadId.-listIncompleteParts :: Bucket -> Object -> UploadId- -> C.ConduitM () ObjectPartInfo Minio ()+listIncompleteParts ::+ Bucket ->+ Object ->+ UploadId ->+ C.ConduitM () ObjectPartInfo Minio () listIncompleteParts bucket object uploadId = loop Nothing where loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio () loop nextPartMarker = do- res <- lift $ listIncompleteParts' bucket object uploadId Nothing- nextPartMarker+ res <-+ lift $+ listIncompleteParts'+ bucket+ object+ uploadId+ Nothing+ nextPartMarker CL.sourceList $ lprParts res when (lprHasMore res) $ loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs view
@@ -15,43 +15,40 @@ -- 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 qualified Data.HashMap.Strict as H-import qualified Data.Text as T-import qualified Data.Time as Time-import qualified Network.HTTP.Conduit as NC-import qualified Network.HTTP.Types as HT-import Network.HTTP.Types.Header (hHost)--import Lib.Prelude+ ( UrlExpiry,+ makePresignedUrl,+ presignedPutObjectUrl,+ presignedGetObjectUrl,+ presignedHeadObjectUrl,+ PostPolicyCondition (..),+ ppCondBucket,+ ppCondContentLengthRange,+ ppCondContentType,+ ppCondKey,+ ppCondKeyStartsWith,+ ppCondSuccessActionStatus,+ PostPolicy (..),+ PostPolicyError (..),+ newPostPolicy,+ showPostPolicy,+ presignedPostPolicy,+ )+where -import Network.Minio.Data-import Network.Minio.Data.Time-import Network.Minio.Errors-import Network.Minio.Sign.V4+import Data.Aeson ((.=))+import qualified Data.Aeson as Json+import Data.ByteString.Builder (byteString, toLazyByteString)+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import qualified Data.Time as Time+import Lib.Prelude+import qualified Network.HTTP.Conduit as NC+import qualified Network.HTTP.Types as HT+import Network.HTTP.Types.Header (hHost)+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.@@ -61,42 +58,56 @@ -- -- 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 ::+ 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) $- throwIO $ MErrVInvalidUrlExpiry expiry+ when (expiry > 7 * 24 * 3600 || expiry < 0)+ $ throwIO+ $ MErrVInvalidUrlExpiry expiry ci <- asks mcConnInfo - let- hostHeader = (hHost, getHostAddr ci)- req = NC.defaultRequest {- NC.method = method- , NC.secure = connectIsSecure ci- , NC.host = encodeUtf8 $ connectHost ci- , NC.port = connectPort ci- , NC.path = getS3Path bucket object- , NC.requestHeaders = hostHeader : extraHeaders- , NC.queryString = HT.renderQuery True extraQuery- }+ let hostHeader = (hHost, getHostAddr ci)+ req =+ NC.defaultRequest+ { NC.method = method,+ NC.secure = connectIsSecure ci,+ NC.host = encodeUtf8 $ connectHost ci,+ NC.port = connectPort ci,+ NC.path = getS3Path bucket object,+ NC.requestHeaders = hostHeader : extraHeaders,+ NC.queryString = HT.renderQuery True extraQuery+ } ts <- liftIO Time.getCurrentTime - let sp = SignParams (connectAccessKey ci) (connectSecretKey ci)- ts region (Just expiry) Nothing-+ let sp =+ SignParams+ (connectAccessKey ci)+ (connectSecretKey ci)+ ts+ region+ (Just expiry)+ Nothing signPairs = signV4 sp req- qpToAdd = (fmap . fmap) Just signPairs- queryStr = HT.renderQueryBuilder True- ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)+ queryStr =+ HT.renderQueryBuilder+ True+ ((HT.parseQuery $ NC.queryString req) ++ qpToAdd) scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci - return $ toS $ toLazyByteString $ scheme- <> byteString (getHostAddr ci)- <> byteString (getS3Path bucket object)- <> queryStr+ return $ toStrictBS $ toLazyByteString $+ scheme+ <> byteString (getHostAddr ci)+ <> byteString (getS3Path bucket object)+ <> queryStr -- | Generate a URL with authentication signature to PUT (upload) an -- object. Any extra headers if passed, are signed, and so they are@@ -105,12 +116,22 @@ -- -- 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 ->+ UrlExpiry ->+ HT.RequestHeaders ->+ Minio ByteString presignedPutObjectUrl bucket object expirySeconds extraHeaders = do region <- asks (Just . connectRegion . mcConnInfo)- makePresignedUrl expirySeconds HT.methodPut- (Just bucket) (Just object) region [] extraHeaders+ makePresignedUrl+ expirySeconds+ HT.methodPut+ (Just bucket)+ (Just object)+ region+ []+ extraHeaders -- | Generate a URL with authentication signature to GET (download) an -- object. All extra query parameters and headers passed here will be@@ -121,12 +142,23 @@ -- -- 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 ->+ UrlExpiry ->+ HT.Query ->+ HT.RequestHeaders ->+ Minio ByteString presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do region <- asks (Just . connectRegion . mcConnInfo)- makePresignedUrl expirySeconds HT.methodGet- (Just bucket) (Just object) region extraQuery extraHeaders+ makePresignedUrl+ expirySeconds+ HT.methodGet+ (Just bucket)+ (Just object)+ region+ extraQuery+ extraHeaders -- | Generate a URL with authentication signature to make a HEAD -- request on an object. This is used to fetch metadata about an@@ -135,18 +167,29 @@ -- -- 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 ->+ UrlExpiry ->+ HT.RequestHeaders ->+ Minio ByteString presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do region <- asks (Just . connectRegion . mcConnInfo)- makePresignedUrl expirySeconds HT.methodHead- (Just bucket) (Just object) region [] extraHeaders+ makePresignedUrl+ expirySeconds+ HT.methodHead+ (Just bucket)+ (Just object)+ region+ []+ extraHeaders -- | Represents individual conditions in a Post Policy document.-data PostPolicyCondition = PPCStartsWith Text Text- | PPCEquals Text Text- | PPCRange Text Int64 Int64- deriving (Show, Eq)+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]@@ -160,25 +203,28 @@ 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)+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- ]+ 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)+data PostPolicyError+ = PPEKeyNotSpecified+ | PPEBucketNotSpecified+ | PPEConditionKeyEmpty+ | PPERangeInvalid+ deriving (Eq, Show) -- | Set the bucket name that the upload should use. ppCondBucket :: Bucket -> PostPolicyCondition@@ -186,8 +232,10 @@ -- | Set the content length range constraint with minimum and maximum -- byte count values.-ppCondContentLengthRange :: Int64 -> Int64- -> PostPolicyCondition+ppCondContentLengthRange ::+ Int64 ->+ Int64 ->+ PostPolicyCondition ppCondContentLengthRange = PPCRange "content-length-range" -- | Set the content-type header for the upload.@@ -210,83 +258,91 @@ -- | This function creates a PostPolicy after validating its -- arguments.-newPostPolicy :: UTCTime -> [PostPolicyCondition]- -> Either PostPolicyError PostPolicy+newPostPolicy ::+ UTCTime ->+ [PostPolicyCondition] ->+ Either PostPolicyError PostPolicy newPostPolicy expirationTime conds -- object name condition must be present | not $ any (keyEquals "key") conds =- Left PPEKeyNotSpecified-+ Left PPEKeyNotSpecified -- bucket name condition must be present | not $ any (keyEquals "bucket") conds =- Left PPEBucketNotSpecified-+ Left PPEBucketNotSpecified -- a condition with an empty key is invalid | any (keyEquals "") conds || any isEmptyRangeKey conds =- Left PPEConditionKeyEmpty-+ Left PPEConditionKeyEmpty -- invalid range check | any isInvalidRange conds =- Left PPERangeInvalid-+ Left PPERangeInvalid -- all good! | otherwise =- return $ PostPolicy expirationTime conds-+ return $ PostPolicy expirationTime conds where keyEquals k' (PPCStartsWith k _) = k == k'- keyEquals k' (PPCEquals k _) = k == k'- keyEquals _ _ = False-+ keyEquals k' (PPCEquals k _) = k == k'+ keyEquals _ _ = False isEmptyRangeKey (PPCRange k _ _) = k == ""- isEmptyRangeKey _ = False-+ isEmptyRangeKey _ = False isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma- isInvalidRange _ = False+ isInvalidRange _ = False -- | Convert Post Policy to a string (e.g. for printing). showPostPolicy :: PostPolicy -> ByteString-showPostPolicy = toS . Json.encode+showPostPolicy = toStrictBS . 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, H.HashMap Text ByteString)+presignedPostPolicy ::+ PostPolicy ->+ Minio (ByteString, H.HashMap 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- }- sp = SignParams (connectAccessKey ci) (connectSecretKey ci)- signTime (Just $ connectRegion ci) Nothing Nothing- signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp--- -- compute form-data- mkPair (PPCStartsWith k v) = Just (k, v)- mkPair (PPCEquals k v) = Just (k, v)- mkPair _ = Nothing- formFromPolicy = H.map toS $ H.fromList $ catMaybes $- mkPair <$> conditions ppWithCreds- formData = formFromPolicy `H.union` signData-- -- compute POST upload URL- bucket = H.lookupDefault "" "bucket" formData- scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci- region = connectRegion ci-- url = toS $ toLazyByteString $ scheme <> byteString (getHostAddr ci) <>- byteString "/" <> byteString (toS bucket) <> byteString "/"+ 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+ }+ sp =+ SignParams+ (connectAccessKey ci)+ (connectSecretKey ci)+ signTime+ (Just $ connectRegion ci)+ Nothing+ Nothing+ signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp+ -- compute form-data+ mkPair (PPCStartsWith k v) = Just (k, v)+ mkPair (PPCEquals k v) = Just (k, v)+ mkPair _ = Nothing+ formFromPolicy =+ H.map toUtf8 $ H.fromList $ catMaybes $+ mkPair <$> conditions ppWithCreds+ formData = formFromPolicy `H.union` signData+ -- compute POST upload URL+ bucket = H.lookupDefault "" "bucket" formData+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci+ region = connectRegion ci+ url =+ toStrictBS $ toLazyByteString $+ scheme <> byteString (getHostAddr ci)+ <> byteString "/"+ <> byteString bucket+ <> byteString "/" return (url, formData)
src/Network/Minio/PutObject.hs view
@@ -15,29 +15,24 @@ -- module Network.Minio.PutObject- (- putObjectInternal- , ObjectData(..)- , selectPartSizes- ) where-+ ( putObjectInternal,+ ObjectData (..),+ selectPartSizes,+ )+where -import Conduit (takeC)-import qualified Conduit as C-import qualified Data.ByteString.Lazy as LBS-import qualified Data.Conduit.Binary as CB+import Conduit (takeC)+import qualified Conduit as C+import qualified Data.ByteString.Lazy as LBS+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 Lib.Prelude--import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.S3API-import Network.Minio.Utils-+import qualified Data.Conduit.List as CL+import qualified Data.List as List+import Lib.Prelude+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.S3API+import Network.Minio.Utils -- | A data-type to represent the source data for an object. A -- file-path or a producer-conduit may be provided.@@ -50,37 +45,45 @@ -- the input - if it is not provided, upload will continue until the -- stream ends or the object reaches `maxObjectSize` size. data ObjectData m- = ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional- -- size.- | ODStream (C.ConduitM () ByteString m ()) (Maybe Int64) -- ^ Pass- -- size- -- (bytes)- -- if- -- known.+ = -- | Takes filepath and optional+ -- size.+ ODFile FilePath (Maybe Int64)+ | -- | Pass+ -- size+ -- (bytes)+ -- if+ -- known.+ ODStream (C.ConduitM () ByteString m ()) (Maybe Int64) -- | Put an object from ObjectData. This high-level API handles -- objects of all sizes, and even if the object size is unknown.-putObjectInternal :: Bucket -> Object -> PutObjectOptions- -> ObjectData Minio -> Minio ETag+putObjectInternal ::+ Bucket ->+ Object ->+ PutObjectOptions ->+ ObjectData Minio ->+ Minio ETag putObjectInternal b o opts (ODStream src sizeMay) = do case sizeMay of -- unable to get size, so assume non-seekable file Nothing -> sequentialMultipartUpload b o opts Nothing src- -- got file size, so check for single/multipart upload Just size ->- if | size <= 64 * oneMiB -> do- bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs- putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs- | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size- | otherwise -> sequentialMultipartUpload b o opts (Just size) src-+ if+ | size <= 64 * oneMiB -> do+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size+ | otherwise -> sequentialMultipartUpload b o opts (Just size) src putObjectInternal b o opts (ODFile fp sizeMay) = do hResE <- withNewHandle fp $ \h -> liftM2 (,) (isHandleSeekable h) (getFileSize h) - (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return- hResE+ (isSeekable, handleSizeMay) <-+ either+ (const $ return (False, Nothing))+ return+ hResE -- prefer given size to queried size. let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]@@ -88,18 +91,25 @@ case finalSizeMay of -- unable to get size, so assume non-seekable file Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp- -- got file size, so check for single/multipart upload Just size ->- if | size <= 64 * oneMiB -> either throwIO return =<<- withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)- | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size- | isSeekable -> parallelMultipartUpload b o opts fp size- | otherwise -> sequentialMultipartUpload b o opts (Just size) $- CB.sourceFile fp+ if+ | size <= 64 * oneMiB ->+ either throwIO return+ =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size+ | isSeekable -> parallelMultipartUpload b o opts fp size+ | otherwise ->+ sequentialMultipartUpload b o opts (Just size) $+ CB.sourceFile fp -parallelMultipartUpload :: Bucket -> Object -> PutObjectOptions- -> FilePath -> Int64 -> Minio ETag+parallelMultipartUpload ::+ Bucket ->+ Object ->+ PutObjectOptions ->+ FilePath ->+ Int64 ->+ Minio ETag parallelMultipartUpload b o opts filePath size = do -- get a new upload id. uploadId <- newMultipartUpload b o (pooToHeaders opts)@@ -109,15 +119,17 @@ let threads = fromMaybe 10 $ pooNumThreads opts -- perform upload with 'threads' threads- uploadedPartsE <- limitedMapConcurrently (fromIntegral threads)- (uploadPart uploadId) partSizeInfo+ uploadedPartsE <-+ limitedMapConcurrently+ (fromIntegral threads)+ (uploadPart uploadId)+ partSizeInfo -- if there were any errors, rethrow exception. mapM_ throwIO $ lefts uploadedPartsE -- if we get here, all parts were successfully uploaded. completeMultipartUpload b o uploadId $ rights uploadedPartsE- where uploadPart uploadId (partNum, offset, sz) = withNewHandle filePath $ \h -> do@@ -125,10 +137,13 @@ putObjectPart b o uploadId partNum [] payload -- | Upload multipart object from conduit source sequentially-sequentialMultipartUpload :: Bucket -> Object -> PutObjectOptions- -> Maybe Int64- -> C.ConduitM () ByteString Minio ()- -> Minio ETag+sequentialMultipartUpload ::+ Bucket ->+ Object ->+ PutObjectOptions ->+ Maybe Int64 ->+ C.ConduitM () ByteString Minio () ->+ Minio ETag sequentialMultipartUpload b o opts sizeMay src = do -- get a new upload id. uploadId <- newMultipartUpload b o (pooToHeaders opts)@@ -136,22 +151,23 @@ -- upload parts in loop let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay (pnums, _, sizes) = List.unzip3 partSizes- uploadedParts <- C.runConduit- $ src- C..| chunkBSConduit (map fromIntegral sizes)- C..| CL.map PayloadBS- C..| uploadPart' uploadId pnums- C..| CC.sinkList+ uploadedParts <-+ C.runConduit $+ src+ C..| chunkBSConduit (map fromIntegral sizes)+ C..| CL.map PayloadBS+ C..| uploadPart' uploadId pnums+ C..| CC.sinkList -- complete multipart upload completeMultipartUpload b o uploadId uploadedParts- where uploadPart' _ [] = return ()- uploadPart' uid (pn:pns) = do+ uploadPart' uid (pn : pns) = do payloadMay <- C.await case payloadMay of Nothing -> return ()- Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload- C.yield pinfo- uploadPart' uid pns+ Just payload -> do+ pinfo <- lift $ putObjectPart b o uid pn [] payload+ C.yield pinfo+ uploadPart' uid pns
src/Network/Minio/S3API.hs view
@@ -15,150 +15,166 @@ -- module Network.Minio.S3API- (- Region- , getLocation-- -- * Listing buckets- --------------------- , getService-- -- * Listing objects- --------------------- , ListObjectsResult(..)- , ListObjectsV1Result(..)- , listObjects'- , listObjectsV1'+ ( Region,+ getLocation, - -- * Retrieving buckets- , headBucket+ -- * Listing buckets+ --------------------+ getService, - -- * Retrieving objects- ------------------------ , getObject'- , headObject+ -- * Listing objects+ --------------------+ ListObjectsResult (..),+ ListObjectsV1Result (..),+ listObjects',+ listObjectsV1', - -- * Creating buckets and objects- ---------------------------------- , putBucket- , ETag- , maxSinglePutObjectSizeBytes- , putObjectSingle'- , putObjectSingle- , copyObjectSingle+ -- * Retrieving buckets+ headBucket, - -- * Multipart Upload APIs- --------------------------- , UploadId- , PartTuple- , Payload(..)- , PartNumber- , newMultipartUpload- , putObjectPart- , copyObjectPart- , completeMultipartUpload- , abortMultipartUpload- , ListUploadsResult(..)- , listIncompleteUploads'- , ListPartsResult(..)- , listIncompleteParts'+ -- * Retrieving objects+ -----------------------+ getObject',+ headObject, - -- * Deletion APIs- --------------------------- , deleteBucket- , deleteObject+ -- * Creating buckets and objects+ ---------------------------------+ putBucket,+ ETag,+ maxSinglePutObjectSizeBytes,+ putObjectSingle',+ putObjectSingle,+ copyObjectSingle, - -- * Presigned Operations- ------------------------------ , module Network.Minio.PresignedOperations+ -- * Multipart Upload APIs+ --------------------------+ UploadId,+ PartTuple,+ Payload (..),+ PartNumber,+ newMultipartUpload,+ putObjectPart,+ copyObjectPart,+ completeMultipartUpload,+ abortMultipartUpload,+ ListUploadsResult (..),+ listIncompleteUploads',+ ListPartsResult (..),+ listIncompleteParts', - -- ** Bucket Policies- , getBucketPolicy- , setBucketPolicy+ -- * Deletion APIs+ --------------------------+ deleteBucket,+ deleteObject, - -- * Bucket Notifications- -------------------------- , Notification(..)- , NotificationConfig(..)- , Arn- , Event(..)- , Filter(..)- , FilterKey(..)- , FilterRules(..)- , FilterRule(..)- , getBucketNotification- , putBucketNotification- , removeAllBucketNotification- ) where+ -- * Presigned Operations+ -----------------------------+ module Network.Minio.PresignedOperations, -import qualified Data.ByteString as BS-import qualified Data.Text as T-import qualified Network.HTTP.Conduit as NC-import qualified Network.HTTP.Types as HT-import Network.HTTP.Types.Status (status404)-import UnliftIO (Handler (Handler))+ -- ** Bucket Policies+ getBucketPolicy,+ setBucketPolicy, -import Lib.Prelude+ -- * Bucket Notifications+ -------------------------+ Notification (..),+ NotificationConfig (..),+ Arn,+ Event (..),+ Filter (..),+ FilterKey (..),+ FilterRules (..),+ FilterRule (..),+ getBucketNotification,+ putBucketNotification,+ removeAllBucketNotification,+ )+where -import Network.Minio.API-import Network.Minio.APICommon-import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.PresignedOperations-import Network.Minio.Utils-import Network.Minio.XmlGenerator-import Network.Minio.XmlParser+import qualified Data.ByteString as BS+import qualified Data.Text as T+import Lib.Prelude+import qualified Network.HTTP.Conduit as NC+import qualified Network.HTTP.Types as HT+import Network.HTTP.Types.Status (status404)+import Network.Minio.API+import Network.Minio.APICommon+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.PresignedOperations+import Network.Minio.Utils+import Network.Minio.XmlGenerator+import Network.Minio.XmlParser+import UnliftIO (Handler (Handler)) -- | Fetch all buckets from the service. getService :: Minio [BucketInfo] getService = do- resp <- executeRequest $ defaultS3ReqInfo {- riNeedsLocation = False- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riNeedsLocation = False+ } parseListBuckets $ NC.responseBody resp -- Parse headers from getObject and headObject calls. parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo parseGetObjectHeaders object headers =- let metadataPairs = getMetadata headers- userMetadata = getUserMetadataMap metadataPairs- metadata = getNonUserMetadataMap metadataPairs- in ObjectInfo <$> Just object- <*> getLastModifiedHeader headers- <*> getETagHeader headers- <*> getContentLength headers- <*> Just userMetadata- <*> Just metadata+ let metadataPairs = getMetadata headers+ userMetadata = getUserMetadataMap metadataPairs+ metadata = getNonUserMetadataMap metadataPairs+ in ObjectInfo <$> Just object+ <*> getLastModifiedHeader headers+ <*> getETagHeader headers+ <*> getContentLength headers+ <*> Just userMetadata+ <*> Just metadata -- | GET an object from the service and return parsed ObjectInfo and a -- conduit source for the object content-getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]- -> Minio GetObjectResponse+getObject' ::+ Bucket ->+ Object ->+ HT.Query ->+ [HT.Header] ->+ Minio GetObjectResponse getObject' bucket object queryParams headers = do- resp <- mkStreamRequest reqInfo- let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp- objInfo <- maybe (throwIO MErrVInvalidObjectInfoResponse) return- objInfoMaybe- return $ GetObjectResponse { gorObjectInfo = objInfo- , gorObjectStream = NC.responseBody resp- }+ resp <- mkStreamRequest reqInfo+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp+ objInfo <-+ maybe+ (throwIO MErrVInvalidObjectInfoResponse)+ return+ objInfoMaybe+ return $+ GetObjectResponse+ { gorObjectInfo = objInfo,+ gorObjectStream = NC.responseBody resp+ } where- reqInfo = defaultS3ReqInfo { riBucket = Just bucket- , riObject = Just object- , riQueryParams = queryParams- , riHeaders = headers- }+ reqInfo =+ defaultS3ReqInfo+ { riBucket = Just bucket,+ riObject = Just object,+ riQueryParams = queryParams,+ riHeaders = headers+ -- This header is required for safety as otherwise http-client,+ -- sends Accept-Encoding: gzip, and the server may actually gzip+ -- body. In that case Content-Length header will be missing.+ <> [("Accept-Encoding", "identity")]+ } -- | Creates a bucket via a PUT bucket call. putBucket :: Bucket -> Region -> Minio () putBucket bucket location = do ns <- asks getSvcNamespace void $ executeRequest $- defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just bucket- , riPayload = PayloadBS $ mkCreateBucketConfig ns location- , riNeedsLocation = False- }+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just bucket,+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,+ riNeedsLocation = False+ } -- | Single PUT object size. maxSinglePutObjectSizeBytes :: Int64@@ -172,315 +188,432 @@ putObjectSingle' bucket object headers bs = do let size = fromIntegral (BS.length bs) -- check length is within single PUT object size.- when (size > maxSinglePutObjectSizeBytes) $- throwIO $ MErrVSinglePUTSizeExceeded size+ when (size > maxSinglePutObjectSizeBytes)+ $ throwIO+ $ MErrVSinglePUTSizeExceeded size let payload = mkStreamingPayload $ PayloadBS bs- resp <- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just bucket- , riObject = Just object- , riHeaders = headers- , riPayload = payload- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just bucket,+ riObject = Just object,+ riHeaders = headers,+ riPayload = payload+ } let rheaders = NC.responseHeaders resp etag = getETagHeader rheaders maybe (throwIO MErrVETagHeaderNotFound)- return etag+ return+ etag -- | PUT an object into the service. This function performs a single -- PUT object call, and so can only transfer objects upto 5GiB.-putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64- -> Int64 -> Minio ETag+putObjectSingle ::+ Bucket ->+ Object ->+ [HT.Header] ->+ Handle ->+ Int64 ->+ Int64 ->+ Minio ETag putObjectSingle bucket object headers h offset size = do -- check length is within single PUT object size.- when (size > maxSinglePutObjectSizeBytes) $- throwIO $ MErrVSinglePUTSizeExceeded size+ when (size > maxSinglePutObjectSizeBytes)+ $ throwIO+ $ MErrVSinglePUTSizeExceeded size -- content-length header is automatically set by library. let payload = mkStreamingPayload $ PayloadH h offset size- resp <- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just bucket- , riObject = Just object- , riHeaders = headers- , riPayload = payload- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just bucket,+ riObject = Just object,+ riHeaders = headers,+ riPayload = payload+ } let rheaders = NC.responseHeaders resp etag = getETagHeader rheaders maybe (throwIO MErrVETagHeaderNotFound)- return etag+ 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 ->+ Maybe Text ->+ Maybe Text ->+ Maybe Text ->+ Maybe Int ->+ Minio ListObjectsV1Result listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet- , riBucket = Just bucket- , riQueryParams = mkOptionalParams params- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { 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)+ 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+listObjects' ::+ Bucket ->+ Maybe Text ->+ Maybe Text ->+ Maybe Text ->+ Maybe Int ->+ Minio ListObjectsResult listObjects' bucket prefix nextToken delimiter maxKeys = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet- , riBucket = Just bucket- , riQueryParams = mkOptionalParams params- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodGet,+ riBucket = Just bucket,+ riQueryParams = mkOptionalParams params+ } parseListObjectsResponse $ NC.responseBody resp where- params = [- ("list-type", Just "2")- , ("continuation_token", nextToken)- , ("prefix", prefix)- , ("delimiter", delimiter)- , ("max-keys", show <$> maxKeys)+ params =+ [ ("list-type", Just "2"),+ ("continuation_token", nextToken),+ ("prefix", prefix),+ ("delimiter", delimiter),+ ("max-keys", show <$> maxKeys) ] -- | DELETE a bucket from the service. deleteBucket :: Bucket -> Minio ()-deleteBucket bucket = void $- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodDelete- , riBucket = Just bucket- }+deleteBucket bucket =+ void+ $ executeRequest+ $ defaultS3ReqInfo+ { riMethod = HT.methodDelete,+ riBucket = Just bucket+ } -- | DELETE an object from the service. deleteObject :: Bucket -> Object -> Minio ()-deleteObject bucket object = void $- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodDelete- , riBucket = Just bucket- , riObject = Just object- }+deleteObject bucket object =+ void+ $ executeRequest+ $ defaultS3ReqInfo+ { riMethod = HT.methodDelete,+ riBucket = Just bucket,+ riObject = Just object+ } -- | Create a new multipart upload. newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId newMultipartUpload bucket object headers = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost- , riBucket = Just bucket- , riObject = Just object- , riQueryParams = [("uploads", Nothing)]- , riHeaders = headers- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPost,+ riBucket = Just bucket,+ riObject = Just object,+ riQueryParams = [("uploads", Nothing)],+ riHeaders = headers+ } parseNewMultipartUpload $ NC.responseBody resp -- | PUT a part of an object as part of a multipart upload.-putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]- -> Payload -> Minio PartTuple+putObjectPart ::+ Bucket ->+ Object ->+ UploadId ->+ PartNumber ->+ [HT.Header] ->+ Payload ->+ Minio PartTuple putObjectPart bucket object uploadId partNumber headers payload = do -- transform payload to conduit to enable streaming signature let payload' = mkStreamingPayload payload- resp <- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just bucket- , riObject = Just object- , riQueryParams = mkOptionalParams params- , riHeaders = headers- , riPayload = payload'- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just bucket,+ riObject = Just object,+ riQueryParams = mkOptionalParams params,+ riHeaders = headers,+ riPayload = payload'+ } let rheaders = NC.responseHeaders resp etag = getETagHeader rheaders maybe (throwIO MErrVETagHeaderNotFound)- (return . (partNumber, )) etag+ (return . (partNumber,))+ etag where- params = [- ("uploadId", Just uploadId)- , ("partNumber", Just $ show partNumber)+ params =+ [ ("uploadId", Just uploadId),+ ("partNumber", Just $ show partNumber) ] srcInfoToHeaders :: SourceInfo -> [HT.Header]-srcInfoToHeaders srcInfo = ("x-amz-copy-source",- toS $ T.concat ["/", srcBucket srcInfo,- "/", srcObject srcInfo]- ) : rangeHdr ++ zip names values+srcInfoToHeaders srcInfo =+ ( "x-amz-copy-source",+ toUtf8 $+ T.concat+ [ "/",+ srcBucket srcInfo,+ "/",+ srcObject srcInfo+ ]+ )+ : rangeHdr+ ++ zip names values where- names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",- "x-amz-copy-source-if-unmodified-since",- "x-amz-copy-source-if-modified-since"]- values = mapMaybe (fmap encodeUtf8 . (srcInfo &))- [srcIfMatch, srcIfNoneMatch,- fmap formatRFC1123 . srcIfUnmodifiedSince,- fmap formatRFC1123 . srcIfModifiedSince]- rangeHdr = maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])])- $ toByteRange <$> srcRange srcInfo+ names =+ [ "x-amz-copy-source-if-match",+ "x-amz-copy-source-if-none-match",+ "x-amz-copy-source-if-unmodified-since",+ "x-amz-copy-source-if-modified-since"+ ]+ values =+ mapMaybe+ (fmap encodeUtf8 . (srcInfo &))+ [ srcIfMatch,+ srcIfNoneMatch,+ fmap formatRFC1123 . srcIfUnmodifiedSince,+ fmap formatRFC1123 . srcIfModifiedSince+ ]+ rangeHdr =+ maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) $+ toByteRange <$> srcRange srcInfo toByteRange :: (Int64, Int64) -> HT.ByteRange toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y) -- | Performs server-side copy of an object or part of an object as an -- upload part of an ongoing multi-part upload.-copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId- -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)+copyObjectPart ::+ DestinationInfo ->+ SourceInfo ->+ UploadId ->+ PartNumber ->+ [HT.Header] ->+ Minio (ETag, UTCTime) copyObjectPart dstInfo srcInfo uploadId partNumber headers = do- resp <- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just $ dstBucket dstInfo- , riObject = Just $ dstObject dstInfo- , riQueryParams = mkOptionalParams params- , riHeaders = headers ++ srcInfoToHeaders srcInfo- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just $ dstBucket dstInfo,+ riObject = Just $ dstObject dstInfo,+ riQueryParams = mkOptionalParams params,+ riHeaders = headers ++ srcInfoToHeaders srcInfo+ } parseCopyObjectResponse $ NC.responseBody resp where- params = [- ("uploadId", Just uploadId)- , ("partNumber", Just $ show partNumber)+ params =+ [ ("uploadId", Just uploadId),+ ("partNumber", Just $ show partNumber) ] -- | Performs server-side copy of an object that is upto 5GiB in -- size. If the object is greater than 5GiB, this function throws the -- error returned by the server.-copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]- -> Minio (ETag, UTCTime)+copyObjectSingle ::+ Bucket ->+ Object ->+ SourceInfo ->+ [HT.Header] ->+ Minio (ETag, UTCTime) copyObjectSingle bucket object srcInfo headers = do -- validate that srcRange is Nothing for this API. when (isJust $ srcRange srcInfo) $ throwIO MErrVCopyObjSingleNoRangeAccepted- resp <- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just bucket- , riObject = Just object- , riHeaders = headers ++ srcInfoToHeaders srcInfo- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just bucket,+ riObject = Just object,+ riHeaders = headers ++ srcInfoToHeaders srcInfo+ } parseCopyObjectResponse $ NC.responseBody resp -- | Complete a multipart upload.-completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]- -> Minio ETag+completeMultipartUpload ::+ Bucket ->+ Object ->+ UploadId ->+ [PartTuple] ->+ Minio ETag completeMultipartUpload bucket object uploadId partTuple = do- resp <- executeRequest $- defaultS3ReqInfo { riMethod = HT.methodPost- , riBucket = Just bucket- , riObject = Just object- , riQueryParams = mkOptionalParams params- , riPayload = PayloadBS $- mkCompleteMultipartUploadRequest partTuple- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPost,+ riBucket = Just bucket,+ riObject = Just object,+ riQueryParams = mkOptionalParams params,+ riPayload =+ PayloadBS $+ mkCompleteMultipartUploadRequest partTuple+ } parseCompleteMultipartUploadResponse $ NC.responseBody resp where params = [("uploadId", Just uploadId)] -- | Abort a multipart upload. abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()-abortMultipartUpload bucket object uploadId = void $- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete- , riBucket = Just bucket- , riObject = Just object- , riQueryParams = mkOptionalParams params- }+abortMultipartUpload bucket object uploadId =+ void+ $ executeRequest+ $ defaultS3ReqInfo+ { riMethod = HT.methodDelete,+ riBucket = Just bucket,+ riObject = Just object,+ riQueryParams = mkOptionalParams params+ } where params = [("uploadId", Just uploadId)] -- | List incomplete multipart uploads.-listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text- -> Maybe Text -> Maybe Int -> Minio ListUploadsResult+listIncompleteUploads' ::+ Bucket ->+ Maybe Text ->+ Maybe Text ->+ Maybe Text ->+ Maybe Text ->+ Maybe Int ->+ Minio ListUploadsResult listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet- , riBucket = Just bucket- , riQueryParams = params- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodGet,+ riBucket = Just bucket,+ riQueryParams = params+ } parseListUploadsResponse $ NC.responseBody resp where -- build query params- params = ("uploads", Nothing) : mkOptionalParams- [ ("prefix", prefix)- , ("delimiter", delimiter)- , ("key-marker", keyMarker)- , ("upload-id-marker", uploadIdMarker)- , ("max-uploads", show <$> maxKeys)- ]-+ params =+ ("uploads", Nothing)+ : mkOptionalParams+ [ ("prefix", prefix),+ ("delimiter", delimiter),+ ("key-marker", keyMarker),+ ("upload-id-marker", uploadIdMarker),+ ("max-uploads", show <$> maxKeys)+ ] -- | List parts of an ongoing multipart upload.-listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text- -> Maybe Text -> Minio ListPartsResult+listIncompleteParts' ::+ Bucket ->+ Object ->+ UploadId ->+ Maybe Text ->+ Maybe Text ->+ Minio ListPartsResult listIncompleteParts' bucket object uploadId maxParts partNumMarker = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet- , riBucket = Just bucket- , riObject = Just object- , riQueryParams = mkOptionalParams params- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodGet,+ riBucket = Just bucket,+ riObject = Just object,+ riQueryParams = mkOptionalParams params+ } parseListPartsResponse $ NC.responseBody resp where -- build optional query params- params = [- ("uploadId", Just uploadId)- , ("part-number-marker", partNumMarker)- , ("max-parts", maxParts)+ params =+ [ ("uploadId", Just uploadId),+ ("part-number-marker", partNumMarker),+ ("max-parts", maxParts) ] -- | Get metadata of an object. headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo headObject bucket object reqHeaders = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead- , riBucket = Just bucket- , riObject = Just object- , riHeaders = reqHeaders- }-- maybe (throwIO MErrVInvalidObjectInfoResponse) return $- parseGetObjectHeaders object $ NC.responseHeaders resp-+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodHead,+ riBucket = Just bucket,+ riObject = Just object,+ riHeaders = reqHeaders+ -- This header is required for safety as otherwise http-client,+ -- sends Accept-Encoding: gzip, and the server may actually gzip+ -- body. In that case Content-Length header will be missing.+ <> [("Accept-Encoding", "identity")]+ }+ maybe (throwIO MErrVInvalidObjectInfoResponse) return+ $ parseGetObjectHeaders object+ $ NC.responseHeaders resp -- | Query the object store if a given bucket exists. headBucket :: Bucket -> Minio Bool-headBucket bucket = headBucketEx `catches`- [ Handler handleNoSuchBucket- , Handler handleStatus404- ]-+headBucket bucket =+ headBucketEx+ `catches` [ Handler handleNoSuchBucket,+ Handler handleStatus404+ ] where handleNoSuchBucket :: ServiceErr -> Minio Bool- handleNoSuchBucket e | e == NoSuchBucket = return False- | otherwise = throwIO e-+ handleNoSuchBucket e+ | e == NoSuchBucket = return False+ | otherwise = throwIO e handleStatus404 :: NC.HttpException -> Minio Bool handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) = if NC.responseStatus res == status404- then return False- else throwIO e+ then return False+ else throwIO e handleStatus404 e = throwIO e- headBucketEx = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead- , riBucket = Just bucket- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodHead,+ riBucket = Just bucket+ } return $ NC.responseStatus resp == HT.ok200 -- | Set the notification configuration on a bucket. putBucketNotification :: Bucket -> Notification -> Minio () putBucketNotification bucket ncfg = do ns <- asks getSvcNamespace- void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just bucket- , riQueryParams = [("notification", Nothing)]- , riPayload = PayloadBS $- mkPutNotificationRequest ns ncfg- }+ void $ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just bucket,+ riQueryParams = [("notification", Nothing)],+ riPayload =+ PayloadBS $+ mkPutNotificationRequest ns ncfg+ } -- | Retrieve the notification configuration on a bucket. getBucketNotification :: Bucket -> Minio Notification getBucketNotification bucket = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet- , riBucket = Just bucket- , riQueryParams = [("notification", Nothing)]- }+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodGet,+ riBucket = Just bucket,+ riQueryParams = [("notification", Nothing)]+ } parseNotification $ NC.responseBody resp -- | Remove all notifications configured on a bucket.@@ -490,11 +623,14 @@ -- | Fetch the policy if any on a bucket. getBucketPolicy :: Bucket -> Minio Text getBucketPolicy bucket = do- resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet- , riBucket = Just bucket- , riQueryParams = [("policy", Nothing)]- }- return $ toS $ NC.responseBody resp+ resp <-+ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodGet,+ riBucket = Just bucket,+ riQueryParams = [("policy", Nothing)]+ }+ return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp -- | Set a new policy on a bucket. -- As a special condition if the policy is empty@@ -506,18 +642,22 @@ else putBucketPolicy bucket policy -- | Save a new policy on a bucket.-putBucketPolicy :: Bucket -> Text -> Minio()+putBucketPolicy :: Bucket -> Text -> Minio () putBucketPolicy bucket policy = do- void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut- , riBucket = Just bucket- , riQueryParams = [("policy", Nothing)]- , riPayload = PayloadBS $ encodeUtf8 policy- }+ void $ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodPut,+ riBucket = Just bucket,+ riQueryParams = [("policy", Nothing)],+ riPayload = PayloadBS $ encodeUtf8 policy+ } -- | Delete any policy set on a bucket.-deleteBucketPolicy :: Bucket -> Minio()+deleteBucketPolicy :: Bucket -> Minio () deleteBucketPolicy bucket = do- void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete- , riBucket = Just bucket- , riQueryParams = [("policy", Nothing)]- }+ void $ executeRequest $+ defaultS3ReqInfo+ { riMethod = HT.methodDelete,+ riBucket = Just bucket,+ riQueryParams = [("policy", Nothing)]+ }
src/Network/Minio/SelectAPI.hs view
@@ -15,113 +15,103 @@ -- module Network.Minio.SelectAPI- (-- -- | The `selectObjectContent` allows querying CSV, JSON or Parquet- -- format objects in AWS S3 and in MinIO using SQL Select- -- statements. This allows significant reduction of data transfer- -- from object storage for computation-intensive tasks, as relevant- -- data is filtered close to the storage.-- selectObjectContent-- , SelectRequest- , selectRequest-- -- *** Input Serialization-- , InputSerialization- , defaultCsvInput- , linesJsonInput- , documentJsonInput- , defaultParquetInput- , setInputCSVProps-- , CompressionType(..)- , setInputCompressionType-- -- *** CSV Format details-- -- | CSV format options such as delimiters and quote characters are- -- specified using using the functions below. Options are combined- -- monoidally.-- , CSVProp- , recordDelimiter- , fieldDelimiter- , quoteCharacter- , quoteEscapeCharacter- , commentCharacter- , allowQuotedRecordDelimiter- , FileHeaderInfo(..)- , fileHeaderInfo- , QuoteFields(..)- , quoteFields-- -- *** Output Serialization-- , OutputSerialization- , defaultCsvOutput- , defaultJsonOutput- , outputCSVFromProps- , outputJSONFromRecordDelimiter+ ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet+ -- format objects in AWS S3 and in MinIO using SQL Select+ -- statements. This allows significant reduction of data transfer+ -- from object storage for computation-intensive tasks, as relevant+ -- data is filtered close to the storage.+ selectObjectContent,+ SelectRequest,+ selectRequest, - -- *** Progress messages+ -- *** Input Serialization+ InputSerialization,+ defaultCsvInput,+ linesJsonInput,+ documentJsonInput,+ defaultParquetInput,+ setInputCSVProps,+ CompressionType (..),+ setInputCompressionType, - , setRequestProgressEnabled+ -- *** CSV Format details - -- *** Interpreting Select output+ -- | CSV format options such as delimiters and quote characters are+ -- specified using using the functions below. Options are combined+ -- monoidally.+ CSVProp,+ recordDelimiter,+ fieldDelimiter,+ quoteCharacter,+ quoteEscapeCharacter,+ commentCharacter,+ allowQuotedRecordDelimiter,+ FileHeaderInfo (..),+ fileHeaderInfo,+ QuoteFields (..),+ quoteFields, - -- | The conduit returned by `selectObjectContent` returns values of- -- the `EventMessage` data type. This returns the query output- -- messages formatted according to the chosen output serialization,- -- interleaved with progress messages (if enabled by- -- `setRequestProgressEnabled`), and at the end a statistics- -- message.- --- -- If the application is interested in only the payload, then- -- `getPayloadBytes` can be used. For example to simply print the- -- payload to stdout:- --- -- > resultConduit <- selectObjectContent bucket object mySelectRequest- -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC- --- -- Note that runConduit, the connect operator (.|) and stdoutC are- -- all from the "conduit" package.+ -- *** Output Serialization+ OutputSerialization,+ defaultCsvOutput,+ defaultJsonOutput,+ outputCSVFromProps,+ outputJSONFromRecordDelimiter, - , getPayloadBytes- , EventMessage(..)- , Progress(..)- , Stats- ) where+ -- *** Progress messages+ setRequestProgressEnabled, -import Conduit ((.|))-import qualified Conduit as C-import qualified Data.Binary as Bin-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as LB-import Data.Digest.CRC32 (crc32, crc32Update)-import qualified Network.HTTP.Conduit as NC-import qualified Network.HTTP.Types as HT-import UnliftIO (MonadUnliftIO)+ -- *** Interpreting Select output -import Lib.Prelude+ -- | The conduit returned by `selectObjectContent` returns values of+ -- the `EventMessage` data type. This returns the query output+ -- messages formatted according to the chosen output serialization,+ -- interleaved with progress messages (if enabled by+ -- `setRequestProgressEnabled`), and at the end a statistics+ -- message.+ --+ -- If the application is interested in only the payload, then+ -- `getPayloadBytes` can be used. For example to simply print the+ -- payload to stdout:+ --+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC+ --+ -- Note that runConduit, the connect operator (.|) and stdoutC are+ -- all from the "conduit" package.+ getPayloadBytes,+ EventMessage (..),+ Progress (..),+ Stats,+ )+where -import Network.Minio.API-import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.Utils-import Network.Minio.XmlGenerator-import Network.Minio.XmlParser+import Conduit ((.|))+import qualified Conduit as C+import qualified Data.Binary as Bin+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as LB+import Data.Digest.CRC32 (crc32, crc32Update)+import Lib.Prelude+import qualified Network.HTTP.Conduit as NC+import qualified Network.HTTP.Types as HT+import Network.Minio.API+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.Utils+import Network.Minio.XmlGenerator+import Network.Minio.XmlParser+import UnliftIO (MonadUnliftIO) -data EventStreamException = ESEPreludeCRCFailed- | ESEMessageCRCFailed- | ESEUnexpectedEndOfStream- | ESEDecodeFail [Char]- | ESEInvalidHeaderType- | ESEInvalidHeaderValueType- | ESEInvalidMessageType- deriving (Eq, Show)+data EventStreamException+ = ESEPreludeCRCFailed+ | ESEMessageCRCFailed+ | ESEUnexpectedEndOfStream+ | ESEDecodeFail [Char]+ | ESEInvalidHeaderType+ | ESEInvalidHeaderValueType+ | ESEInvalidMessageType+ deriving (Eq, Show) instance Exception EventStreamException @@ -131,169 +121,174 @@ parseBinary :: Bin.Binary a => ByteString -> IO a parseBinary b = do- case Bin.decodeOrFail $ LB.fromStrict b of- Left (_, _, msg) -> throwIO $ ESEDecodeFail msg- Right (_, _, r) -> return r+ case Bin.decodeOrFail $ LB.fromStrict b of+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg+ Right (_, _, r) -> return r bytesToHeaderName :: Text -> IO MsgHeaderName bytesToHeaderName t = case t of- ":message-type" -> return MessageType- ":event-type" -> return EventType- ":content-type" -> return ContentType- ":error-code" -> return ErrorCode+ ":message-type" -> return MessageType+ ":event-type" -> return EventType+ ":content-type" -> return ContentType+ ":error-code" -> return ErrorCode ":error-message" -> return ErrorMessage- _ -> throwIO ESEInvalidHeaderType+ _ -> throwIO ESEInvalidHeaderType -parseHeaders :: MonadUnliftIO m- => Word32 -> C.ConduitM ByteString a m [MessageHeader]+parseHeaders ::+ MonadUnliftIO m =>+ Word32 ->+ C.ConduitM ByteString a m [MessageHeader] parseHeaders 0 = return [] parseHeaders hdrLen = do- bs1 <- readNBytes 1- n :: Word8 <- liftIO $ parseBinary bs1+ bs1 <- readNBytes 1+ n :: Word8 <- liftIO $ parseBinary bs1 - headerKeyBytes <- readNBytes $ fromIntegral n- let headerKey = decodeUtf8Lenient headerKeyBytes- headerName <- liftIO $ bytesToHeaderName headerKey+ headerKeyBytes <- readNBytes $ fromIntegral n+ let headerKey = decodeUtf8Lenient headerKeyBytes+ headerName <- liftIO $ bytesToHeaderName headerKey - bs2 <- readNBytes 1- headerValueType :: Word8 <- liftIO $ parseBinary bs2- when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType+ bs2 <- readNBytes 1+ headerValueType :: Word8 <- liftIO $ parseBinary bs2+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType - bs3 <- readNBytes 2- vLen :: Word16 <- liftIO $ parseBinary bs3- headerValueBytes <- readNBytes $ fromIntegral vLen- let headerValue = decodeUtf8Lenient headerValueBytes- m = (headerName, headerValue)- k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen+ bs3 <- readNBytes 2+ vLen :: Word16 <- liftIO $ parseBinary bs3+ headerValueBytes <- readNBytes $ fromIntegral vLen+ let headerValue = decodeUtf8Lenient headerValueBytes+ m = (headerName, headerValue)+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen - ms <- parseHeaders (hdrLen - k)- return (m:ms)+ ms <- parseHeaders (hdrLen - k)+ return (m : ms) -- readNBytes returns N bytes read from the string and throws an -- exception if N bytes are not present on the stream. readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString readNBytes n = do- b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)- if B.length b /= n- then throwIO ESEUnexpectedEndOfStream- else return b+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)+ if B.length b /= n+ then throwIO ESEUnexpectedEndOfStream+ else return b -crcCheck :: MonadUnliftIO m- => C.ConduitM ByteString ByteString m ()+crcCheck ::+ MonadUnliftIO m =>+ C.ConduitM ByteString ByteString m () crcCheck = do- b <- readNBytes 12- n :: Word32 <- liftIO $ parseBinary $ B.take 4 b- preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b- when (crc32 (B.take 8 b) /= preludeCRC) $- throwIO ESEPreludeCRCFailed+ b <- readNBytes 12+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b+ when (crc32 (B.take 8 b) /= preludeCRC) $+ throwIO ESEPreludeCRCFailed - -- we do not yield the checksum- C.yield $ B.take 8 b+ -- we do not yield the checksum+ C.yield $ B.take 8 b - -- 12 bytes have been read off the current message. Now read the- -- next (n-12)-4 bytes and accumulate the checksum, and yield it.- let startCrc = crc32 b- finalCrc <- accumulateYield (fromIntegral n-16) startCrc+ -- 12 bytes have been read off the current message. Now read the+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.+ let startCrc = crc32 b+ finalCrc <- accumulateYield (fromIntegral n -16) startCrc - bs <- readNBytes 4- expectedCrc :: Word32 <- liftIO $ parseBinary bs+ bs <- readNBytes 4+ expectedCrc :: Word32 <- liftIO $ parseBinary bs - when (finalCrc /= expectedCrc) $- throwIO ESEMessageCRCFailed+ when (finalCrc /= expectedCrc) $+ throwIO ESEMessageCRCFailed - -- we unconditionally recurse - downstream figures out when to- -- quit reading the stream- crcCheck+ -- we unconditionally recurse - downstream figures out when to+ -- quit reading the stream+ crcCheck where accumulateYield n checkSum = do- let toRead = min n chunkSize- b <- readNBytes toRead- let c' = crc32Update checkSum b- n' = n - B.length b- C.yield b- if n' > 0- then accumulateYield n' c'- else return c'+ let toRead = min n chunkSize+ b <- readNBytes toRead+ let c' = crc32Update checkSum b+ n' = n - B.length b+ C.yield b+ if n' > 0+ then accumulateYield n' c'+ else return c' handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m () handleMessage = do- b1 <- readNBytes 4- msgLen :: Word32 <- liftIO $ parseBinary b1-- b2 <- readNBytes 4- hdrLen :: Word32 <- liftIO $ parseBinary b2-- hs <- parseHeaders hdrLen-- let payloadLen = msgLen - hdrLen - 16- getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)- eventHdrValue = getHdrVal EventType hs- msgHdrValue = getHdrVal MessageType hs- errCode = getHdrVal ErrorCode hs- errMsg = getHdrVal ErrorMessage hs+ b1 <- readNBytes 4+ msgLen :: Word32 <- liftIO $ parseBinary b1 - case msgHdrValue of- Just "event" -> do- case eventHdrValue of- Just "Records" -> passThrough $ fromIntegral payloadLen- Just "Cont" -> return ()- Just "Progress" -> do- bs <- readNBytes $ fromIntegral payloadLen- progress <- parseSelectProgress bs- C.yield $ ProgressEventMessage progress- Just "Stats" -> do- bs <- readNBytes $ fromIntegral payloadLen- stats <- parseSelectProgress bs- C.yield $ StatsEventMessage stats- Just "End" -> return ()- _ -> throwIO ESEInvalidMessageType- when (eventHdrValue /= Just "End") handleMessage+ b2 <- readNBytes 4+ hdrLen :: Word32 <- liftIO $ parseBinary b2 - Just "error" -> do- let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg- maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay+ hs <- parseHeaders hdrLen - _ -> throwIO ESEInvalidMessageType+ let payloadLen = msgLen - hdrLen - 16+ getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)+ eventHdrValue = getHdrVal EventType hs+ msgHdrValue = getHdrVal MessageType hs+ errCode = getHdrVal ErrorCode hs+ errMsg = getHdrVal ErrorMessage hs + case msgHdrValue of+ Just "event" -> do+ case eventHdrValue of+ Just "Records" -> passThrough $ fromIntegral payloadLen+ Just "Cont" -> return ()+ Just "Progress" -> do+ bs <- readNBytes $ fromIntegral payloadLen+ progress <- parseSelectProgress bs+ C.yield $ ProgressEventMessage progress+ Just "Stats" -> do+ bs <- readNBytes $ fromIntegral payloadLen+ stats <- parseSelectProgress bs+ C.yield $ StatsEventMessage stats+ Just "End" -> return ()+ _ -> throwIO ESEInvalidMessageType+ when (eventHdrValue /= Just "End") handleMessage+ Just "error" -> do+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay+ _ -> throwIO ESEInvalidMessageType where passThrough 0 = return () passThrough n = do- let c = min n chunkSize- b <- readNBytes c- C.yield $ RecordPayloadEventMessage b- passThrough $ n - B.length b-+ let c = min n chunkSize+ b <- readNBytes c+ C.yield $ RecordPayloadEventMessage b+ passThrough $ n - B.length b -selectProtoConduit :: MonadUnliftIO m- => C.ConduitT ByteString EventMessage m ()+selectProtoConduit ::+ MonadUnliftIO m =>+ C.ConduitT ByteString EventMessage m () selectProtoConduit = crcCheck .| handleMessage -- | selectObjectContent calls the SelectRequest on the given -- object. It returns a Conduit of event messages that can be consumed -- by the client.-selectObjectContent :: Bucket -> Object -> SelectRequest- -> Minio (C.ConduitT () EventMessage Minio ())+selectObjectContent ::+ Bucket ->+ Object ->+ SelectRequest ->+ Minio (C.ConduitT () EventMessage Minio ()) selectObjectContent b o r = do- let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost- , riBucket = Just b- , riObject = Just o- , riPayload = PayloadBS $ mkSelectRequest r- , riNeedsLocation = False- , riQueryParams = [("select", Nothing), ("select-type", Just "2")]- }- --print $ mkSelectRequest r- resp <- mkStreamRequest reqInfo- return $ NC.responseBody resp .| selectProtoConduit+ let reqInfo =+ defaultS3ReqInfo+ { riMethod = HT.methodPost,+ riBucket = Just b,+ riObject = Just o,+ riPayload = PayloadBS $ mkSelectRequest r,+ riNeedsLocation = False,+ riQueryParams = [("select", Nothing), ("select-type", Just "2")]+ }+ --print $ mkSelectRequest r+ resp <- mkStreamRequest reqInfo+ return $ NC.responseBody resp .| selectProtoConduit -- | A helper conduit that returns only the record payload bytes. getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m () getPayloadBytes = do- evM <- C.await- case evM of- Just v -> do- case v of- RecordPayloadEventMessage b -> C.yield b- RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m- _ -> return ()- getPayloadBytes- Nothing -> return ()+ evM <- C.await+ case evM of+ Just v -> do+ case v of+ RecordPayloadEventMessage b -> C.yield b+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m+ _ -> return ()+ getPayloadBytes+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs view
@@ -13,57 +13,62 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --+{-# LANGUAGE BangPatterns #-} module Network.Minio.Sign.V4 where -import qualified Conduit as C-import qualified Data.ByteString as B-import qualified Data.ByteString.Base64 as Base64-import qualified Data.ByteString.Char8 as B8-import Data.CaseInsensitive (mk)-import qualified Data.CaseInsensitive as CI-import qualified Data.HashMap.Strict as Map-import qualified Data.HashSet as Set-import qualified Data.Time as Time-import qualified Network.HTTP.Conduit as NC-import Network.HTTP.Types (Header, parseQuery)-import qualified Network.HTTP.Types as H-import Text.Printf (printf)--import Lib.Prelude--import Network.Minio.Data.ByteString-import Network.Minio.Data.Crypto-import Network.Minio.Data.Time-import Network.Minio.Errors+import qualified Conduit as C+import qualified Data.ByteString as B+import qualified Data.ByteString.Base64 as Base64+import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString.Lazy as LB+import Data.CaseInsensitive (mk)+import qualified Data.CaseInsensitive as CI+import qualified Data.HashMap.Strict as Map+import qualified Data.HashSet as Set+import qualified Data.Time as Time+import Lib.Prelude+import qualified Network.HTTP.Conduit as NC+import Network.HTTP.Types (Header, parseQuery)+import qualified Network.HTTP.Types as H+import Network.Minio.Data.ByteString+import Network.Minio.Data.Crypto+import Network.Minio.Data.Time+import Network.Minio.Errors+import Text.Printf (printf) -- these headers are not included in the string to sign when signing a -- request ignoredHeaders :: Set.HashSet ByteString-ignoredHeaders = Set.fromList $ map CI.foldedCase- [ H.hAuthorization- , H.hContentType- , H.hUserAgent- ]+ignoredHeaders =+ Set.fromList $+ map+ CI.foldedCase+ [ H.hAuthorization,+ H.hContentType,+ H.hUserAgent+ ] -data SignV4Data = SignV4Data {- sv4SignTime :: UTCTime- , sv4Scope :: ByteString- , sv4CanonicalRequest :: ByteString- , sv4HeadersToSign :: [(ByteString, ByteString)]- , sv4Output :: [(ByteString, ByteString)]- , sv4StringToSign :: ByteString- , sv4SigningKey :: ByteString- } deriving (Show)+data SignV4Data = SignV4Data+ { sv4SignTime :: UTCTime,+ sv4Scope :: ByteString,+ sv4CanonicalRequest :: ByteString,+ sv4HeadersToSign :: [(ByteString, ByteString)],+ sv4Output :: [(ByteString, ByteString)],+ sv4StringToSign :: ByteString,+ sv4SigningKey :: ByteString+ }+ deriving (Show) -data SignParams = SignParams {- spAccessKey :: Text- , spSecretKey :: Text- , spTimeStamp :: UTCTime- , spRegion :: Maybe Text- , spExpirySecs :: Maybe Int- , spPayloadHash :: Maybe ByteString- } deriving (Show)+data SignParams = SignParams+ { spAccessKey :: Text,+ spSecretKey :: Text,+ spTimeStamp :: UTCTime,+ spRegion :: Maybe Text,+ spExpirySecs :: Maybe Int,+ spPayloadHash :: Maybe ByteString+ }+ deriving (Show) debugPrintSignV4Data :: SignV4Data -> IO () debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do@@ -79,22 +84,23 @@ B8.putStrLn "END of SignV4Data =========" where printBytes b = do- mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b+ mapM_ (\x -> B.putStr $ B.singleton x <> " ") $ B.unpack b B8.putStrLn "" mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header mkAuthHeader accessKey scope signedHeaderKeys sign =- let authValue = B.concat- [ "AWS4-HMAC-SHA256 Credential="- , toS accessKey- , "/"- , scope- , ", SignedHeaders="- , signedHeaderKeys- , ", Signature="- , sign- ]- in (H.hAuthorization, authValue)+ let authValue =+ B.concat+ [ "AWS4-HMAC-SHA256 Credential=",+ toUtf8 accessKey,+ "/",+ scope,+ ", SignedHeaders=",+ signedHeaderKeys,+ ", Signature=",+ sign+ ]+ in (H.hAuthorization, authValue) -- | Given SignParams and request details, including request method, -- request path, headers, query params and payload hash, generates an@@ -110,122 +116,132 @@ -- the request. signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)] signV4 !sp !req =- let- region = fromMaybe "" $ spRegion sp- ts = spTimeStamp sp- scope = mkScope ts region- accessKey = toS $ spAccessKey sp- secretKey = toS $ spSecretKey sp- expiry = spExpirySecs sp-- -- headers to be added to the request- datePair = ("X-Amz-Date", awsTimeFormatBS ts)- computedHeaders = NC.requestHeaders req ++- if isJust $ expiry- then []- else [(\(x, y) -> (mk x, y)) datePair]- headersToSign = getHeadersToSign computedHeaders- signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign-- -- 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 = parseQuery (NC.queryString req) ++- if isJust expiry- then (fmap . fmap) Just authQP- else []-- -- 1. compute canonical request- canonicalRequest = mkCanonicalRequest False sp- (NC.setQueryString finalQP req)- headersToSign-- -- 2. compute string to sign- stringToSign = mkStringToSign ts scope canonicalRequest-- -- 3.1 compute signing key- signingKey = mkSigningKey ts region secretKey-- -- 3.2 compute signature- signature = computeSignature stringToSign signingKey-- -- 4. compute auth header- authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature-- -- finally compute output pairs- sha256Hdr = ("x-amz-content-sha256",- fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)- output = if isJust expiry- then ("X-Amz-Signature", signature) : authQP- else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,- datePair, sha256Hdr]-- in output-+ let region = fromMaybe "" $ spRegion sp+ ts = spTimeStamp sp+ scope = mkScope ts region+ accessKey = toUtf8 $ spAccessKey sp+ secretKey = toUtf8 $ spSecretKey sp+ expiry = spExpirySecs sp+ sha256Hdr =+ ( "x-amz-content-sha256",+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp+ )+ -- headers to be added to the request+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)+ computedHeaders =+ NC.requestHeaders req+ ++ if isJust $ expiry+ then []+ else map (\(x, y) -> (mk x, y)) [datePair, sha256Hdr]+ headersToSign = getHeadersToSign computedHeaders+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign+ -- 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 "" showBS expiry),+ ("X-Amz-SignedHeaders", signedHeaderKeys)+ ]+ finalQP =+ parseQuery (NC.queryString req)+ ++ if isJust expiry+ then (fmap . fmap) Just authQP+ else []+ -- 1. compute canonical request+ canonicalRequest =+ mkCanonicalRequest+ False+ sp+ (NC.setQueryString finalQP req)+ headersToSign+ -- 2. compute string to sign+ stringToSign = mkStringToSign ts scope canonicalRequest+ -- 3.1 compute signing key+ signingKey = mkSigningKey ts region secretKey+ -- 3.2 compute signature+ signature = computeSignature stringToSign signingKey+ -- 4. compute auth header+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature+ -- finally compute output pairs+ output =+ if isJust expiry+ then ("X-Amz-Signature", signature) : authQP+ else+ [ (\(x, y) -> (CI.foldedCase x, y)) authHeader,+ datePair,+ sha256Hdr+ ]+ in output mkScope :: UTCTime -> Text -> ByteString-mkScope ts region = B.intercalate "/"- [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts- , toS region- , "s3"- , "aws4_request"- ]+mkScope ts region =+ B.intercalate+ "/"+ [ toUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,+ toUtf8 region,+ "s3",+ "aws4_request"+ ] getHeadersToSign :: [Header] -> [(ByteString, ByteString)] getHeadersToSign !h = filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $- map (\(x, y) -> (CI.foldedCase x, stripBS y)) h+ map (\(x, y) -> (CI.foldedCase x, stripBS y)) h -mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]- -> ByteString+mkCanonicalRequest ::+ Bool ->+ SignParams ->+ NC.Request ->+ [(ByteString, ByteString)] ->+ ByteString mkCanonicalRequest !isStreaming !sp !req !headersForSign =- let- canonicalQueryString = B.intercalate "&" $- map (\(x, y) -> B.concat [x, "=", y]) $- sort $ map (\(x, y) ->- (uriEncode True x, maybe "" (uriEncode True) y)) $- (parseQuery $ NC.queryString req)-- sortedHeaders = sort headersForSign-- canonicalHeaders = B.concat $- map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders-- signedHeaders = B.intercalate ";" $ map fst sortedHeaders-- payloadHashStr =+ let canonicalQueryString =+ B.intercalate "&"+ $ map (\(x, y) -> B.concat [x, "=", y])+ $ sort+ $ map+ ( \(x, y) ->+ (uriEncode True x, maybe "" (uriEncode True) y)+ )+ $ (parseQuery $ NC.queryString req)+ sortedHeaders = sort headersForSign+ canonicalHeaders =+ B.concat $+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders+ payloadHashStr = if isStreaming- then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"- else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp- in- B.intercalate "\n"- [ NC.method req- , uriEncode False $ NC.path req- , canonicalQueryString- , canonicalHeaders- , signedHeaders- , payloadHashStr- ]+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp+ in B.intercalate+ "\n"+ [ NC.method req,+ uriEncode False $ NC.path req,+ canonicalQueryString,+ canonicalHeaders,+ signedHeaders,+ payloadHashStr+ ] mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString-mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"- [ "AWS4-HMAC-SHA256"- , awsTimeFormatBS ts- , scope- , hashSHA256 canonicalRequest- ]+mkStringToSign ts !scope !canonicalRequest =+ B.intercalate+ "\n"+ [ "AWS4-HMAC-SHA256",+ awsTimeFormatBS ts,+ scope,+ hashSHA256 canonicalRequest+ ] mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString-mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"- . hmacSHA256RawBS "s3"- . hmacSHA256RawBS (toS region)- . hmacSHA256RawBS (awsDateFormatBS ts)- $ B.concat ["AWS4", secretKey]+mkSigningKey ts region !secretKey =+ hmacSHA256RawBS "aws4_request"+ . hmacSHA256RawBS "s3"+ . hmacSHA256RawBS (toUtf8 region)+ . hmacSHA256RawBS (awsDateFormatBS ts)+ $ B.concat ["AWS4", secretKey] computeSignature :: ByteString -> ByteString -> ByteString computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key@@ -233,18 +249,19 @@ -- | 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 -> SignParams- -> Map.HashMap Text ByteString+signV4PostPolicy ::+ ByteString ->+ SignParams ->+ Map.HashMap Text ByteString signV4PostPolicy !postPolicyJSON !sp =- let- stringToSign = Base64.encode postPolicyJSON- region = fromMaybe "" $ spRegion sp- signingKey = mkSigningKey (spTimeStamp sp) region $ toS $ spSecretKey sp- signature = computeSignature stringToSign signingKey- in- Map.fromList [ ("x-amz-signature", signature)- , ("policy", stringToSign)- ]+ let stringToSign = Base64.encode postPolicyJSON+ region = fromMaybe "" $ spRegion sp+ signingKey = mkSigningKey (spTimeStamp sp) region $ toUtf8 $ spSecretKey sp+ signature = computeSignature stringToSign signingKey+ in Map.fromList+ [ ("x-amz-signature", signature),+ ("policy", stringToSign)+ ] chunkSizeConstant :: Int chunkSizeConstant = 64 * 1024@@ -252,140 +269,141 @@ -- base16Len computes the number of bytes required to represent @n (> 0)@ in -- hexadecimal. base16Len :: Integral a => a -> Int-base16Len n | n == 0 = 0- | otherwise = 1 + base16Len (n `div` 16)+base16Len n+ | n == 0 = 0+ | otherwise = 1 + base16Len (n `div` 16) signedStreamLength :: Int64 -> Int64 signedStreamLength dataLen =- let- chunkSzInt = fromIntegral chunkSizeConstant- (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt--- -- Structure of a chunk:- -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n- encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2- fullChunkSize = encodedChunkLen chunkSzInt- lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0- finalChunkSize = 1 + 17 + 64 + 2 + 2- in- numChunks * fullChunkSize + lastChunkSize + finalChunkSize+ let chunkSzInt = fromIntegral chunkSizeConstant+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt+ -- Structure of a chunk:+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2+ fullChunkSize = encodedChunkLen chunkSzInt+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0+ finalChunkSize = 1 + 17 + 64 + 2 + 2+ in numChunks * fullChunkSize + lastChunkSize + finalChunkSize -signV4Stream :: Int64 -> SignParams -> NC.Request- -> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)- -- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)+signV4Stream ::+ Int64 ->+ SignParams ->+ NC.Request ->+ (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)+-- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody) signV4Stream !payloadLength !sp !req =- let- ts = spTimeStamp sp-- addContentEncoding hs =+ let ts = spTimeStamp sp+ addContentEncoding hs = let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs- in case ceMay of- Nothing -> ("content-encoding", "aws-chunked") : hs- Just (_, ce) -> ("content-encoding", ce <> ",aws-chunked") :- filter (\(x, _) -> x /= "content-encoding") hs-- -- headers to be added to the request- datePair = ("X-Amz-Date", awsTimeFormatBS ts)- computedHeaders = addContentEncoding $- datePair : NC.requestHeaders req-- -- headers specific to streaming signature- signedContentLength = signedStreamLength payloadLength- streamingHeaders :: [Header]- streamingHeaders =- [ ("x-amz-decoded-content-length", show payloadLength)- , ("content-length", show signedContentLength )- , ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")- ]- headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders- signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign- finalQP = parseQuery (NC.queryString req)-- -- 1. Compute Seed Signature- -- 1.1 Canonical Request- canonicalReq = mkCanonicalRequest True sp- (NC.setQueryString finalQP req)- headersToSign-- region = fromMaybe "" $ spRegion sp- scope = mkScope ts region- accessKey = spAccessKey sp- secretKey = spSecretKey sp-- -- 1.2 String toSign- stringToSign = mkStringToSign ts scope canonicalReq-- -- 1.3 Compute signature- -- 1.3.1 compute signing key- signingKey = mkSigningKey ts region $ toS secretKey-- -- 1.3.2 Compute signature- seedSignature = computeSignature stringToSign signingKey-- -- 1.3.3 Compute Auth Header- authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature-- -- 1.4 Updated headers for the request- finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)- -- headersToAdd = authHeader : datePair : streamingHeaders-- toHexStr n = B8.pack $ printf "%x" n-- (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant-- -- Function to compute string to sign for each chunk.- chunkStrToSign prevSign currChunkHash =- B.intercalate "\n"- [ "AWS4-HMAC-SHA256-PAYLOAD"- , awsTimeFormatBS ts- , scope- , prevSign- , hashSHA256 ""- , currChunkHash+ in case ceMay of+ Nothing -> ("content-encoding", "aws-chunked") : hs+ Just (_, ce) ->+ ("content-encoding", ce <> ",aws-chunked")+ : filter (\(x, _) -> x /= "content-encoding") hs+ -- headers to be added to the request+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)+ computedHeaders =+ addContentEncoding $+ datePair : NC.requestHeaders req+ -- headers specific to streaming signature+ signedContentLength = signedStreamLength payloadLength+ streamingHeaders :: [Header]+ streamingHeaders =+ [ ("x-amz-decoded-content-length", showBS payloadLength),+ ("content-length", showBS signedContentLength),+ ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD") ]+ headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign+ finalQP = parseQuery (NC.queryString req)+ -- 1. Compute Seed Signature+ -- 1.1 Canonical Request+ canonicalReq =+ mkCanonicalRequest+ True+ sp+ (NC.setQueryString finalQP req)+ headersToSign+ region = fromMaybe "" $ spRegion sp+ scope = mkScope ts region+ accessKey = spAccessKey sp+ secretKey = spSecretKey sp+ -- 1.2 String toSign+ stringToSign = mkStringToSign ts scope canonicalReq+ -- 1.3 Compute signature+ -- 1.3.1 compute signing key+ signingKey = mkSigningKey ts region $ toUtf8 secretKey+ -- 1.3.2 Compute signature+ seedSignature = computeSignature stringToSign signingKey+ -- 1.3.3 Compute Auth Header+ authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature+ -- 1.4 Updated headers for the request+ finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)+ -- headersToAdd = authHeader : datePair : streamingHeaders - -- Read n byte from upstream and return a strict bytestring.- mustTakeN n = do- bs <- toS <$> (C.takeCE n C..| C.sinkLazy)+ toHexStr n = B8.pack $ printf "%x" n+ (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant+ -- Function to compute string to sign for each chunk.+ chunkStrToSign prevSign currChunkHash =+ B.intercalate+ "\n"+ [ "AWS4-HMAC-SHA256-PAYLOAD",+ awsTimeFormatBS ts,+ scope,+ prevSign,+ hashSHA256 "",+ currChunkHash+ ]+ -- Read n byte from upstream and return a strict bytestring.+ mustTakeN n = do+ bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy) when (B.length bs /= n) $- throwIO MErrVStreamingBodyUnexpectedEOF+ throwIO MErrVStreamingBodyUnexpectedEOF return bs-- signerConduit n lps prevSign =- -- First case encodes a full chunk of length- -- 'chunkSizeConstant'.- if | n > 0 -> do- bs <- mustTakeN chunkSizeConstant- let strToSign = chunkStrToSign prevSign (hashSHA256 bs)- nextSign = computeSignature strToSign signingKey- chunkBS = toHexStr chunkSizeConstant- <> ";chunk-signature="- <> nextSign <> "\r\n" <> bs <> "\r\n"- C.yield chunkBS- signerConduit (n-1) lps nextSign+ signerConduit n lps prevSign =+ -- First case encodes a full chunk of length+ -- 'chunkSizeConstant'.+ if+ | n > 0 -> do+ bs <- mustTakeN chunkSizeConstant+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)+ nextSign = computeSignature strToSign signingKey+ chunkBS =+ toHexStr chunkSizeConstant+ <> ";chunk-signature="+ <> nextSign+ <> "\r\n"+ <> bs+ <> "\r\n"+ C.yield chunkBS+ signerConduit (n -1) lps nextSign - -- Second case encodes the last chunk which is smaller than- -- 'chunkSizeConstant'- | lps > 0 -> do- bs <- mustTakeN $ fromIntegral lps- let strToSign = chunkStrToSign prevSign (hashSHA256 bs)- nextSign = computeSignature strToSign signingKey- chunkBS = toHexStr lps <> ";chunk-signature="- <> nextSign <> "\r\n" <> bs <> "\r\n"- C.yield chunkBS- signerConduit 0 0 nextSign+ -- Second case encodes the last chunk which is smaller than+ -- 'chunkSizeConstant'+ | lps > 0 -> do+ bs <- mustTakeN $ fromIntegral lps+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)+ nextSign = computeSignature strToSign signingKey+ chunkBS =+ toHexStr lps <> ";chunk-signature="+ <> nextSign+ <> "\r\n"+ <> bs+ <> "\r\n"+ C.yield chunkBS+ signerConduit 0 0 nextSign - -- Last case encodes the final signature chunk that has no- -- data.- | otherwise -> do- let strToSign = chunkStrToSign prevSign (hashSHA256 "")- nextSign = computeSignature strToSign signingKey- lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"- C.yield lastChunkBS- in- \src -> req { NC.requestHeaders = finalReqHeaders- , NC.requestBody =- NC.requestBodySource signedContentLength $- src C..| signerConduit numParts lastPSize seedSignature- }+ -- Last case encodes the final signature chunk that has no+ -- data.+ | otherwise -> do+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")+ nextSign = computeSignature strToSign signingKey+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"+ C.yield lastChunkBS+ in \src ->+ req+ { NC.requestHeaders = finalReqHeaders,+ NC.requestBody =+ NC.requestBodySource signedContentLength $+ src C..| signerConduit numParts lastPSize seedSignature+ }
src/Network/Minio/Utils.hs view
@@ -16,37 +16,40 @@ module Network.Minio.Utils where -import qualified Conduit as C-import Control.Monad.IO.Unlift (MonadUnliftIO)-import qualified Control.Monad.Trans.Resource as R-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as LB-import Data.CaseInsensitive (mk, original)-import qualified Data.Conduit.Binary as CB-import qualified Data.HashMap.Strict as H-import qualified Data.List as List-import qualified Data.Text as T-import Data.Text.Read (decimal)-import Data.Time (defaultTimeLocale, parseTimeM,- rfc822DateFormat)-import Network.HTTP.Conduit (Response)-import qualified Network.HTTP.Conduit as NC-import qualified Network.HTTP.Types as HT-import qualified Network.HTTP.Types.Header as Hdr-import qualified System.IO as IO-import qualified UnliftIO as U-import qualified UnliftIO.Async as A-import qualified UnliftIO.MVar as UM--import Lib.Prelude--import Network.Minio.Data-import Network.Minio.Data.ByteString-import Network.Minio.JsonParser (parseErrResponseJSON)-import Network.Minio.XmlParser (parseErrResponse)+import qualified Conduit as C+import Control.Monad.IO.Unlift (MonadUnliftIO)+import qualified Control.Monad.Trans.Resource as R+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as LB+import Data.CaseInsensitive (mk, original)+import qualified Data.Conduit.Binary as CB+import qualified Data.HashMap.Strict as H+import qualified Data.List as List+import qualified Data.Text as T+import Data.Text.Read (decimal)+import Data.Time+ ( defaultTimeLocale,+ parseTimeM,+ rfc822DateFormat,+ )+import Lib.Prelude+import Network.HTTP.Conduit (Response)+import qualified Network.HTTP.Conduit as NC+import qualified Network.HTTP.Types as HT+import qualified Network.HTTP.Types.Header as Hdr+import Network.Minio.Data+import Network.Minio.Data.ByteString+import Network.Minio.JsonParser (parseErrResponseJSON)+import Network.Minio.XmlParser (parseErrResponse)+import qualified System.IO as IO+import qualified UnliftIO as U+import qualified UnliftIO.Async as A+import qualified UnliftIO.MVar as UM -allocateReadFile :: (MonadUnliftIO m, R.MonadResource m)- => FilePath -> m (R.ReleaseKey, Handle)+allocateReadFile ::+ (MonadUnliftIO m, R.MonadResource m) =>+ FilePath ->+ m (R.ReleaseKey, Handle) allocateReadFile fp = do (rk, hdlE) <- R.allocate (openReadFile fp) cleanup either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE@@ -56,30 +59,37 @@ -- | Queries the file size from the handle. Catches any file operation -- exceptions and returns Nothing instead.-getFileSize :: (MonadUnliftIO m, R.MonadResource m)- => Handle -> m (Maybe Int64)+getFileSize ::+ (MonadUnliftIO m, R.MonadResource m) =>+ Handle ->+ m (Maybe Int64) getFileSize h = do resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h case resE of Left (_ :: IOException) -> return Nothing- Right s -> return $ Just s+ Right s -> return $ Just s -- | Queries if handle is seekable. Catches any file operation -- exceptions and return False instead.-isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)- => Handle -> m Bool+isHandleSeekable ::+ (R.MonadResource m, MonadUnliftIO m) =>+ Handle ->+ m Bool isHandleSeekable h = do resE <- liftIO $ try $ IO.hIsSeekable h case resE of Left (_ :: IOException) -> return False- Right v -> return v+ Right v -> return v -- | Helper function that opens a handle to the filepath and performs -- the given action on it. Exceptions of type MError are caught and -- returned - both during file handle allocation and when the action -- is run.-withNewHandle :: (MonadUnliftIO m, R.MonadResource m)- => FilePath -> (Handle -> m a) -> m (Either IOException a)+withNewHandle ::+ (MonadUnliftIO m, R.MonadResource m) =>+ FilePath ->+ (Handle -> m a) ->+ m (Either IOException a) withNewHandle fp fileAction = do -- opening a handle can throw MError exception. handleE <- try $ allocateReadFile fp@@ -103,24 +113,27 @@ getMetadata :: [HT.Header] -> [(Text, Text)] getMetadata =- map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))+ map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))) toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text) toMaybeMetadataHeader (k, v) =- (, v) <$> userMetadataHeaderNameMaybe k+ (,v) <$> userMetadataHeaderNameMaybe k getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text-getNonUserMetadataMap = H.fromList- . filter ( isNothing- . userMetadataHeaderNameMaybe- . fst- )+getNonUserMetadataMap =+ H.fromList+ . filter+ ( isNothing+ . userMetadataHeaderNameMaybe+ . fst+ ) -- | This function collects all headers starting with `x-amz-meta-` -- and strips off this prefix, and returns a map. getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text-getUserMetadataMap = H.fromList- . mapMaybe toMaybeMetadataHeader+getUserMetadataMap =+ H.fromList+ . mapMaybe toMaybeMetadataHeader getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime getLastModifiedHeader hs = do@@ -132,17 +145,19 @@ nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs fst <$> hush (decimal nbs) - decodeUtf8Lenient :: ByteString -> Text decodeUtf8Lenient = decodeUtf8With lenientDecode isSuccessStatus :: HT.Status -> Bool-isSuccessStatus sts = let s = HT.statusCode sts- in (s >= 200 && s < 300)+isSuccessStatus sts =+ let s = HT.statusCode sts+ in (s >= 200 && s < 300) -httpLbs :: MonadIO m- => NC.Request -> NC.Manager- -> m (NC.Response LByteString)+httpLbs ::+ MonadIO m =>+ NC.Request ->+ NC.Manager ->+ m (NC.Response LByteString) httpLbs req mgr = do respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr resp <- either throwIO return respE@@ -154,21 +169,25 @@ Just "application/json" -> do sErr <- parseErrResponseJSON $ NC.responseBody resp throwIO sErr-- _ -> throwIO $ NC.HttpExceptionRequest req $- NC.StatusCodeException (void resp) (show resp)+ _ ->+ throwIO $ NC.HttpExceptionRequest req $+ NC.StatusCodeException (void resp) (showBS resp) return resp where- tryHttpEx :: IO (NC.Response LByteString)- -> IO (Either NC.HttpException (NC.Response LByteString))+ tryHttpEx ::+ IO (NC.Response LByteString) ->+ IO (Either NC.HttpException (NC.Response LByteString)) tryHttpEx = try- contentTypeMay resp = lookupHeader Hdr.hContentType $- NC.responseHeaders resp+ contentTypeMay resp =+ lookupHeader Hdr.hContentType $+ NC.responseHeaders resp -http :: (MonadUnliftIO m, R.MonadResource m)- => NC.Request -> NC.Manager- -> m (Response (C.ConduitT () ByteString m ()))+http ::+ (MonadUnliftIO m, R.MonadResource m) =>+ NC.Request ->+ NC.Manager ->+ m (Response (C.ConduitT () ByteString m ())) http req mgr = do respE <- tryHttpEx $ NC.http req mgr resp <- either throwIO return respE@@ -178,25 +197,30 @@ respBody <- C.connect (NC.responseBody resp) CB.sinkLbs sErr <- parseErrResponse respBody throwIO sErr- _ -> do content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp throwIO $ NC.HttpExceptionRequest req $- NC.StatusCodeException (void resp) content-+ NC.StatusCodeException (void resp) content return resp where- tryHttpEx :: (MonadUnliftIO m) => m a- -> m (Either NC.HttpException a)+ tryHttpEx ::+ (MonadUnliftIO m) =>+ m a ->+ m (Either NC.HttpException a) tryHttpEx = try- contentTypeMay resp = lookupHeader Hdr.hContentType $- NC.responseHeaders resp+ contentTypeMay resp =+ lookupHeader Hdr.hContentType $+ NC.responseHeaders resp -- Similar to mapConcurrently but limits the number of threads that -- can run using a quantity semaphore.-limitedMapConcurrently :: MonadUnliftIO m- => Int -> (t -> m a) -> [t] -> m [a]+limitedMapConcurrently ::+ MonadUnliftIO m =>+ Int ->+ (t -> m a) ->+ [t] ->+ m [a] limitedMapConcurrently 0 _ _ = return [] limitedMapConcurrently count act args = do t' <- U.newTVarIO count@@ -205,17 +229,15 @@ where wThread t arg = U.bracket_ (waitSem t) (signalSem t) $ act arg- -- quantity semaphore implementation using TVar waitSem t = U.atomically $ do v <- U.readTVar t if v > 0- then U.writeTVar t (v-1)- else U.retrySTM-+ then U.writeTVar t (v -1)+ else U.retrySTM signalSem t = U.atomically $ do v <- U.readTVar t- U.writeTVar t (v+1)+ U.writeTVar t (v + 1) -- helper function to 'drop' empty optional parameter. mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)@@ -224,7 +246,7 @@ -- helper function to build query parameters that are optional. -- don't use it with mandatory query params with empty value. mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query-mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params -- | Conduit that rechunks bytestrings into the given chunk -- lengths. Stops after given chunk lengths are yielded. Stops if@@ -232,23 +254,30 @@ -- received. Does not throw any errors. chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m () chunkBSConduit [] = return ()-chunkBSConduit (s:ss) = do- bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy- if | B.length bs == s -> C.yield bs >> chunkBSConduit ss- | B.length bs > 0 -> C.yield bs- | otherwise -> return ()+chunkBSConduit (s : ss) = do+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy+ if+ | B.length bs == s -> C.yield bs >> chunkBSConduit ss+ | B.length bs > 0 -> C.yield bs+ | otherwise -> return () -- | Select part sizes - the logic is that the minimum part-size will -- be 64MiB. selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]-selectPartSizes size = uncurry (List.zip3 [1..]) $- List.unzip $ loop 0 size+selectPartSizes size =+ uncurry (List.zip3 [1 ..])+ $ List.unzip+ $ loop 0 size where ceil :: Double -> Int64 ceil = ceiling- partSize = max minPartSize (ceil $ fromIntegral size /- fromIntegral maxMultipartParts)-+ partSize =+ max+ minPartSize+ ( ceil $+ fromIntegral size+ / fromIntegral maxMultipartParts+ ) m = fromIntegral partSize loop st sz | st > sz = []@@ -257,16 +286,16 @@ lookupRegionCache :: Bucket -> Minio (Maybe Region) lookupRegionCache b = do- rMVar <- asks mcRegionMap- rMap <- UM.readMVar rMVar- return $ H.lookup b rMap+ rMVar <- asks mcRegionMap+ rMap <- UM.readMVar rMVar+ return $ H.lookup b rMap addToRegionCache :: Bucket -> Region -> Minio () addToRegionCache b region = do- rMVar <- asks mcRegionMap- UM.modifyMVar_ rMVar $ return . H.insert b region+ rMVar <- asks mcRegionMap+ UM.modifyMVar_ rMVar $ return . H.insert b region deleteFromRegionCache :: Bucket -> Minio () deleteFromRegionCache b = do- rMVar <- asks mcRegionMap- UM.modifyMVar_ rMVar $ return . H.delete b+ rMVar <- asks mcRegionMap+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/XmlGenerator.hs view
@@ -15,63 +15,80 @@ -- module Network.Minio.XmlGenerator- ( mkCreateBucketConfig- , mkCompleteMultipartUploadRequest- , mkPutNotificationRequest- , mkSelectRequest- ) where-+ ( mkCreateBucketConfig,+ mkCompleteMultipartUploadRequest,+ mkPutNotificationRequest,+ mkSelectRequest,+ )+where import qualified Data.ByteString.Lazy as LBS-import qualified Data.HashMap.Strict as H-import qualified Data.Text as T-import Text.XML--import Lib.Prelude--import Network.Minio.Data-+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import Lib.Prelude+import Network.Minio.Data+import Text.XML -- | Create a bucketConfig request body XML mkCreateBucketConfig :: Text -> Region -> ByteString mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig where- s3Element n = Element (s3Name ns n) mempty- root = s3Element "CreateBucketConfiguration"- [ NodeElement $ s3Element "LocationConstraint"- [ NodeContent location]+ s3Element n = Element (s3Name ns n) mempty+ root =+ s3Element+ "CreateBucketConfiguration"+ [ NodeElement $+ s3Element+ "LocationConstraint"+ [NodeContent location] ]- bucketConfig = Document (Prologue [] Nothing []) root []+ bucketConfig = Document (Prologue [] Nothing []) root [] -- | Create a completeMultipartUpload request body XML mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString mkCompleteMultipartUploadRequest partInfo = LBS.toStrict $ renderLBS def cmur where- root = Element "CompleteMultipartUpload" mempty $- map (NodeElement . mkPart) partInfo- mkPart (n, etag) = Element "Part" mempty- [ NodeElement $ Element "PartNumber" mempty- [NodeContent $ T.pack $ show n]- , NodeElement $ Element "ETag" mempty- [NodeContent etag]- ]+ root =+ Element "CompleteMultipartUpload" mempty $+ map (NodeElement . mkPart) partInfo+ mkPart (n, etag) =+ Element+ "Part"+ mempty+ [ NodeElement $+ Element+ "PartNumber"+ mempty+ [NodeContent $ T.pack $ show n],+ NodeElement $+ Element+ "ETag"+ mempty+ [NodeContent etag]+ ] cmur = Document (Prologue [] Nothing []) root [] -- Simplified XML representation without element attributes.-data XNode = XNode Text [XNode]- | XLeaf Text Text+data XNode+ = XNode Text [XNode]+ | XLeaf Text Text deriving (Eq, Show) toXML :: Text -> XNode -> ByteString-toXML ns node = LBS.toStrict $ renderLBS def $- Document (Prologue [] Nothing []) (xmlNode node) []+toXML ns node =+ LBS.toStrict $ renderLBS def $+ Document (Prologue [] Nothing []) (xmlNode node) [] where xmlNode :: XNode -> Element- xmlNode (XNode name nodes) = Element (s3Name ns name) mempty $- map (NodeElement . xmlNode) nodes- xmlNode (XLeaf name content) = Element (s3Name ns name) mempty- [NodeContent content]+ xmlNode (XNode name nodes) =+ Element (s3Name ns name) mempty $+ map (NodeElement . xmlNode) nodes+ xmlNode (XLeaf name content) =+ Element+ (s3Name ns name)+ mempty+ [NodeContent content] class ToXNode a where toXNode :: a -> XNode@@ -80,24 +97,29 @@ 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+ 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]+ 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- ]+getFRXNode (FilterRule n v) =+ XNode+ "FilterRule"+ [ XLeaf "Name" n,+ XLeaf "Value" v+ ] mkPutNotificationRequest :: Text -> Notification -> ByteString mkPutNotificationRequest ns = toXML ns . toXNode@@ -106,60 +128,103 @@ mkSelectRequest r = LBS.toStrict $ renderLBS def sr where sr = Document (Prologue [] Nothing []) root []- root = Element "SelectRequest" mempty $- [ NodeElement (Element "Expression" mempty- [NodeContent $ srExpression r])- , NodeElement (Element "ExpressionType" mempty- [NodeContent $ show $ srExpressionType r])- , NodeElement (Element "InputSerialization" mempty $- inputSerializationNodes $ srInputSerialization r)- , NodeElement (Element "OutputSerialization" mempty $- outputSerializationNodes $ srOutputSerialization r)- ] ++ maybe [] reqProgElem (srRequestProgressEnabled r)- reqProgElem enabled = [NodeElement- (Element "RequestProgress" mempty- [NodeElement- (Element "Enabled" mempty- [NodeContent- (if enabled then "TRUE" else "FALSE")]- )- ]- )- ]- inputSerializationNodes is = comprTypeNode (isCompressionType is) ++- [NodeElement $ formatNode (isFormatInfo is)]- comprTypeNode (Just c) = [NodeElement $ Element "CompressionType" mempty- [NodeContent $ case c of- CompressionTypeNone -> "NONE"- CompressionTypeGzip -> "GZIP"- CompressionTypeBzip2 -> "BZIP2"- ]- ]+ root =+ Element "SelectRequest" mempty $+ [ NodeElement+ ( Element+ "Expression"+ mempty+ [NodeContent $ srExpression r]+ ),+ NodeElement+ ( Element+ "ExpressionType"+ mempty+ [NodeContent $ show $ srExpressionType r]+ ),+ NodeElement+ ( Element "InputSerialization" mempty+ $ inputSerializationNodes+ $ srInputSerialization r+ ),+ NodeElement+ ( Element "OutputSerialization" mempty+ $ outputSerializationNodes+ $ srOutputSerialization r+ )+ ]+ ++ maybe [] reqProgElem (srRequestProgressEnabled r)+ reqProgElem enabled =+ [ NodeElement+ ( Element+ "RequestProgress"+ mempty+ [ NodeElement+ ( Element+ "Enabled"+ mempty+ [ NodeContent+ (if enabled then "TRUE" else "FALSE")+ ]+ )+ ]+ )+ ]+ inputSerializationNodes is =+ comprTypeNode (isCompressionType is)+ ++ [NodeElement $ formatNode (isFormatInfo is)]+ comprTypeNode (Just c) =+ [ NodeElement $+ Element+ "CompressionType"+ mempty+ [ NodeContent $ case c of+ CompressionTypeNone -> "NONE"+ CompressionTypeGzip -> "GZIP"+ CompressionTypeBzip2 -> "BZIP2"+ ]+ ] comprTypeNode Nothing = []- kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v] formatNode (InputFormatCSV (CSVProp h)) =- Element "CSV" mempty- (map NodeElement $ map kvElement $ H.toList h)+ Element+ "CSV"+ mempty+ (map NodeElement $ map kvElement $ H.toList h) formatNode (InputFormatJSON p) =- Element "JSON" mempty- [NodeElement- (Element "Type" mempty- [NodeContent $ case jsonipType p of- JSONTypeDocument -> "DOCUMENT"- JSONTypeLines -> "LINES"- ]- )- ]+ Element+ "JSON"+ mempty+ [ NodeElement+ ( Element+ "Type"+ mempty+ [ NodeContent $ case jsonipType p of+ JSONTypeDocument -> "DOCUMENT"+ JSONTypeLines -> "LINES"+ ]+ )+ ] formatNode InputFormatParquet = Element "Parquet" mempty []- outputSerializationNodes (OutputSerializationJSON j) =- [NodeElement (Element "JSON" mempty $- rdElem $ jsonopRecordDelimiter j)]+ [ NodeElement+ ( Element "JSON" mempty+ $ rdElem+ $ jsonopRecordDelimiter j+ )+ ] outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =- [NodeElement $ Element "CSV" mempty- (map NodeElement $ map kvElement $ H.toList h)]-+ [ NodeElement $+ Element+ "CSV"+ mempty+ (map NodeElement $ map kvElement $ H.toList h)+ ] rdElem Nothing = []- rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty- [NodeContent t]]+ rdElem (Just t) =+ [ NodeElement $+ Element+ "RecordDelimiter"+ mempty+ [NodeContent t]+ ]
src/Network/Minio/XmlParser.hs view
@@ -15,34 +15,32 @@ -- module Network.Minio.XmlParser- ( parseListBuckets- , parseLocation- , parseNewMultipartUpload- , parseCompleteMultipartUploadResponse- , parseCopyObjectResponse- , parseListObjectsResponse- , parseListObjectsV1Response- , parseListUploadsResponse- , parseListPartsResponse- , parseErrResponse- , parseNotification- , parseSelectProgress- ) where+ ( parseListBuckets,+ parseLocation,+ parseNewMultipartUpload,+ parseCompleteMultipartUploadResponse,+ parseCopyObjectResponse,+ parseListObjectsResponse,+ parseListObjectsV1Response,+ parseListUploadsResponse,+ parseListPartsResponse,+ parseErrResponse,+ parseNotification,+ parseSelectProgress,+ )+where import qualified Data.ByteString.Lazy as LB-import qualified Data.HashMap.Strict as H-import Data.List (zip3, zip4, zip6)-import qualified Data.Text as T-import Data.Text.Read (decimal)-import Data.Time-import Text.XML-import Text.XML.Cursor hiding (bool)--import Lib.Prelude--import Network.Minio.Data-import Network.Minio.Errors-+import qualified Data.HashMap.Strict as H+import Data.List (zip3, zip4, zip6)+import qualified Data.Text as T+import Data.Text.Read (decimal)+import Data.Time+import Lib.Prelude+import Network.Minio.Data+import Network.Minio.Errors+import Text.XML+import Text.XML.Cursor hiding (bool) -- | Represent the time format string returned by S3 API calls. s3TimeFormat :: [Char]@@ -58,12 +56,14 @@ -- | Parse time strings from XML parseS3XMLTime :: MonadIO m => Text -> m UTCTime parseS3XMLTime t =- maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $- parseTimeM True defaultTimeLocale s3TimeFormat $ T.unpack t+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return+ $ parseTimeM True defaultTimeLocale s3TimeFormat+ $ T.unpack t parseDecimal :: (MonadIO m, Integral a) => Text -> m a-parseDecimal numStr = either (throwIO . MErrVXmlParse . show) return $- fst <$> decimal numStr+parseDecimal numStr =+ either (throwIO . MErrVXmlParse . show) return $+ fst <$> decimal numStr parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a] parseDecimals numStr = forM numStr parseDecimal@@ -72,18 +72,18 @@ s3Elem ns = element . s3Name ns parseRoot :: (MonadIO m) => LByteString -> m Cursor-parseRoot = either (throwIO . MErrVXmlParse . show) (return . fromDocument)- . parseLBS def+parseRoot =+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)+ . parseLBS def -- | Parse the response XML of a list buckets call. parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo] parseListBuckets xmldata = do r <- parseRoot xmldata ns <- asks getSvcNamespace- let- s3Elem' = s3Elem ns- names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content- timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content+ let s3Elem' = s3Elem ns+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content times <- mapM parseS3XMLTime timeStrings return $ zipWith BucketInfo names times@@ -116,41 +116,38 @@ parseCopyObjectResponse xmldata = do r <- parseRoot xmldata ns <- asks getSvcNamespace- let- s3Elem' = s3Elem ns- mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content+ let s3Elem' = s3Elem ns+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content mtime <- parseS3XMLTime mtimeStr return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime) -- | Parse the response XML of a list objects v1 call.-parseListObjectsV1Response :: (MonadReader env m, HasSvcNamespace env, MonadIO m)- => LByteString -> m ListObjectsV1Result+parseListObjectsV1Response ::+ (MonadReader env m, HasSvcNamespace env, MonadIO m) =>+ LByteString ->+ m ListObjectsV1Result parseListObjectsV1Response xmldata = do r <- parseRoot xmldata ns <- asks getSvcNamespace- let- s3Elem' = s3Elem ns- 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+ let s3Elem' = s3Elem ns+ 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 (uncurry6 ObjectInfo) $- zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)+ let objects =+ map (uncurry6 ObjectInfo) $+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty) return $ ListObjectsV1Result hasMore nextMarker objects prefixes @@ -159,28 +156,24 @@ parseListObjectsResponse xmldata = do r <- parseRoot xmldata ns <- asks getSvcNamespace- let- s3Elem' = s3Elem ns- hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)-- nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ 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+ let s3Elem' = s3Elem ns+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)+ nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ 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 (uncurry6 ObjectInfo) $- zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)+ let objects =+ map (uncurry6 ObjectInfo) $+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty) return $ ListObjectsResult hasMore nextToken objects prefixes @@ -189,20 +182,18 @@ parseListUploadsResponse xmldata = do r <- parseRoot xmldata ns <- asks getSvcNamespace- let- s3Elem' = s3Elem ns- hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)- prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content- nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content- nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content- uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content- uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content- uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content+ let s3Elem' = s3Elem ns+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content+ nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content+ nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr - let- uploads = zip3 uploadKeys uploadIds uploadInitTimes+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes @@ -210,27 +201,25 @@ parseListPartsResponse xmldata = do r <- parseRoot xmldata ns <- asks getSvcNamespace- let- s3Elem' = s3Elem ns- hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)- nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content- partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content- partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content- partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content- partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content+ let s3Elem' = s3Elem ns+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)+ nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content partModTimes <- mapM parseS3XMLTime partModTimeStr partSizes <- parseDecimals partSizeStr partNumbers <- parseDecimals partNumberStr nextPartNum <- parseDecimals $ maybeToList nextPartNumStr - let- partInfos = map (uncurry4 ObjectPartInfo) $- zip4 partNumbers partETags partSizes partModTimes+ let partInfos =+ map (uncurry4 ObjectPartInfo) $+ zip4 partNumbers partETags partSizes partModTimes return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos - parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr parseErrResponse xmldata = do r <- parseRoot xmldata@@ -250,28 +239,31 @@ <*> (mapM (parseNode ns "Topic") tcfg) <*> (mapM (parseNode ns "CloudFunction") lcfg) where- getFilterRule ns c = let name = T.concat $ c $/ s3Elem ns "Name" &/ content value = T.concat $ c $/ s3Elem ns "Value" &/ content- in FilterRule name value-+ in FilterRule name value parseNode ns arnName nodeData = do let c = fromNode nodeData id = T.concat $ c $/ s3Elem ns "Id" &/ content arn = T.concat $ c $/ s3Elem ns arnName &/ content events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content- rules = c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key" &/- s3Elem ns "FilterRule" &| getFilterRule ns- return $ NotificationConfig id arn events- (Filter $ FilterKey $ FilterRules rules)+ rules =+ c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key"+ &/ s3Elem ns "FilterRule" &| getFilterRule ns+ return $+ NotificationConfig+ id+ arn+ events+ (Filter $ FilterKey $ FilterRules rules) parseSelectProgress :: MonadIO m => ByteString -> m Progress parseSelectProgress xmldata = do- r <- parseRoot $ LB.fromStrict xmldata- let bScanned = T.concat $ r $/ element "BytesScanned" &/ content- bProcessed = T.concat $ r $/element "BytesProcessed" &/ content- bReturned = T.concat $ r $/element "BytesReturned" &/ content- Progress <$> parseDecimal bScanned- <*> parseDecimal bProcessed- <*> parseDecimal bReturned+ r <- parseRoot $ LB.fromStrict xmldata+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content+ bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content+ bReturned = T.concat $ r $/ element "BytesReturned" &/ content+ Progress <$> parseDecimal bScanned+ <*> parseDecimal bProcessed+ <*> parseDecimal bReturned
stack.yaml view
@@ -15,7 +15,7 @@ # resolver: # name: custom-snapshot # location: "./custom-snapshot.yaml"-resolver: lts-14.6+resolver: lts-16.0 # User packages to be built. # Various formats can be used as shown in the example below.@@ -39,7 +39,9 @@ - '.' # Dependency packages to be pulled from upstream that are not in the resolver # (e.g., acme-missiles-0.3)-extra-deps: []+extra-deps:+- unliftio-core-0.2.0.1+- protolude-0.3.0 # Override default flag values for local packages and extra-deps flags: {}
test/LiveServer.hs view
@@ -1,1009 +1,1182 @@ {-# LANGUAGE OverloadedStrings #-}------ MinIO Haskell SDK, (C) 2017-2019 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.-----import qualified Test.QuickCheck as Q-import Test.Tasty-import Test.Tasty.HUnit-import Test.Tasty.QuickCheck as QC--import Conduit (replicateC)-import qualified Conduit as C-import qualified Control.Monad.Trans.Resource as R-import qualified Data.ByteString as BS-import Data.Conduit (yield)-import qualified Data.Conduit.Binary as CB-import Data.Conduit.Combinators (sinkList)-import qualified Data.HashMap.Strict as H-import qualified Data.Text as T-import Data.Time (fromGregorian)-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.Directory (getTemporaryDirectory)-import System.Environment (lookupEnv)-import qualified System.IO as SIO--import Lib.Prelude--import Network.Minio-import Network.Minio.Data-import Network.Minio.Data.Crypto-import Network.Minio.PutObject-import Network.Minio.S3API-import Network.Minio.Utils--main :: IO ()-main = defaultMain tests--tests :: TestTree-tests = testGroup "Tests" [liveServerUnitTests]---- conduit that generates random binary stream of given length-randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()-randomDataSrc s' = genBS s'- where- concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++- [BS.take (fromIntegral r) bs]- where (q, r) = n `divMod` fromIntegral (BS.length bs)-- genBS s = do- w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))- let byteArr64 = BS.pack w8s- if s < oneMiB- then yield $ concatIt byteArr64 s- else do yield $ concatIt byteArr64 oneMiB- genBS (s - oneMiB)--mkRandFile :: R.MonadResource m => Int64 -> m FilePath-mkRandFile size = do- dir <- liftIO $ getTemporaryDirectory- C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"--funTestBucketPrefix :: Text-funTestBucketPrefix = "miniohstest-"--loadTestServer :: IO ConnectInfo-loadTestServer = do- val <- lookupEnv "MINIO_LOCAL"- isSecure <- lookupEnv "MINIO_SECURE"- return $ case (val, isSecure) of- (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"- (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"- (Nothing, _) -> minioPlayCI--funTestWithBucket :: TestName- -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree-funTestWithBucket t minioTest = testCaseSteps t $ \step -> do- -- generate a random name for the bucket- bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))- let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]- liftStep = liftIO . step- connInfo <- loadTestServer- ret <- runMinio connInfo $ do- liftStep $ "Creating bucket for test - " ++ t- foundBucket <- bucketExists b- liftIO $ foundBucket @?= False- makeBucket b Nothing- minioTest liftStep b- deleteBucket b- isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)--liveServerUnitTests :: TestTree-liveServerUnitTests = testGroup "Unit tests against a live server"- [ basicTests- , listingTest- , highLevelListingTest- , lowLevelMultipartTest- , putObjectSizeTest- , putObjectNoSizeTest- , multipartTest- , putObjectContentTypeTest- , putObjectContentLanguageTest- , putObjectStorageClassTest- , putObjectUserMetadataTest- , getObjectTest- , copyObjectTests- , presignedUrlFunTest- , presignedPostPolicyFunTest- , bucketPolicyFunTest- , getNPutSSECTest- ]--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 <- 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 <- 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" defaultPutObjectOptions-- step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"- fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions- case fpE of- Left exn -> liftIO $ exn @?= NoSuchBucket- _ -> return ()-- outFile <- mkRandFile 0- step "simple fGetObject works"- fGetObject bucket "lsb-release" outFile defaultGetObjectOptions-- let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857- step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"- resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {- gooIfUnmodifiedSince = (Just unmodifiedTime)- }- case resE of- Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"- _ -> return ()-- step "fGetObject an object with no matching etag, check for exception"- resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {- gooIfMatch = (Just "invalid-etag")- }- case resE1 of- Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"- _ -> return ()-- step "fGetObject an object with no valid range, check for exception"- resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {- gooRange = (Just $ HT.ByteRangeFromTo 100 200)- }- case resE2 of- Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"- _ -> return ()-- step "fGetObject on object with a valid range"- fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {- gooRange = (Just $ HT.ByteRangeFrom 1)- }-- step "fGetObject a non-existent object and check for NoSuchKey exception"- resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions- case resE3 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 defaultPutObjectOptions-- step "get metadata of the object"- res <- statObject bucket object defaultGetObjectOptions- liftIO $ (oiSize res) @?= 0-- step "delete object"- deleteObject bucket object--lowLevelMultipartTest :: TestTree-lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $- \step bucket -> do- -- low-level multipart operation tests.- let object = "newmpupload"- mb15 = 15 * 1024 * 1024-- step "Prepare for low-level multipart tests."- step "create new multipart upload"- uid <- newMultipartUpload bucket object []- liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")-- randFile <- mkRandFile mb15-- step "put object parts 1 of 1"- h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode- partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15-- step "complete multipart"- void $ completeMultipartUpload bucket object uid [partInfo]-- destFile <- mkRandFile 0- step "Retrieve the created object and check size"- fGetObject bucket object destFile defaultGetObjectOptions- gotSize <- withNewHandle destFile getFileSize- liftIO $ gotSize == Right (Just mb15) @?- "Wrong file size of put file after getting"-- step "Cleanup actions"- removeObject bucket object--putObjectSizeTest :: TestTree-putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $- \step bucket -> do- -- putObject test (conduit source, size specified)- let obj = "msingle"- mb1 = 1 * 1024 * 1024-- step "Prepare for putObject with from source with size."- rFile <- mkRandFile mb1-- step "Upload single file."- putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions-- step "Retrieve and verify file size"- destFile <- mkRandFile 0- fGetObject bucket obj destFile defaultGetObjectOptions- gotSize <- withNewHandle destFile getFileSize- liftIO $ gotSize == Right (Just mb1) @?- "Wrong file size of put file after getting"-- step "Cleanup actions"- deleteObject bucket obj--putObjectNoSizeTest :: TestTree-putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $- \step bucket -> do- -- putObject test (conduit source, no size specified)- let obj = "mpart"- mb70 = 70 * 1024 * 1024-- step "Prepare for putObject with from source without providing size."- rFile <- mkRandFile mb70-- step "Upload multipart file."- putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions-- step "Retrieve and verify file size"- destFile <- mkRandFile 0- fGetObject bucket obj destFile defaultGetObjectOptions- gotSize <- withNewHandle destFile getFileSize- liftIO $ gotSize == Right (Just mb70) @?- "Wrong file size of put file after getting"-- step "Cleanup actions"- deleteObject bucket obj--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", "o4"]- extractObjectsFromList os =- mapM (\t -> case t of- ListItemObject o -> Just $ oiObject o- _ -> Nothing) os- expectedNonRecList = ["o4", "dir/"]- extractObjectsAndDirsFromList os =- map (\t -> case t of- ListItemObject o -> oiObject o- ListItemPrefix d -> d) os-- forM_ expectedObjects $- \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions-- step "High-level listing of objects"- items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList- liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $- extractObjectsAndDirsFromList items-- step "High-level recursive listing of objects"- objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList-- liftIO $ assertEqual "Objects match failed!"- (Just $ sort expectedObjects) $- extractObjectsFromList objects-- step "High-level listing of objects (version 1)"- itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList- liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $- extractObjectsAndDirsFromList itemsV1-- step "High-level recursive listing of objects (version 1)"- objectsV1 <- C.runConduit $ listObjectsV1 bucket Nothing True C..|- sinkList-- liftIO $ assertEqual "Objects match failed!"- (Just $ sort expectedObjects) $- extractObjectsFromList objectsV1-- let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]- expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]- step "High-level listing with prefix"- prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList- liftIO $ assertEqual "Objects/Dirs under prefix match failed!"- expectedPrefListing $ extractObjectsAndDirsFromList prefItems-- step "High-level listing with prefix recursive"- prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList- liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"- expectedPrefListingRec $ extractObjectsFromList prefItemsRec-- step "High-level listing with prefix (version 1)"- prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList- liftIO $ assertEqual "Objects/Dirs under prefix match failed!"- expectedPrefListing $ extractObjectsAndDirsFromList prefItemsV1-- step "High-level listing with prefix recursive (version 1)"- prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList- liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"- expectedPrefListingRec $ extractObjectsFromList prefItemsRecV1-- 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 <- C.runConduit $- listIncompleteUploads bucket (Just "newmpupload") True C..|- sinkList- liftIO $ length uploads @?= 10-- step "cleanup"- forM_ uploads $ \(UploadInfo _ uid _ _) ->- abortMultipartUpload bucket object uid-- step "High-level listIncompleteParts Test"- 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 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 <- C.runConduit $ listIncompleteParts bucket object uid- C..| 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" defaultPutObjectOptions-- 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"- resV1 <- 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' resV1)-- step "Cleanup actions"- forM_ objects $ \obj -> deleteObject bucket obj-- step "listIncompleteUploads' test"- 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.")-- step "list incomplete multipart uploads"- incompleteUploads <- listIncompleteUploads' bucket (Just "newmpupload") Nothing- Nothing Nothing Nothing- liftIO $ (length $ lurUploads incompleteUploads) @?= 10-- step "cleanup"- forM_ (lurUploads incompleteUploads) $- \(_, uid, _) -> abortMultipartUpload bucket object uid-- step "Basic listIncompleteParts Test"- 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 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"- listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing- liftIO $ (length $ lprParts listPartsResult) @?= 10- abortMultipartUpload bucket object uid--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 <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs- liftIO $ (bs == NC.responseBody getResp) @?- "presigned put and get got mismatched data"-- step "PUT object presigned - presignedPutObjectURL"- putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []-- 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 let 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 = H.fromList $ NC.responseHeaders headResp- cLen = H.lookupDefault "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 <- C.runConduit $ CB.sourceFile testFile C..| 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- let 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- let 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) $- H.toList formData- parts' = parts ++ [Form.partFile "file" inputFile]- req' <- Form.formDataBody parts' req- mgr <- NC.newManager NC.tlsManagerSettings- NC.httpLbs req' mgr--bucketPolicyFunTest :: TestTree-bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $- \step bucket -> do-- step "bucketPolicy basic test - no policy exception"- resE <- try $ getBucketPolicy bucket- case resE of- Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"- _ -> return ()-- resE' <- try $ setBucketPolicy bucket T.empty- case resE' of- Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"- _ -> return ()-- let expectedPolicyJSON = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket/*\"]}]}"-- step "try a malformed policy, expect error"- resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON- case resE'' of- Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "Policy has invalid resource."- _ -> return ()-- let expectedPolicyJSON' = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "/*\"]}]}"-- step "set bucket policy"- setBucketPolicy bucket expectedPolicyJSON'-- let obj = "myobject"-- step "verify bucket policy: (1) create `myobject`"- putObject bucket obj (replicateC 100 "c") Nothing defaultPutObjectOptions-- step "verify bucket policy: (2) get `myobject` anonymously"- connInfo <- asks mcConnInfo- let proto = bool "http://" "https://" $ connectIsSecure connInfo- url = BS.concat [proto, getHostAddr connInfo, "/", toS bucket,- "/", toS obj]- respE <- liftIO $ (fmap (Right . toS) $ NC.simpleHttp $ toS url) `catch`- (\(e :: NC.HttpException) -> return $ Left (show e :: Text))- case respE of- Left err -> liftIO $ assertFailure $ show err- Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")-- deleteObject bucket obj-- step "delete bucket policy"- setBucketPolicy bucket T.empty--multipartTest :: TestTree-multipartTest = 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"-- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)-- step "Retrieve and verify file size"- destFile <- mkRandFile 0- fGetObject bucket obj destFile defaultGetObjectOptions- gotSize <- withNewHandle destFile getFileSize- liftIO $ gotSize == Right (Just mb80) @?- "Wrong file size of put file after getting"-- step "Cleanup actions"- removeObject bucket obj-- step "cleanup"- removeObject bucket "big"-- step "Prepare for removeIncompleteUpload"- -- low-level multipart operation tests.- let object = "newmpupload"- kb5 = 5 * 1024-- step "create new multipart upload"- uid <- newMultipartUpload bucket object []- liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."-- randFile <- mkRandFile kb5-- 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 "remove ongoing upload"- removeIncompleteUpload bucket object- uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False- C..| sinkList- liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"--putObjectContentTypeTest :: TestTree-putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $- \step bucket -> do- step "fPutObject content type test"- let object = "xxx-content-type"- size1 = 100 :: Int64-- step "create server object with content-type"- inputFile <- mkRandFile size1- fPutObject bucket object inputFile defaultPutObjectOptions {- pooContentType = Just "application/javascript"- }-- -- retrieve obj info to check- oi <- headObject bucket object []- let m = oiMetadata oi-- step "Validate content-type"- liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)-- step "upload object with content-encoding set to identity"- fPutObject bucket object inputFile defaultPutObjectOptions {- pooContentEncoding = Just "identity"- }-- oiCE <- headObject bucket object []- let m' = oiMetadata oiCE-- step "Validate content-encoding"- liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")- (H.lookup "Content-Encoding" m')-- step "Cleanup actions"-- removeObject bucket object--putObjectContentLanguageTest :: TestTree-putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage tests" $- \step bucket -> do- step "fPutObject content language test"- let object = "xxx-content-language"- size1 = 100 :: Int64-- step "create server object with content-language"- inputFile <- mkRandFile size1- fPutObject bucket object inputFile defaultPutObjectOptions {- pooContentLanguage = Just "en-US"- }-- -- retrieve obj info to check- oi <- headObject bucket object []- let m = oiMetadata oi-- step "Validate content-language"- liftIO $ assertEqual "content-language did not match" (Just "en-US")- (H.lookup "Content-Language" m)- step "Cleanup actions"-- removeObject bucket object--putObjectUserMetadataTest :: TestTree-putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $- \step bucket -> do- step "putObject user-metadata test"- let object = "object-with-metadata"- size1 = 100 :: Int64-- step "create server object with usermetdata"- inputFile <- mkRandFile size1-- fPutObject bucket object inputFile defaultPutObjectOptions {- pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")- , ("mykey2", "myval2")- ]- }-- step "Validate user-metadata"- -- retrieve obj info to check- oi <- headObject bucket object []- let m = oiUserMetadata oi- -- need to do a case-insensitive comparison- sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $- H.toList m- ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]-- liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"-- step "Cleanup actions"- removeObject bucket object--getObjectTest :: TestTree-getObjectTest = funTestWithBucket "getObject test" $- \step bucket -> do- step "putObject with some metadata"- let object = "object-with-metadata"- size1 = 100 :: Int64-- inputFile <- mkRandFile size1- fPutObject bucket object inputFile defaultPutObjectOptions {- pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")- , ("mykey2", "myval2")- ]- }-- step "get the object - check the metadata matches"- -- retrieve obj info to check- gor <- getObject bucket object defaultGetObjectOptions- let m = oiUserMetadata $ gorObjectInfo gor- -- need to do a case-insensitive comparison- sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $- H.toList m- ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]-- liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"-- step "get the object content"- getObjectHash <- hashSHA256FromSource $ gorObjectStream gor- inputHash <- hashSHA256FromSource $ C.sourceFile inputFile- liftIO $ (getObjectHash == inputHash) @? "Input file and output file mismatched!"-- step "Cleanup actions"- removeObject bucket object--putObjectStorageClassTest :: TestTree-putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $- \step bucket -> do- step "fPutObject storage class test"- let object = "xxx-storage-class-standard"- object' = "xxx-storage-class-reduced"- object'' = "xxx-storage-class-invalid"- size1 = 100 :: Int64- size0 = 0 :: Int64-- step "create server objects with storageClass"- inputFile <- mkRandFile size1- inputFile' <- mkRandFile size1- inputFile'' <- mkRandFile size0-- fPutObject bucket object inputFile defaultPutObjectOptions {- pooStorageClass = Just "STANDARD"- }-- fPutObject bucket object' inputFile' defaultPutObjectOptions {- pooStorageClass = Just "REDUCED_REDUNDANCY"- }-- removeObject bucket object-- -- retrieve obj info to check- oi' <- headObject bucket object' []- let m' = oiMetadata oi'-- step "Validate x-amz-storage-class rrs"- liftIO $ assertEqual "storageClass did not match" (Just "REDUCED_REDUNDANCY")- (H.lookup "X-Amz-Storage-Class" m')-- fpE <- try $ fPutObject bucket object'' inputFile'' defaultPutObjectOptions {- pooStorageClass = Just "INVALID_STORAGE_CLASS"- }- case fpE of- Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."- _ -> return ()-- step "Cleanup actions"-- removeObject bucket object'--copyObjectTests :: TestTree-copyObjectTests = funTestWithBucket "copyObject related tests" $- \step bucket -> do- step "copyObjectSingle basic tests"- let object = "xxx"- objCopy = "xxxCopy"- size1 = 100 :: Int64-- step "create server object to copy"- inputFile <- mkRandFile size1- fPutObject bucket object inputFile defaultPutObjectOptions-- step "copy object"- let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}- (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []-- -- retrieve obj info to check- oi <- headObject bucket objCopy []- let t = oiModTime oi- let e = oiETag oi- let s = oiSize oi-- let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0-- liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?- "Copied object did not match expected."-- step "cleanup actions"- removeObject bucket object- removeObject bucket objCopy-- step "copyObjectPart basic tests"- let srcObj = "XXX"- copyObj = "XXXCopy"-- step "Prepare"- let mb15 = 15 * 1024 * 1024- mb5 = 5 * 1024 * 1024- randFile <- mkRandFile mb15- fPutObject bucket srcObj randFile defaultPutObjectOptions-- step "create new multipart upload"- uid <- newMultipartUpload bucket copyObj []- liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."-- step "put object parts 1-3"- let srcInfo' = defaultSourceInfo { srcBucket = bucket, srcObject = srcObj }- dstInfo' = defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj }- parts <- forM [1..3] $ \p -> do- (etag', _) <- copyObjectPart dstInfo' srcInfo'{- srcRange = Just $ (,) ((p-1)*mb5) ((p-1)*mb5 + (mb5 - 1))- } uid (fromIntegral p) []- return (fromIntegral p, etag')-- step "complete multipart"- void $ completeMultipartUpload bucket copyObj uid parts-- step "verify copied object size"- oi' <- headObject bucket copyObj []- let s' = oiSize oi'-- liftIO $ (s' == mb15) @? "Size failed to match"-- step "Cleanup actions"- removeObject bucket srcObj- removeObject bucket copyObj-- step "copyObject basic tests"- let srcs = ["XXX", "XXXL"]- copyObjs = ["XXXCopy", "XXXLCopy"]- sizes = map (* (1024 * 1024)) [15, 65]-- step "Prepare"- forM_ (zip srcs sizes) $ \(src, size) -> do- inputFile' <- mkRandFile size- fPutObject bucket src inputFile' defaultPutObjectOptions-- step "make small and large object copy"- forM_ (zip copyObjs srcs) $ \(cp, src) ->- copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = cp} defaultSourceInfo {srcBucket = bucket, srcObject = src}-- step "verify uploaded objects"- uploadedSizes <- fmap oiSize <$> forM copyObjs (\o -> headObject bucket o [])-- liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match"-- forM_ (srcs ++ copyObjs) (removeObject bucket)-- step "copyObject with offset test "- let src = "XXX"- size = 15 * 1024 * 1024-- step "Prepare"- inputFile' <- mkRandFile size- fPutObject bucket src inputFile' defaultPutObjectOptions-- step "copy last 10MiB of object"- copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {- srcBucket = bucket- , srcObject = src- , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)- }-- step "verify uploaded object"- cSize <- oiSize <$> headObject bucket copyObj []-- liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"-- forM_ [src, copyObj] (removeObject bucket)--getNPutSSECTest :: TestTree-getNPutSSECTest =- funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do- -- Skip this test if the server is not using TLS as encryption is- -- disabled anyway.- isTLSConn <- asks (connectIsSecure . mcConnInfo)- if isTLSConn- then do step "Make an encryption key"- key <- case mkSSECKey $ BS.pack [0..31] of- Nothing -> liftIO $ assertFailure "This should not happen"- Just k -> return k-- let mb1 = 1024*1024- obj = "1"- step "Upload an object using the encryption key"- rFile <- mkRandFile mb1- let putOpts = defaultPutObjectOptions { pooSSE = Just $ SSEC key }- fPutObject bucket obj rFile putOpts-- step "Stat object without key - should fail"- headRes <- try $ statObject bucket obj defaultGetObjectOptions- case headRes of- Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"- Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))- | NC.responseStatus rsp == HT.status400 -> return ()- | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex- Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex-- step "Get file and check length"- dstFile <- mkRandFile 0- let getOpts = defaultGetObjectOptions { gooSSECKey = Just key }- fGetObject bucket obj dstFile getOpts-- gotSize <- withNewHandle dstFile getFileSize- liftIO $ gotSize == Right (Just mb1) @?- "Wrong file size of object when getting"-- step "Cleanup"- deleteObject bucket obj-- else step "Skipping encryption test as server is not using TLS"++--+-- MinIO Haskell SDK, (C) 2017-2019 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.+--++import Conduit (replicateC)+import qualified Conduit as C+import qualified Control.Monad.Trans.Resource as R+import qualified Data.ByteString as BS+import Data.Conduit (yield)+import qualified Data.Conduit.Binary as CB+import Data.Conduit.Combinators (sinkList)+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import Data.Time (fromGregorian)+import qualified Data.Time as Time+import Lib.Prelude+import qualified Network.HTTP.Client.MultipartFormData as Form+import qualified Network.HTTP.Conduit as NC+import qualified Network.HTTP.Types as HT+import Network.Minio+import Network.Minio.Data+import Network.Minio.Data.Crypto+import Network.Minio.PutObject+import Network.Minio.S3API+import Network.Minio.Utils+import System.Directory (getTemporaryDirectory)+import System.Environment (lookupEnv)+import qualified Test.QuickCheck as Q+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck as QC+import qualified UnliftIO.IO as UIO++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests" [liveServerUnitTests]++-- conduit that generates random binary stream of given length+randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()+randomDataSrc s' = genBS s'+ where+ concatIt bs n =+ BS.concat $+ replicate (fromIntegral q) bs+ ++ [BS.take (fromIntegral r) bs]+ where+ (q, r) = n `divMod` fromIntegral (BS.length bs)+ genBS s = do+ w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))+ let byteArr64 = BS.pack w8s+ if s < oneMiB+ then yield $ concatIt byteArr64 s+ else do+ yield $ concatIt byteArr64 oneMiB+ genBS (s - oneMiB)++mkRandFile :: R.MonadResource m => Int64 -> m FilePath+mkRandFile size = do+ dir <- liftIO getTemporaryDirectory+ C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"++funTestBucketPrefix :: Text+funTestBucketPrefix = "miniohstest-"++loadTestServer :: IO ConnectInfo+loadTestServer = do+ val <- lookupEnv "MINIO_LOCAL"+ isSecure <- lookupEnv "MINIO_SECURE"+ return $ case (val, isSecure) of+ (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"+ (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"+ (Nothing, _) -> minioPlayCI++funTestWithBucket ::+ TestName ->+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->+ TestTree+funTestWithBucket t minioTest = testCaseSteps t $ \step -> do+ -- generate a random name for the bucket+ bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))+ let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]+ liftStep = liftIO . step+ connInfo <- loadTestServer+ ret <- runMinio connInfo $ do+ liftStep $ "Creating bucket for test - " ++ t+ foundBucket <- bucketExists b+ liftIO $ foundBucket @?= False+ makeBucket b Nothing+ minioTest liftStep b+ deleteBucket b+ isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)++liveServerUnitTests :: TestTree+liveServerUnitTests =+ testGroup+ "Unit tests against a live server"+ [ basicTests,+ listingTest,+ highLevelListingTest,+ lowLevelMultipartTest,+ putObjectSizeTest,+ putObjectNoSizeTest,+ multipartTest,+ putObjectContentTypeTest,+ putObjectContentLanguageTest,+ putObjectStorageClassTest,+ putObjectUserMetadataTest,+ getObjectTest,+ copyObjectTests,+ presignedUrlFunTest,+ presignedPostPolicyFunTest,+ bucketPolicyFunTest,+ getNPutSSECTest+ ]++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 <- 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 <- 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"+ testFilepath <- mkRandFile 200+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions++ step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions+ case fpE of+ Left exn -> liftIO $ exn @?= NoSuchBucket+ _ -> return ()++ outFile <- mkRandFile 0+ step "simple fGetObject works"+ fGetObject bucket "test-file" outFile defaultGetObjectOptions++ let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857+ step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"+ resE <-+ try $+ fGetObject+ bucket+ "test-file"+ outFile+ defaultGetObjectOptions+ { gooIfUnmodifiedSince = (Just unmodifiedTime)+ }+ case resE of+ Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"+ _ -> return ()++ step "fGetObject an object with no matching etag, check for exception"+ resE1 <-+ try $+ fGetObject+ bucket+ "test-file"+ outFile+ defaultGetObjectOptions+ { gooIfMatch = (Just "invalid-etag")+ }+ case resE1 of+ Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"+ _ -> return ()++ step "fGetObject an object with no valid range, check for exception"+ resE2 <-+ try $+ fGetObject+ bucket+ "test-file"+ outFile+ defaultGetObjectOptions+ { gooRange = (Just $ HT.ByteRangeFromTo 100 300)+ }+ case resE2 of+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"+ _ -> return ()++ step "fGetObject on object with a valid range"+ fGetObject+ bucket+ "test-file"+ outFile+ defaultGetObjectOptions+ { gooRange = (Just $ HT.ByteRangeFrom 1)+ }++ step "fGetObject a non-existent object and check for NoSuchKey exception"+ resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions+ case resE3 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 "test-file"++ step "statObject test"+ let object = "sample"+ step "create an object"+ inputFile <- mkRandFile 0+ fPutObject bucket object inputFile defaultPutObjectOptions++ step "get metadata of the object"+ res <- statObject bucket object defaultGetObjectOptions+ liftIO $ (oiSize res) @?= 0++ step "delete object"+ deleteObject bucket object++lowLevelMultipartTest :: TestTree+lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $+ \step bucket -> do+ -- low-level multipart operation tests.+ let object = "newmpupload"+ mb15 = 15 * 1024 * 1024++ step "Prepare for low-level multipart tests."+ step "create new multipart upload"+ uid <- newMultipartUpload bucket object []+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")++ randFile <- mkRandFile mb15++ step "put object parts 1 of 1"+ partInfo <-+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->+ putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15++ step "complete multipart"+ void $ completeMultipartUpload bucket object uid [partInfo]++ destFile <- mkRandFile 0+ step "Retrieve the created object and check size"+ fGetObject bucket object destFile defaultGetObjectOptions+ gotSize <- withNewHandle destFile getFileSize+ liftIO $+ gotSize == Right (Just mb15)+ @? "Wrong file size of put file after getting"++ step "Cleanup actions"+ removeObject bucket object++putObjectSizeTest :: TestTree+putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $+ \step bucket -> do+ -- putObject test (conduit source, size specified)+ let obj = "msingle"+ mb1 = 1 * 1024 * 1024++ step "Prepare for putObject with from source with size."+ rFile <- mkRandFile mb1++ step "Upload single file."+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions++ step "Retrieve and verify file size"+ destFile <- mkRandFile 0+ fGetObject bucket obj destFile defaultGetObjectOptions+ gotSize <- withNewHandle destFile getFileSize+ liftIO $+ gotSize == Right (Just mb1)+ @? "Wrong file size of put file after getting"++ step "Cleanup actions"+ deleteObject bucket obj++putObjectNoSizeTest :: TestTree+putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $+ \step bucket -> do+ -- putObject test (conduit source, no size specified)+ let obj = "mpart"+ mb70 = 70 * 1024 * 1024++ step "Prepare for putObject with from source without providing size."+ rFile <- mkRandFile mb70++ step "Upload multipart file."+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions++ step "Retrieve and verify file size"+ destFile <- mkRandFile 0+ fGetObject bucket obj destFile defaultGetObjectOptions+ gotSize <- withNewHandle destFile getFileSize+ liftIO $+ gotSize == Right (Just mb70)+ @? "Wrong file size of put file after getting"++ step "Cleanup actions"+ deleteObject bucket obj++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", "o4"]+ extractObjectsFromList os =+ mapM+ ( \t -> case t of+ ListItemObject o -> Just $ oiObject o+ _ -> Nothing+ )+ os+ expectedNonRecList = ["o4", "dir/"]+ extractObjectsAndDirsFromList os =+ map+ ( \t -> case t of+ ListItemObject o -> oiObject o+ ListItemPrefix d -> d+ )+ os++ testFilepath <- mkRandFile 200+ forM_ expectedObjects $+ \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions++ step "High-level listing of objects"+ items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $+ extractObjectsAndDirsFromList items++ step "High-level recursive listing of objects"+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList++ liftIO+ $ assertEqual+ "Objects match failed!"+ (Just $ sort expectedObjects)+ $ extractObjectsFromList objects++ step "High-level listing of objects (version 1)"+ itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $+ extractObjectsAndDirsFromList itemsV1++ step "High-level recursive listing of objects (version 1)"+ objectsV1 <-+ C.runConduit $+ listObjectsV1 bucket Nothing True+ C..| sinkList++ liftIO+ $ assertEqual+ "Objects match failed!"+ (Just $ sort expectedObjects)+ $ extractObjectsFromList objectsV1++ let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]+ expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]+ step "High-level listing with prefix"+ prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList+ liftIO+ $ assertEqual+ "Objects/Dirs under prefix match failed!"+ expectedPrefListing+ $ extractObjectsAndDirsFromList prefItems++ step "High-level listing with prefix recursive"+ prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList+ liftIO+ $ assertEqual+ "Objects/Dirs under prefix match recursive failed!"+ expectedPrefListingRec+ $ extractObjectsFromList prefItemsRec++ step "High-level listing with prefix (version 1)"+ prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList+ liftIO+ $ assertEqual+ "Objects/Dirs under prefix match failed!"+ expectedPrefListing+ $ extractObjectsAndDirsFromList prefItemsV1++ step "High-level listing with prefix recursive (version 1)"+ prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList+ liftIO+ $ assertEqual+ "Objects/Dirs under prefix match recursive failed!"+ expectedPrefListingRec+ $ extractObjectsFromList prefItemsRecV1++ 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 <-+ C.runConduit $+ listIncompleteUploads bucket (Just "newmpupload") True+ C..| sinkList+ liftIO $ length uploads @?= 10++ step "cleanup"+ forM_ uploads $ \(UploadInfo _ uid _ _) ->+ abortMultipartUpload bucket object uid++ step "High-level listIncompleteParts Test"+ 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 mb5+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->+ forM_ [1 .. 10] $ \pnum ->+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5++ step "fetch list parts"+ incompleteParts <-+ C.runConduit $+ listIncompleteParts bucket object uid+ C..| 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 ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]++ testFilepath <- mkRandFile 200+ forM_ [1 .. 10 :: Int] $ \s ->+ fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions++ 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"+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing+ let expected =+ sort $+ map+ ( T.concat+ . ("test-file-" :)+ . (\x -> [x])+ . T.pack+ . show+ )+ [1 .. 10 :: Int]+ liftIO $+ assertEqual+ "Objects match failed!"+ expected+ (map oiObject $ lorObjects' resV1)++ step "Cleanup actions"+ forM_ objects $ \obj -> deleteObject bucket obj++ step "listIncompleteUploads' test"+ 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.")++ step "list incomplete multipart uploads"+ incompleteUploads <-+ listIncompleteUploads'+ bucket+ (Just "newmpupload")+ Nothing+ Nothing+ Nothing+ Nothing+ liftIO $ (length $ lurUploads incompleteUploads) @?= 10++ step "cleanup"+ forM_ (lurUploads incompleteUploads) $+ \(_, uid, _) -> abortMultipartUpload bucket object uid++ step "Basic listIncompleteParts Test"+ 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 mb5+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->+ forM_ [1 .. 10] $ \pnum ->+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5++ step "fetch list parts"+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing+ liftIO $ (length $ lprParts listPartsResult) @?= 10+ abortMultipartUpload bucket object uid++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 <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs+ liftIO $+ (bs == NC.responseBody getResp)+ @? "presigned put and get got mismatched data"++ step "PUT object presigned - presignedPutObjectURL"+ putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []++ 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+ let req = NC.parseRequest_ $ toS $ decodeUtf8 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 = H.fromList $ NC.responseHeaders headResp+ cLen = H.lookupDefault "0" HT.hContentLength h+ liftIO $ (cLen == showBS 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 <- C.runConduit $ CB.sourceFile testFile C..| 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+ let req = NC.parseRequest_ $ toS $ decodeUtf8 url+ let req' =+ req+ { NC.method = HT.methodPut,+ NC.requestBody =+ NC.requestBodySource size $+ CB.sourceFile filePath+ }+ NC.httpLbs req' mgr+ getR mgr url = do+ let req = NC.parseRequest_ $ toS $ decodeUtf8 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 $ decodeUtf8 url+ let parts =+ map (\(x, y) -> Form.partBS x y) $+ H.toList formData+ parts' = parts ++ [Form.partFile "file" inputFile]+ req' <- Form.formDataBody parts' req+ mgr <- NC.newManager NC.tlsManagerSettings+ NC.httpLbs req' mgr++bucketPolicyFunTest :: TestTree+bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $+ \step bucket -> do+ step "bucketPolicy basic test - no policy exception"+ resE <- try $ getBucketPolicy bucket+ case resE of+ Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"+ _ -> return ()++ resE' <- try $ setBucketPolicy bucket T.empty+ case resE' of+ Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"+ _ -> return ()++ let expectedPolicyJSON = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket/*\"]}]}"++ step "try a malformed policy, expect error"+ resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON+ case resE'' of+ Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"+ _ -> return ()++ let expectedPolicyJSON' = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "/*\"]}]}"++ step "set bucket policy"+ setBucketPolicy bucket expectedPolicyJSON'++ let obj = "myobject"++ step "verify bucket policy: (1) create `myobject`"+ putObject bucket obj (replicateC 100 "c") Nothing defaultPutObjectOptions++ step "verify bucket policy: (2) get `myobject` anonymously"+ connInfo <- asks mcConnInfo+ let proto = bool "http://" "https://" $ connectIsSecure connInfo+ url =+ BS.concat+ [ proto,+ getHostAddr connInfo,+ "/",+ toUtf8 bucket,+ "/",+ toUtf8 obj+ ]+ respE <-+ liftIO $+ (fmap (Right . toStrictBS) $ NC.simpleHttp $ toS $ decodeUtf8 url)+ `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))+ case respE of+ Left err -> liftIO $ assertFailure $ show err+ Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")++ deleteObject bucket obj++ step "delete bucket policy"+ setBucketPolicy bucket T.empty++multipartTest :: TestTree+multipartTest = funTestWithBucket "Multipart Tests" $+ \step bucket -> do+ -- Commenting out test since it's platform specific.+ -- FIXME: Need to find a platform agnostic way to test this.+ -- step "Prepare for putObjectInternal with non-seekable file, with size."+ -- step "Upload multipart file."+ -- let mb80 = 80 * 1024 * 1024+ -- obj = "mpart"++ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)++ -- step "Retrieve and verify file size"+ -- destFile <- mkRandFile 0+ -- fGetObject bucket obj destFile defaultGetObjectOptions+ -- gotSize <- withNewHandle destFile getFileSize+ -- liftIO $+ -- gotSize == Right (Just mb80)+ -- @? "Wrong file size of put file after getting"++ -- step "Cleanup actions"+ -- removeObject bucket obj++ -- step "cleanup"+ -- removeObject bucket "big"++ step "Prepare for removeIncompleteUpload"+ -- low-level multipart operation tests.+ let object = "newmpupload"+ kb5 = 5 * 1024++ step "create new multipart upload"+ uid <- newMultipartUpload bucket object []+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."++ randFile <- mkRandFile kb5++ step "upload 2 parts"+ forM_ [1, 2] $ \partNum -> do+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5++ step "remove ongoing upload"+ removeIncompleteUpload bucket object+ uploads <-+ C.runConduit $+ listIncompleteUploads bucket (Just object) False+ C..| sinkList+ liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"++putObjectContentTypeTest :: TestTree+putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $+ \step bucket -> do+ step "fPutObject content type test"+ let object = "xxx-content-type"+ size1 = 100 :: Int64++ step "create server object with content-type"+ inputFile <- mkRandFile size1+ fPutObject+ bucket+ object+ inputFile+ defaultPutObjectOptions+ { pooContentType = Just "application/javascript"+ }++ -- retrieve obj info to check+ oi <- headObject bucket object []+ let m = oiMetadata oi++ step "Validate content-type"+ liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)++ step "upload object with content-encoding set to identity"+ fPutObject+ bucket+ object+ inputFile+ defaultPutObjectOptions+ { pooContentEncoding = Just "identity"+ }++ oiCE <- headObject bucket object []+ let m' = oiMetadata oiCE++ step "Validate content-encoding"+ liftIO $+ assertEqual+ "Content-Encoding did not match"+ (Just "identity")+ (H.lookup "Content-Encoding" m')++ step "Cleanup actions"++ removeObject bucket object++putObjectContentLanguageTest :: TestTree+putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage tests" $+ \step bucket -> do+ step "fPutObject content language test"+ let object = "xxx-content-language"+ size1 = 100 :: Int64++ step "create server object with content-language"+ inputFile <- mkRandFile size1+ fPutObject+ bucket+ object+ inputFile+ defaultPutObjectOptions+ { pooContentLanguage = Just "en-US"+ }++ -- retrieve obj info to check+ oi <- headObject bucket object []+ let m = oiMetadata oi++ step "Validate content-language"+ liftIO $+ assertEqual+ "content-language did not match"+ (Just "en-US")+ (H.lookup "Content-Language" m)+ step "Cleanup actions"++ removeObject bucket object++putObjectUserMetadataTest :: TestTree+putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $+ \step bucket -> do+ step "putObject user-metadata test"+ let object = "object-with-metadata"+ size1 = 100 :: Int64++ step "create server object with usermetdata"+ inputFile <- mkRandFile size1++ fPutObject+ bucket+ object+ inputFile+ defaultPutObjectOptions+ { pooUserMetadata =+ [ ("x-Amz-meta-mykey1", "myval1"),+ ("mykey2", "myval2")+ ]+ }++ step "Validate user-metadata"+ -- retrieve obj info to check+ oi <- headObject bucket object []+ let m = oiUserMetadata oi+ -- need to do a case-insensitive comparison+ sortedMeta =+ sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $+ H.toList m+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]++ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"++ step "Cleanup actions"+ removeObject bucket object++getObjectTest :: TestTree+getObjectTest = funTestWithBucket "getObject test" $+ \step bucket -> do+ step "putObject with some metadata"+ let object = "object-with-metadata"+ size1 = 100 :: Int64++ inputFile <- mkRandFile size1+ fPutObject+ bucket+ object+ inputFile+ defaultPutObjectOptions+ { pooUserMetadata =+ [ ("x-Amz-meta-mykey1", "myval1"),+ ("mykey2", "myval2")+ ]+ }++ step "get the object - check the metadata matches"+ -- retrieve obj info to check+ gor <- getObject bucket object defaultGetObjectOptions+ let m = oiUserMetadata $ gorObjectInfo gor+ -- need to do a case-insensitive comparison+ sortedMeta =+ sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $+ H.toList m+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]++ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"++ step "get the object content"+ getObjectHash <- hashSHA256FromSource $ gorObjectStream gor+ inputHash <- hashSHA256FromSource $ C.sourceFile inputFile+ liftIO $ (getObjectHash == inputHash) @? "Input file and output file mismatched!"++ step "Cleanup actions"+ removeObject bucket object++putObjectStorageClassTest :: TestTree+putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $+ \step bucket -> do+ step "fPutObject storage class test"+ let object = "xxx-storage-class-standard"+ object' = "xxx-storage-class-reduced"+ object'' = "xxx-storage-class-invalid"+ size1 = 100 :: Int64+ size0 = 0 :: Int64++ step "create server objects with storageClass"+ inputFile <- mkRandFile size1+ inputFile' <- mkRandFile size1+ inputFile'' <- mkRandFile size0++ fPutObject+ bucket+ object+ inputFile+ defaultPutObjectOptions+ { pooStorageClass = Just "STANDARD"+ }++ fPutObject+ bucket+ object'+ inputFile'+ defaultPutObjectOptions+ { pooStorageClass = Just "REDUCED_REDUNDANCY"+ }++ removeObject bucket object++ -- retrieve obj info to check+ oi' <- headObject bucket object' []+ let m' = oiMetadata oi'++ step "Validate x-amz-storage-class rrs"+ liftIO $+ assertEqual+ "storageClass did not match"+ (Just "REDUCED_REDUNDANCY")+ (H.lookup "X-Amz-Storage-Class" m')++ fpE <-+ try $+ fPutObject+ bucket+ object''+ inputFile''+ defaultPutObjectOptions+ { pooStorageClass = Just "INVALID_STORAGE_CLASS"+ }+ case fpE of+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."+ _ -> return ()++ step "Cleanup actions"++ removeObject bucket object'++copyObjectTests :: TestTree+copyObjectTests = funTestWithBucket "copyObject related tests" $+ \step bucket -> do+ step "copyObjectSingle basic tests"+ let object = "xxx"+ objCopy = "xxxCopy"+ size1 = 100 :: Int64++ step "create server object to copy"+ inputFile <- mkRandFile size1+ fPutObject bucket object inputFile defaultPutObjectOptions++ step "copy object"+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}+ (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []++ -- retrieve obj info to check+ oi <- headObject bucket objCopy []+ let t = oiModTime oi+ let e = oiETag oi+ let s = oiSize oi++ let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0++ liftIO $+ (s == size1 && e == etag && isMTimeDiffOk)+ @? "Copied object did not match expected."++ step "cleanup actions"+ removeObject bucket object+ removeObject bucket objCopy++ step "copyObjectPart basic tests"+ let srcObj = "XXX"+ copyObj = "XXXCopy"++ step "Prepare"+ let mb15 = 15 * 1024 * 1024+ mb5 = 5 * 1024 * 1024+ randFile <- mkRandFile mb15+ fPutObject bucket srcObj randFile defaultPutObjectOptions++ step "create new multipart upload"+ uid <- newMultipartUpload bucket copyObj []+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."++ step "put object parts 1-3"+ let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}+ dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}+ parts <- forM [1 .. 3] $ \p -> do+ (etag', _) <-+ copyObjectPart+ dstInfo'+ srcInfo'+ { srcRange = Just $ (,) ((p -1) * mb5) ((p -1) * mb5 + (mb5 - 1))+ }+ uid+ (fromIntegral p)+ []+ return (fromIntegral p, etag')++ step "complete multipart"+ void $ completeMultipartUpload bucket copyObj uid parts++ step "verify copied object size"+ oi' <- headObject bucket copyObj []+ let s' = oiSize oi'++ liftIO $ (s' == mb15) @? "Size failed to match"++ step "Cleanup actions"+ removeObject bucket srcObj+ removeObject bucket copyObj++ step "copyObject basic tests"+ let srcs = ["XXX", "XXXL"]+ copyObjs = ["XXXCopy", "XXXLCopy"]+ sizes = map (* (1024 * 1024)) [15, 65]++ step "Prepare"+ forM_ (zip srcs sizes) $ \(src, size) -> do+ inputFile' <- mkRandFile size+ fPutObject bucket src inputFile' defaultPutObjectOptions++ step "make small and large object copy"+ forM_ (zip copyObjs srcs) $ \(cp, src) ->+ copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = cp} defaultSourceInfo {srcBucket = bucket, srcObject = src}++ step "verify uploaded objects"+ uploadedSizes <- fmap oiSize <$> forM copyObjs (\o -> headObject bucket o [])++ liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match"++ forM_ (srcs ++ copyObjs) (removeObject bucket)++ step "copyObject with offset test "+ let src = "XXX"+ size = 15 * 1024 * 1024++ step "Prepare"+ inputFile' <- mkRandFile size+ fPutObject bucket src inputFile' defaultPutObjectOptions++ step "copy last 10MiB of object"+ copyObject+ defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}+ defaultSourceInfo+ { srcBucket = bucket,+ srcObject = src,+ srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)+ }++ step "verify uploaded object"+ cSize <- oiSize <$> headObject bucket copyObj []++ liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"++ forM_ [src, copyObj] (removeObject bucket)++getNPutSSECTest :: TestTree+getNPutSSECTest =+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do+ -- Skip this test if the server is not using TLS as encryption is+ -- disabled anyway.+ isTLSConn <- asks (connectIsSecure . mcConnInfo)+ if isTLSConn+ then do+ step "Make an encryption key"+ key <- case mkSSECKey $ BS.pack [0 .. 31] of+ Nothing -> liftIO $ assertFailure "This should not happen"+ Just k -> return k++ let mb1 = 1024 * 1024+ obj = "1"+ step "Upload an object using the encryption key"+ rFile <- mkRandFile mb1+ let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}+ fPutObject bucket obj rFile putOpts++ step "Stat object without key - should fail"+ headRes <- try $ statObject bucket obj defaultGetObjectOptions+ case headRes of+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))+ | NC.responseStatus rsp == HT.status400 -> return ()+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex++ step "Get file and check length"+ dstFile <- mkRandFile 0+ let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}+ fGetObject bucket obj dstFile getOpts++ gotSize <- withNewHandle dstFile getFileSize+ liftIO $+ gotSize == Right (Just mb1)+ @? "Wrong file size of object when getting"++ step "Cleanup"+ deleteObject bucket obj+ else step "Skipping encryption test as server is not using TLS"
test/Network/Minio/API/Test.hs view
@@ -15,88 +15,100 @@ -- module Network.Minio.API.Test- ( bucketNameValidityTests- , objectNameValidityTests- , parseServerInfoJSONTest- , parseHealStatusTest- , parseHealStartRespTest- ) where--import Data.Aeson (eitherDecode)-import Test.Tasty-import Test.Tasty.HUnit--import Lib.Prelude+ ( bucketNameValidityTests,+ objectNameValidityTests,+ parseServerInfoJSONTest,+ parseHealStatusTest,+ parseHealStartRespTest,+ )+where -import Network.Minio.AdminAPI-import Network.Minio.API+import Data.Aeson (eitherDecode)+import Lib.Prelude+import Network.Minio.API+import Network.Minio.AdminAPI+import Test.Tasty+import Test.Tasty.HUnit assertBool' :: Bool -> Assertion assertBool' = assertBool "Test failed!" bucketNameValidityTests :: TestTree-bucketNameValidityTests = testGroup "Bucket Name Validity Tests"- [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""- , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"- , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"- , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"- , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"- , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"- , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"- , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"- , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"- , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"- , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"- ]+bucketNameValidityTests =+ testGroup+ "Bucket Name Validity Tests"+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",+ testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",+ testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",+ testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",+ testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",+ testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",+ testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",+ testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",+ testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",+ testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",+ testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"+ ] objectNameValidityTests :: TestTree-objectNameValidityTests = testGroup "Object Name Validity Tests"- [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""- , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"- ]+objectNameValidityTests =+ testGroup+ "Object Name Validity Tests"+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",+ testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"+ ] parseServerInfoJSONTest :: TestTree-parseServerInfoJSONTest = testGroup "Parse MinIO Admin API ServerInfo JSON test" $- map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $- tfn (eitherDecode tVal :: Either [Char] [ServerInfo])) testCases+parseServerInfoJSONTest =+ testGroup "Parse MinIO Admin API ServerInfo JSON test" $+ map+ ( \(tName, tDesc, tfn, tVal) ->+ testCase tName $ assertBool tDesc $+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])+ )+ testCases where- testCases = [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON)- , ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON)- , ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)- ]+ testCases =+ [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),+ ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),+ ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)+ ] fsJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":20530,\"Backend\":{\"Type\":1,\"OnlineDisks\":0,\"OfflineDisks\":0,\"StandardSCData\":0,\"StandardSCParity\":0,\"RRSCData\":0,\"RRSCParity\":0,\"Sets\":null}},\"network\":{\"transferred\":808,\"received\":1160},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":5992503019270,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"- erasureJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":2,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"- invalidJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":42,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]" parseHealStatusTest :: TestTree-parseHealStatusTest = testGroup "Parse MinIO Admin API HealStatus JSON test" $- map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $- tfn (eitherDecode tVal :: Either [Char] HealStatus)) testCases-+parseHealStatusTest =+ testGroup "Parse MinIO Admin API HealStatus JSON test" $+ map+ ( \(tName, tDesc, tfn, tVal) ->+ testCase tName $ assertBool tDesc $+ tfn (eitherDecode tVal :: Either [Char] HealStatus)+ )+ testCases where- testCases = [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON')- , ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON')- , ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)- ]-+ testCases =+ [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),+ ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),+ ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)+ ] erasureJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"- invalidJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]"- invalidItemType = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"hello\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}" parseHealStartRespTest :: TestTree-parseHealStartRespTest = testGroup "Parse MinIO Admin API HealStartResp JSON test" $- map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $- tfn (eitherDecode tVal :: Either [Char] HealStartResp)) testCases-+parseHealStartRespTest =+ testGroup "Parse MinIO Admin API HealStartResp JSON test" $+ map+ ( \(tName, tDesc, tfn, tVal) ->+ testCase tName $ assertBool tDesc $+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)+ )+ testCases where- testCases = [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON)- , ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)- ]-+ testCases =+ [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),+ ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)+ ] hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"- missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hs view
@@ -15,23 +15,23 @@ -- module Network.Minio.JsonParser.Test- (- jsonParserTests- ) where--import Test.Tasty-import Test.Tasty.HUnit-import UnliftIO (MonadUnliftIO)--import Lib.Prelude+ ( jsonParserTests,+ )+where -import Network.Minio.Errors-import Network.Minio.JsonParser+import Lib.Prelude+import Network.Minio.Errors+import Network.Minio.JsonParser+import Test.Tasty+import Test.Tasty.HUnit+import UnliftIO (MonadUnliftIO) jsonParserTests :: TestTree-jsonParserTests = testGroup "JSON Parser Tests"- [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON- ]+jsonParserTests =+ testGroup+ "JSON Parser Tests"+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON+ ] tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a) tryValidationErr act = try act@@ -43,22 +43,21 @@ testParseErrResponseJSON = do -- 1. Test parsing of an invalid error json. parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"- when (isRight parseResE) $- assertFailure $ "Parsing should have failed => " ++ show parseResE+ when (isRight parseResE)+ $ assertFailure+ $ "Parsing should have failed => " ++ show parseResE forM_ cases $ \(jsondata, sErr) -> do parseErr <- tryValidationErr $ parseErrResponseJSON jsondata either assertValidationErr (@?= sErr) parseErr- where- cases = [- -- 2. Test parsing of a valid error json.- ("{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",- ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."- )- ,- -- 3. Test parsing of a valid, empty Resource.- ("{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",- ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."- )+ cases =+ [ -- 2. Test parsing of a valid error json.+ ( "{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."+ ),+ -- 3. Test parsing of a valid, empty Resource.+ ( "{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."+ ) ]
test/Network/Minio/TestHelpers.hs view
@@ -15,18 +15,19 @@ -- module Network.Minio.TestHelpers- ( runTestNS- ) where--import Network.Minio.Data+ ( runTestNS,+ )+where -import Lib.Prelude+import Lib.Prelude+import Network.Minio.Data -newtype TestNS = TestNS { testNamespace :: Text }+newtype TestNS = TestNS {testNamespace :: Text} instance HasSvcNamespace TestNS where getSvcNamespace = testNamespace runTestNS :: ReaderT TestNS m a -> m a-runTestNS = flip runReaderT $- TestNS "http://s3.amazonaws.com/doc/2006-03-01/"+runTestNS =+ flip runReaderT $+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs view
@@ -15,33 +15,32 @@ -- module Network.Minio.Utils.Test- (- limitedMapConcurrentlyTests- ) where--import Test.Tasty-import Test.Tasty.HUnit--import Lib.Prelude+ ( limitedMapConcurrentlyTests,+ )+where -import Network.Minio.Utils+import Lib.Prelude+import Network.Minio.Utils+import Test.Tasty+import Test.Tasty.HUnit limitedMapConcurrentlyTests :: TestTree-limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"- [ testCase "Test with various thread counts" testLMC- ]+limitedMapConcurrentlyTests =+ testGroup+ "limitedMapConcurrently Tests"+ [ testCase "Test with various thread counts" testLMC+ ] testLMC :: Assertion testLMC = do let maxNum = 50 -- test with thread count of 1 to 2*maxNum- forM_ [1..(2*maxNum)] $ \threads -> do- res <- limitedMapConcurrently threads compute [1..maxNum]+ forM_ [1 .. (2 * maxNum)] $ \threads -> do+ res <- limitedMapConcurrently threads compute [1 .. maxNum] sum res @?= overallResultCheck maxNum where -- simple function to run in each thread compute :: Int -> IO Int- compute n = return $ sum [1..n]-+ compute n = return $ sum [1 .. n] -- function to check overall result- overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]+ overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
test/Network/Minio/XmlGenerator/Test.hs view
@@ -13,30 +13,31 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE QuasiQuotes #-}-module Network.Minio.XmlGenerator.Test- ( xmlGeneratorTests- ) where -import Test.Tasty-import Test.Tasty.HUnit-import Text.RawString.QQ (r)--import Lib.Prelude+module Network.Minio.XmlGenerator.Test+ ( xmlGeneratorTests,+ )+where -import Network.Minio.Data-import Network.Minio.TestHelpers-import Network.Minio.XmlGenerator-import Network.Minio.XmlParser (parseNotification)+import Lib.Prelude+import Network.Minio.Data+import Network.Minio.TestHelpers+import Network.Minio.XmlGenerator+import Network.Minio.XmlParser (parseNotification)+import Test.Tasty+import Test.Tasty.HUnit+import Text.RawString.QQ (r) xmlGeneratorTests :: TestTree-xmlGeneratorTests = testGroup "XML Generator Tests"- [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig- , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest- , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest- , testCase "Test mkSelectRequest" testMkSelectRequest- ]+xmlGeneratorTests =+ testGroup+ "XML Generator Tests"+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,+ testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,+ testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,+ testCase "Test mkSelectRequest" testMkSelectRequest+ ] testMkCreateBucketConfig :: Assertion testMkCreateBucketConfig = do@@ -44,100 +45,129 @@ assertEqual "CreateBucketConfiguration xml should match: " expected $ mkCreateBucketConfig ns "EU" where- expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\- \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \<LocationConstraint>EU</LocationConstraint>\- \</CreateBucketConfiguration>"+ expected =+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \<LocationConstraint>EU</LocationConstraint>\+ \</CreateBucketConfiguration>" testMkCompleteMultipartUploadRequest :: Assertion testMkCompleteMultipartUploadRequest = assertEqual "completeMultipartUpload xml should match: " expected $- mkCompleteMultipartUploadRequest [(1, "abc")]+ mkCompleteMultipartUploadRequest [(1, "abc")] where- expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\- \<CompleteMultipartUpload>\- \<Part>\- \<PartNumber>1</PartNumber><ETag>abc</ETag>\- \</Part>\- \</CompleteMultipartUpload>"-+ expected =+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<CompleteMultipartUpload>\+ \<Part>\+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\+ \</Part>\+ \</CompleteMultipartUpload>" testMkPutNotificationRequest :: Assertion testMkPutNotificationRequest = forM_ cases $ \val -> do let ns = "http://s3.amazonaws.com/doc/2006-03-01/"- result = toS $ mkPutNotificationRequest ns val+ result = fromStrictBS $ mkPutNotificationRequest ns val ntf <- runExceptT $ runTestNS $ parseNotification result- either (\_ -> assertFailure "XML Parse Error!")- (@?= val) ntf+ either+ (\_ -> assertFailure "XML Parse Error!")+ (@?= val)+ ntf where- cases = [ Notification []- [ NotificationConfig- "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"- "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"- [ReducedRedundancyLostObject, ObjectCreated] defaultFilter- ]- []- , 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] defaultFilter- ]- [ NotificationConfig- "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"- [ReducedRedundancyLostObject] defaultFilter- ]- [ NotificationConfig- "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"- [ObjectCreated] defaultFilter- ]- ]+ cases =+ [ Notification+ []+ [ NotificationConfig+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"+ [ReducedRedundancyLostObject, ObjectCreated]+ defaultFilter+ ]+ [],+ 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]+ defaultFilter+ ]+ [ NotificationConfig+ ""+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"+ [ReducedRedundancyLostObject]+ defaultFilter+ ]+ [ NotificationConfig+ "ObjectCreatedEvents"+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"+ [ObjectCreated]+ defaultFilter+ ]+ ] testMkSelectRequest :: Assertion testMkSelectRequest = mapM_ assertFn cases where assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a- cases = [ ( SelectRequest "Select * from S3Object" SQL- (InputSerialization (Just CompressionTypeGzip)- (InputFormatCSV $ fileHeaderInfo FileHeaderIgnore- <> recordDelimiter "\n"- <> fieldDelimiter ","- <> quoteCharacter "\""- <> quoteEscapeCharacter "\""- ))- (OutputSerializationCSV $ quoteFields QuoteFieldsAsNeeded- <> recordDelimiter "\n"- <> fieldDelimiter ","- <> quoteCharacter "\""- <> quoteEscapeCharacter "\""+ cases =+ [ ( SelectRequest+ "Select * from S3Object"+ SQL+ ( InputSerialization+ (Just CompressionTypeGzip)+ ( InputFormatCSV $+ fileHeaderInfo FileHeaderIgnore+ <> recordDelimiter "\n"+ <> fieldDelimiter ","+ <> quoteCharacter "\""+ <> quoteEscapeCharacter "\"" )- (Just False)- , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>"</QuoteCharacter><RecordDelimiter>+ )+ ( OutputSerializationCSV $+ quoteFields QuoteFieldsAsNeeded+ <> recordDelimiter "\n"+ <> fieldDelimiter ","+ <> quoteCharacter "\""+ <> quoteEscapeCharacter "\""+ )+ (Just False),+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>"</QuoteCharacter><RecordDelimiter> </RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>"</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>"</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter> </RecordDelimiter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]- )- , ( setRequestProgressEnabled False $- setInputCompressionType CompressionTypeGzip $- selectRequest "Select * from S3Object" documentJsonInput- (outputJSONFromRecordDelimiter "\n")- , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>+ ),+ ( setRequestProgressEnabled False+ $ setInputCompressionType CompressionTypeGzip+ $ selectRequest+ "Select * from S3Object"+ documentJsonInput+ (outputJSONFromRecordDelimiter "\n"),+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter> </RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]- )- , ( setRequestProgressEnabled False $- setInputCompressionType CompressionTypeNone $- selectRequest "Select * from S3Object" defaultParquetInput- (outputCSVFromProps $ quoteFields QuoteFieldsAsNeeded- <> recordDelimiter "\n"- <> fieldDelimiter ","- <> quoteCharacter "\""- <> quoteEscapeCharacter "\"")- , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><QuoteCharacter>"</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>+ ),+ ( setRequestProgressEnabled False+ $ setInputCompressionType CompressionTypeNone+ $ selectRequest+ "Select * from S3Object"+ defaultParquetInput+ ( outputCSVFromProps $+ quoteFields QuoteFieldsAsNeeded+ <> recordDelimiter "\n"+ <> fieldDelimiter ","+ <> quoteCharacter "\""+ <> quoteEscapeCharacter "\""+ ),+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><QuoteCharacter>"</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter> </RecordDelimiter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]- )- ]+ )+ ]
test/Network/Minio/XmlParser/Test.hs view
@@ -13,39 +13,40 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- {-# LANGUAGE QuasiQuotes #-}-module Network.Minio.XmlParser.Test- ( xmlParserTests- ) where -import qualified Data.HashMap.Strict as H-import Data.Time (fromGregorian)-import Test.Tasty-import Test.Tasty.HUnit-import Text.RawString.QQ (r)-import UnliftIO (MonadUnliftIO)--import Lib.Prelude+module Network.Minio.XmlParser.Test+ ( xmlParserTests,+ )+where -import Network.Minio.Data-import Network.Minio.Errors-import Network.Minio.TestHelpers-import Network.Minio.XmlParser+import qualified Data.HashMap.Strict as H+import Data.Time (fromGregorian)+import Lib.Prelude+import Network.Minio.Data+import Network.Minio.Errors+import Network.Minio.TestHelpers+import Network.Minio.XmlParser+import Test.Tasty+import Test.Tasty.HUnit+import Text.RawString.QQ (r)+import UnliftIO (MonadUnliftIO) xmlParserTests :: TestTree-xmlParserTests = testGroup "XML Parser Tests"- [ 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- , testCase "Test parseSelectProgress" testParseSelectProgress- ]+xmlParserTests =+ testGroup+ "XML Parser Tests"+ [ 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,+ testCase "Test parseSelectProgress" testParseSelectProgress+ ] tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a) tryValidationErr act = try act@@ -54,232 +55,232 @@ 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 testParseLocation = do -- 1. Test parsing of an invalid location constraint xml. parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"- when (isRight parseResE) $- assertFailure $ "Parsing should have failed => " ++ show parseResE+ when (isRight parseResE)+ $ assertFailure+ $ "Parsing should have failed => " ++ show parseResE forM_ cases $ \(xmldata, expectedLocation) -> do parseLocE <- tryValidationErr $ parseLocation xmldata either assertValidtionErr (@?= expectedLocation) parseLocE where- cases = [- -- 2. Test parsing of a valid location xml.- ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\- \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",- "EU"- )- ,- -- 3. Test parsing of a valid, empty location xml.- ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",- "us-east-1"- )+ cases =+ [ -- 2. Test parsing of a valid location xml.+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",+ "EU"+ ),+ -- 3. Test parsing of a valid, empty location xml.+ ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",+ "us-east-1"+ ) ] - testParseNewMultipartUpload :: Assertion testParseNewMultipartUpload = do forM_ cases $ \(xmldata, expectedUploadId) -> do parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata eitherValidationErr parsedUploadIdE (@?= expectedUploadId) where- cases = [- ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\- \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \ <Bucket>example-bucket</Bucket>\- \ <Key>example-object</Key>\- \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\- \</InitiateMultipartUploadResult>",- "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"- ),- ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\- \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \ <Bucket>example-bucket</Bucket>\- \ <Key>example-object</Key>\- \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\- \</InitiateMultipartUploadResult>",- "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"- )+ cases =+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \ <Bucket>example-bucket</Bucket>\+ \ <Key>example-object</Key>\+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\+ \</InitiateMultipartUploadResult>",+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"+ ),+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \ <Bucket>example-bucket</Bucket>\+ \ <Key>example-object</Key>\+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\+ \</InitiateMultipartUploadResult>",+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"+ ) ] testParseListObjectsResult :: Assertion testParseListObjectsResult = do- let- xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\- \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \<Name>bucket</Name>\- \<Prefix/>\- \<NextContinuationToken>opaque</NextContinuationToken>\- \<KeyCount>1000</KeyCount>\- \<MaxKeys>1000</MaxKeys>\- \<IsTruncated>true</IsTruncated>\- \<Contents>\- \<Key>my-image.jpg</Key>\- \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\- \<ETag>"fba9dede5f27731c9771645a39863328"</ETag>\- \<Size>434234</Size>\- \<StorageClass>STANDARD</StorageClass>\- \</Contents>\- \</ListBucketResult>"-- expectedListResult = ListObjectsResult True (Just "opaque") [object1] []- object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty- modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12+ let xmldata =+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \<Name>bucket</Name>\+ \<Prefix/>\+ \<NextContinuationToken>opaque</NextContinuationToken>\+ \<KeyCount>1000</KeyCount>\+ \<MaxKeys>1000</MaxKeys>\+ \<IsTruncated>true</IsTruncated>\+ \<Contents>\+ \<Key>my-image.jpg</Key>\+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\+ \<ETag>"fba9dede5f27731c9771645a39863328"</ETag>\+ \<Size>434234</Size>\+ \<StorageClass>STANDARD</StorageClass>\+ \</Contents>\+ \</ListBucketResult>"+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12 parsedListObjectsResult <- tryValidationErr $ runTestNS $ 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>"fba9dede5f27731c9771645a39863328"</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 H.empty H.empty- modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12+ 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>"fba9dede5f27731c9771645a39863328"</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 H.empty H.empty+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult) testParseListIncompleteUploads :: Assertion testParseListIncompleteUploads = do- let- xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \<Bucket>example-bucket</Bucket>\- \<KeyMarker/>\- \<UploadIdMarker/>\- \<NextKeyMarker>sample.jpg</NextKeyMarker>\- \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\- \<Delimiter>/</Delimiter>\- \<Prefix/>\- \<MaxUploads>1000</MaxUploads>\- \<IsTruncated>false</IsTruncated>\- \<Upload>\- \<Key>sample.jpg</Key>\- \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\- \<Initiator>\- \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\- \<DisplayName>s3-nickname</DisplayName>\- \</Initiator>\- \<Owner>\- \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\- \<DisplayName>s3-nickname</DisplayName>\- \</Owner>\- \<StorageClass>STANDARD</StorageClass>\- \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\- \</Upload>\- \<CommonPrefixes>\- \<Prefix>photos/</Prefix>\- \</CommonPrefixes>\- \<CommonPrefixes>\- \<Prefix>videos/</Prefix>\- \</CommonPrefixes>\- \</ListMultipartUploadsResult>"- expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes- uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]- initTime = UTCTime (fromGregorian 2010 11 26) 69857- prefixes = ["photos/", "videos/"]+ let xmldata =+ "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \<Bucket>example-bucket</Bucket>\+ \<KeyMarker/>\+ \<UploadIdMarker/>\+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\+ \<Delimiter>/</Delimiter>\+ \<Prefix/>\+ \<MaxUploads>1000</MaxUploads>\+ \<IsTruncated>false</IsTruncated>\+ \<Upload>\+ \<Key>sample.jpg</Key>\+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\+ \<Initiator>\+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\+ \<DisplayName>s3-nickname</DisplayName>\+ \</Initiator>\+ \<Owner>\+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\+ \<DisplayName>s3-nickname</DisplayName>\+ \</Owner>\+ \<StorageClass>STANDARD</StorageClass>\+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\+ \</Upload>\+ \<CommonPrefixes>\+ \<Prefix>photos/</Prefix>\+ \</CommonPrefixes>\+ \<CommonPrefixes>\+ \<Prefix>videos/</Prefix>\+ \</CommonPrefixes>\+ \</ListMultipartUploadsResult>"+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]+ initTime = UTCTime (fromGregorian 2010 11 26) 69857+ prefixes = ["photos/", "videos/"] parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata eitherValidationErr parsedListUploadsResult (@?= expectedListResult) - testParseCompleteMultipartUploadResponse :: Assertion testParseCompleteMultipartUploadResponse = do- let- xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\-\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\- \<Bucket>Example-Bucket</Bucket>\- \<Key>Example-Object</Key>\- \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\-\</CompleteMultipartUploadResult>"- expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""+ let xmldata =+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\+ \<Bucket>Example-Bucket</Bucket>\+ \<Key>Example-Object</Key>\+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\+ \</CompleteMultipartUploadResult>"+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\"" parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata eitherValidationErr parsedETagE (@?= expectedETag) testParseListPartsResponse :: Assertion testParseListPartsResponse = do- let- xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\-\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \<Bucket>example-bucket</Bucket>\- \<Key>example-object</Key>\- \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\- \<Initiator>\- \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\- \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\- \</Initiator>\- \<Owner>\- \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\- \<DisplayName>someName</DisplayName>\- \</Owner>\- \<StorageClass>STANDARD</StorageClass>\- \<PartNumberMarker>1</PartNumberMarker>\- \<NextPartNumberMarker>3</NextPartNumberMarker>\- \<MaxParts>2</MaxParts>\- \<IsTruncated>true</IsTruncated>\- \<Part>\- \<PartNumber>2</PartNumber>\- \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\- \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\- \<Size>10485760</Size>\- \</Part>\- \<Part>\- \<PartNumber>3</PartNumber>\- \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\- \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\- \<Size>10485760</Size>\- \</Part>\-\</ListPartsResult>"-- expectedListResult = ListPartsResult True (Just 3) [part1, part2]- part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1- modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10- part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2- modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10+ let xmldata =+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \<Bucket>example-bucket</Bucket>\+ \<Key>example-object</Key>\+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\+ \<Initiator>\+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\+ \</Initiator>\+ \<Owner>\+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\+ \<DisplayName>someName</DisplayName>\+ \</Owner>\+ \<StorageClass>STANDARD</StorageClass>\+ \<PartNumberMarker>1</PartNumberMarker>\+ \<NextPartNumberMarker>3</NextPartNumberMarker>\+ \<MaxParts>2</MaxParts>\+ \<IsTruncated>true</IsTruncated>\+ \<Part>\+ \<PartNumber>2</PartNumber>\+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\+ \<Size>10485760</Size>\+ \</Part>\+ \<Part>\+ \<PartNumber>3</PartNumber>\+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\+ \<Size>10485760</Size>\+ \</Part>\+ \</ListPartsResult>"+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata eitherValidationErr parsedListPartsResult (@?= expectedListResult) testParseCopyObjectResponse :: Assertion testParseCopyObjectResponse = do- let- cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\-\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\- \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\-\</CopyObjectResult>",- ("\"9b2cf535f27731c974343645a3985328\"",- UTCTime (fromGregorian 2009 10 28) 81120))- , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\-\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\- \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\- \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\-\</CopyPartResult>",- ("\"9b2cf535f27731c974343645a3985328\"",- UTCTime (fromGregorian 2009 10 28) 81120))]+ let cases =+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\+ \</CopyObjectResult>",+ ( "\"9b2cf535f27731c974343645a3985328\"",+ UTCTime (fromGregorian 2009 10 28) 81120+ )+ ),+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\+ \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\+ \</CopyPartResult>",+ ( "\"9b2cf535f27731c974343645a3985328\"",+ UTCTime (fromGregorian 2009 10 28) 81120+ )+ )+ ] forM_ cases $ \(xmldata, (etag, modTime)) -> do parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata@@ -287,73 +288,88 @@ 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+ 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] defaultFilter- ]- [])- , ("<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] defaultFilter- ]- [ NotificationConfig- "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"- [ReducedRedundancyLostObject] defaultFilter- ]- [ NotificationConfig- "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"- [ObjectCreated] defaultFilter- ])- ]+ [ReducedRedundancyLostObject, ObjectCreated]+ defaultFilter+ ]+ []+ ),+ ( "<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]+ defaultFilter+ ]+ [ NotificationConfig+ ""+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"+ [ReducedRedundancyLostObject]+ defaultFilter+ ]+ [ NotificationConfig+ "ObjectCreatedEvents"+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"+ [ObjectCreated]+ defaultFilter+ ]+ )+ ] forM_ cases $ \(xmldata, val) -> do result <- runExceptT $ runTestNS $ parseNotification xmldata@@ -362,20 +378,25 @@ -- | Tests parsing of both progress and stats testParseSelectProgress :: Assertion testParseSelectProgress = do- let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>+ let cases =+ [ ( [r|<?xml version="1.0" encoding="UTF-8"?> <Progress> <BytesScanned>512</BytesScanned> <BytesProcessed>1024</BytesProcessed> <BytesReturned>1024</BytesReturned>-</Progress>|] , Progress 512 1024 1024)- , ([r|<?xml version="1.0" encoding="UTF-8"?>+</Progress>|],+ Progress 512 1024 1024+ ),+ ( [r|<?xml version="1.0" encoding="UTF-8"?> <Stats> <BytesScanned>512</BytesScanned> <BytesProcessed>1024</BytesProcessed> <BytesReturned>1024</BytesReturned>-</Stats>|], Progress 512 1024 1024)- ]+</Stats>|],+ Progress 512 1024 1024+ )+ ] - forM_ cases $ \(xmldata, progress) -> do- result <- runExceptT $ parseSelectProgress xmldata- eitherValidationErr result (@?= progress)+ forM_ cases $ \(xmldata, progress) -> do+ result <- runExceptT $ parseSelectProgress xmldata+ eitherValidationErr result (@?= progress)
test/Spec.hs view
@@ -14,21 +14,18 @@ -- limitations under the License. -- -import Test.Tasty-import Test.Tasty.QuickCheck as QC--import qualified Data.ByteString as B-import qualified Data.List as L--import Lib.Prelude--import Network.Minio.API.Test-import Network.Minio.CopyObject-import Network.Minio.Data-import Network.Minio.PutObject-import Network.Minio.Utils.Test-import Network.Minio.XmlGenerator.Test-import Network.Minio.XmlParser.Test+import qualified Data.ByteString as B+import qualified Data.List as L+import Lib.Prelude+import Network.Minio.API.Test+import Network.Minio.CopyObject+import Network.Minio.Data+import Network.Minio.PutObject+import Network.Minio.Utils.Test+import Network.Minio.XmlGenerator.Test+import Network.Minio.XmlParser.Test+import Test.Tasty+import Test.Tasty.QuickCheck as QC main :: IO () main = defaultMain tests@@ -51,82 +48,84 @@ -- ] qcProps :: TestTree-qcProps = testGroup "(checked by QuickCheck)"- [ QC.testProperty "selectPartSizes:" $- \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)-+qcProps =+ testGroup+ "(checked by QuickCheck)"+ [ QC.testProperty "selectPartSizes:" $+ \n ->+ let (pns, offs, sizes) = L.unzip3 (selectPartSizes n) -- check that pns increments from 1.- isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]-- consPairs [] = []- consPairs [_] = []- consPairs (a:(b:c)) = (a, b):(consPairs (b:c))-+ isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1 ..]+ consPairs [] = []+ consPairs [_] = []+ consPairs (a : (b : c)) = (a, b) : (consPairs (b : c)) -- check `offs` is monotonically increasing. isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs- -- check sizes sums to n. isSumSizeOk = sum sizes == n- -- check sizes are constant except last isSizesConstantExceptLast = all (\(a, b) -> a == b) (consPairs $ L.init sizes)- -- check each part except last is at least minPartSize; -- last part may be 0 only if it is the only part. nparts = length sizes isMinPartSizeOk =- if | nparts > 1 -> -- last part can be smaller but > 0- all (>= minPartSize) (take (nparts - 1) sizes) &&- all (\s -> s > 0) (drop (nparts - 1) sizes)- | nparts == 1 -> -- size may be 0 here.- maybe True (\x -> x >= 0 && x <= minPartSize) $- headMay sizes- | otherwise -> False-- in n < 0 ||- (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&- isSizesConstantExceptLast && isMinPartSizeOk)-- , QC.testProperty "selectCopyRanges:" $- \(start, end) ->- let (_, pairs) = L.unzip (selectCopyRanges (start, end))-- -- is last part's snd offset end?- isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs- -- is first part's fst offset start- isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs-- -- each pair is >=64MiB except last, and all those parts- -- have same size.- initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs- isPartSizesOk = all (>= minPartSize) initSizes &&- maybe True (\k -> all (== k) initSizes)- (headMay initSizes)-- -- returned offsets are contiguous.- fsts = drop 1 $ map fst pairs- snds = take (length pairs - 1) $ map snd pairs- isContParts = length fsts == length snds &&- and (map (\(a, b) -> a == b + 1) $ zip fsts snds)-- in start < 0 || start > end ||- (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)-- , QC.testProperty "mkSSECKey:" $- \w8s -> let bs = B.pack w8s- r = mkSSECKey bs- in case r of- Just _ -> B.length bs == 32+ if+ | nparts > 1 -> -- last part can be smaller but > 0+ all (>= minPartSize) (take (nparts - 1) sizes)+ && all (\s -> s > 0) (drop (nparts - 1) sizes)+ | nparts == 1 -> -- size may be 0 here.+ maybe True (\x -> x >= 0 && x <= minPartSize) $+ headMay sizes+ | otherwise -> False+ in n < 0+ || ( isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk+ && isSizesConstantExceptLast+ && isMinPartSizeOk+ ),+ QC.testProperty "selectCopyRanges:" $+ \(start, end) ->+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))+ -- is last part's snd offset end?+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs+ -- is first part's fst offset start+ isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs+ -- each pair is >=64MiB except last, and all those parts+ -- have same size.+ initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs+ isPartSizesOk =+ all (>= minPartSize) initSizes+ && maybe+ True+ (\k -> all (== k) initSizes)+ (headMay initSizes)+ -- returned offsets are contiguous.+ fsts = drop 1 $ map fst pairs+ snds = take (length pairs - 1) $ map snd pairs+ isContParts =+ length fsts == length snds+ && and (map (\(a, b) -> a == b + 1) $ zip fsts snds)+ in start < 0 || start > end+ || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),+ QC.testProperty "mkSSECKey:" $+ \w8s ->+ let bs = B.pack w8s+ r = mkSSECKey bs+ in case r of+ Just _ -> B.length bs == 32 Nothing -> B.length bs /= 32- ]+ ] unitTests :: TestTree-unitTests = testGroup "Unit tests" [ xmlGeneratorTests, xmlParserTests- , bucketNameValidityTests- , objectNameValidityTests- , parseServerInfoJSONTest- , parseHealStatusTest- , parseHealStartRespTest- , limitedMapConcurrentlyTests- ]+unitTests =+ testGroup+ "Unit tests"+ [ xmlGeneratorTests,+ xmlParserTests,+ bucketNameValidityTests,+ objectNameValidityTests,+ parseServerInfoJSONTest,+ parseHealStatusTest,+ parseHealStartRespTest,+ limitedMapConcurrentlyTests+ ]